List 列表

List 的作用是提供一个滚动的数据表:


展示静态数据列表

List 列表可以用来放置静态数据。

List {
	Text("Line One")
  Text("Line Two")
  Button("Click Here", action: {})
	  .foregroundColor(.green)
  HStack {
    Text("Centered Text")
  }
}

展示动态数据列表

// 结合 ForEach 创建动态数据内容
List {
	// 第1个参数: 传给ForEach一个范围值,参数index也是从1到4
	// 第2个参数: ID识别码设定为前面的值本身
	ForEach(1...4, id: \\.self){ index in
		Text("Item \\(index)")
	}
}
// 脱离 ForEach 直接创建动态数据内容
// 1.去掉了ForEach,直接把参数写到List上
// 2.省略index参数名称,使用缩写$0来代替($0表示闭包第1个参数)
List(1...4, id: \\.self){
		Text("Item \\($0)")
	}
}

静态 / 动态数据结合展示

由于 List 能够同时包含静态和动态内容,就可以利用它创建类似 Wi-Fi 设置界面的内容:一个用于启用 Wi-Fi 的开关、一个展示附近网络的动态列表、再加上一些自动加入热点选项等更静态的内容等。

// List有趣的是可以将静态数据和动态相结合
List {
    Text("Static row 1")
    Text("Static row 2")

    ForEach(0..<5) {
        Text("Dynamic row \\($0)")
    }

    Text("Static row 3")
    Text("Static row 4")
}
// 结合上 Section 使用让代码可读性更强
List {
    Section("Section 1") {
        Text("Static row 1")
        Text("Static row 2")
    }

    Section("Section 2") {
        ForEach(0..<5) {
            Text("Dynamic row \\($0)")
        }
    }

    Section("Section 3") {
        Text("Static row 3")
        Text("Static row 4")
    }
}

从普通数组创建 List

// 例如:有两个数组
var resNames = []
var resImages = []

// 首先:传入给List的不是固定范围,而是一个数组,但是加了.indices;indices是一个属性,它用于获取数组索引范围(Range)的一种便捷方式。具体来说,它返回一个表示数组索引范围的,可以用于遍历数组的索引。 返回的是从0到数组的元素个数减1的一个范围,通常用于循环遍历数组的索引。所以后面的 index 就是从 indices 里循环取值,自然取的也是1、2、3...这样的整数
// 然后:.self 代表用resName中各个元素的字符串本身,作为唯一标识
List(resNames.indices, id: \\.self){ index in
		Image(self.resImages[index])
		Text(self.resNames[index])
	}
}

从复杂数据数组创建 List

当遇到上面同一个事物的两个属性,分成两个数组存储的情况,更好的方式是建立数据模型进行存储。因此可以建立一个struct。有个问题是如果用 name 属性作为唯一标识,如果发生两个餐厅重名的情况,它们的图片也会一样,这是不希望发生的。因此应该给每个餐厅一个唯一的标识,而不是用名字。代码如下:

关于使用 UUID 作为唯一标识的说明,具体参见:参数:ID

// 1: 让餐厅结构遵循 Identifiable 协议,该协议只有一个要求,即该类型必须具备某种ID作为唯一识别码
// 2: 因此给每个餐厅生成一个唯一的ID,用UUID函数产生通用的唯一标识。
struct Restaurant: Identifiable {
	var id = UUID()
	var name: String
	var image: String
}

// 构建数组
var restaurants = [
		Restaurant(name:"xxxx", image:"xxxxxx")
		Restaurant(name:"xxxx", image:"xxxxxx")
		Restaurant(name:"xxxx", image:"xxxxxx")
		......
]

// 读取
// 这时传入List的不是一个范围了,而是一个数组,并且是用它内部的id属性作为唯一标识
List(restaurants, id: \\.id){ restaurant in
		// 传入的是数组,所以循环的是单个数组内的元素
		// 所以取值是也是单个元素的某属性,不需要索引值
		Image(restaurant.image)
		Text(restaurant.name)
	}
}

创建可展开的 List

可展开的  List 视图指的是,列表显示一级视图,并且在一级视图旁边有一个可点击的箭头,点击后列表会展开以显示其里面的子元素。要使用这种形式的 List ,需要具有精确形式的数据:数据模型应该有一个可选的相同类型的子项数组,以便可以创建一棵树。

一旦数据就位,您可以通过传入【数据数组】以及【子级所在位置的键路径】(例子中为 \\.items )将其加载到列表中。然后,您将获得一个常规闭包,您可以在其中提供行数据,就像平常一样。

// 例如有以下数据
struct Bookmark: Identifiable {
    let id = UUID()
    let name: String
    let icon: String
    var items: [Bookmark]?

    // some example websites
    static let apple = Bookmark(name: "Apple", icon: "1.circle")
    static let bbc = Bookmark(name: "BBC", icon: "square.and.pencil")
    static let swift = Bookmark(name: "Swift", icon: "bolt.fill")
    static let twitter = Bookmark(name: "Twitter", icon: "mic")

    // some example groups
    static let example1 = Bookmark(name: "Favorites", icon: "star", items: [Bookmark.apple, Bookmark.bbc, Bookmark.swift, Bookmark.twitter])
    static let example2 = Bookmark(name: "Recent", icon: "timer", items: [Bookmark.apple, Bookmark.bbc, Bookmark.swift, Bookmark.twitter])
    static let example3 = Bookmark(name: "Recommended", icon: "hand.thumbsup", items: [Bookmark.apple, Bookmark.bbc, Bookmark.swift, Bookmark.twitter])
}

struct ContentView: View {
    let items: [Bookmark] = [.example1, .example2, .example3]
    var body: some View {
        List(items, children: \\.items) { row in
            HStack {
                Image(systemName: row.icon)
                Text(row.name)
            }
        }
    }
}

列表的样式

使用 listStyle 设置全局样式

当希望快速设置列表的整体样式时,可以使用 listStyle 修饰符,从名字上看就知道它是附加到 List 后面的(不同于 ListRow)

List{ ... }
.listStyle(.plain)
.listStyle(.grouped)

// 其他支持的样式还有:.sidebar  .grouped  .plain  .insetGrouped  .inset

添加 Section 区分不同区域

列表中可以添加 Section 区分不同的部分,并且可添加页眉、页脚视图

List {
    Section(header: Text("Other tasks"), footer: Text("End")) {
        Text("Row 1")
        Text("Row 2")
        Text("Row 3")
    }
}

// 默认情况下,节标题将匹配默认的 iOS 样式,但您可以使用 headerProminence() 修饰符并指定更大、更粗的节文本:
List {
    Section(header: Text("Header")) {
        Text("Row")
    }
    .headerProminence(.increased)
}
.listStyle(.insetGrouped)

列表的行为

1. 滚动到特定行

如果想以编程方式使 SwiftUI 的 List 滚动到特定行,应该将其嵌入到 ScrollViewReader 中。这在其代理上提供了一个 scrollTo() 方法,只需提供其 ID 和可选的锚点即可移动到列表内的任何行。如果在 withAnimation() 内调用 scrollTo() ,滑动还会有动画。

具体参照:ScrollViewReader

2. 启用下拉刷新

refreshable() 修饰符可将此功能附加到 List ,以便在用户向下拖动足够远时触发。只要代码完成运行,iOS 就会自动显示活动指示器。

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List(1..<100) { row in
                Text("Row \\(row)")
            }
            .refreshable {
                print("Do your refresh work here")
            }
        }
    }
}

放置在 refreshable() 中的代码,会在异步任务中运行,因此这里是放置加载网络内容的完美位置。例如下面的示例,它使用 pull 刷新将一些新闻报道下载到 List 中:

struct NewsItem: Decodable, Identifiable {
    let id: Int
    let title: String
    let strap: String
}

struct ContentView: View {

    @State private var news = [
        NewsItem(id: 0, title: "Want the latest news?", strap: "Pull to refresh!")
    ]

    var body: some View {
        NavigationStack {
            List(news) { item in
                VStack(alignment: .leading) {
                    Text(item.title)
                        .font(.headline)
                    Text(item.strap)
                        .foregroundStyle(.secondary)
                }
            }
            .refreshable {
                do {
                    // Fetch and decode JSON into news items
                    let url = URL(string: "<https://www.hackingwithswift.com/samples/news-1.json>")!
                    let (data, _) = try await URLSession.shared.data(from: url)
                    news = try JSONDecoder().decode([NewsItem].self, from: data)
                } catch {
                    // Something went wrong; clear the news
                    news = []
                }
            }
        }
    }
}

3. 从绑定创建 List/ForEach

SwiftUI 允许我们直接从绑定创建 List 或 ForEach ,然后可以实现后续的内容闭包,与显示的数据集合中的每个单独元素进行绑定。当每个项目视图需要与其某些数据绑定时(例如具有用于编辑用户名的文本字段的列表行),这非常有用。

要使用它,请将绑定直接传递到列表中,例如 $users ,然后接受内容闭包中的绑定,例如 $user 。例如,在此代码中,我们显示用户列表,并向每行添加 Toggle 以确定是否已联系他们:

struct User: Identifiable {
    let id = UUID()
    var name: String
    var isContacted = false
}

struct ContentView: View {

    @State private var users = [
        User(name: "Taylor"),
        User(name: "Justin"),
        User(name: "Adele")
    ]

    var body: some View {
        List($users) { $user in
            Text(user.name)
            Spacer()
            Toggle("User has been contacted", isOn: $user.isContacted)
                .labelsHidden()
        }
    }
    
}

// 以这种方式使用绑定是修改列表的最有效方法,因为当只有单个项目更改时,它不会导致整个视图重新加载。