在用Swift编程语言的SwiftUI包设计苹果设备的程序时,经常会用到ForEach函数。这个函数的作用是将一个数据集里面的内容一条一条地取出,罗列在程序的页面上,使用方式的详解见[1]。
但ForEach和一般的循环不同之处在于它要求输入里面的数据集里元素必须是Identifiable的,否则不可使用[2]。所谓Identifiable,就是说输入ForEach里的数据集里的每一个元素必须有一个唯一确定的,不会重复的id号,所以通过该id号,就可找到唯一确定的与之对应的元素,因此若要修改或删除元素,不会删错或在修改时涉及无关的元素。
一、可输入ForEach的数据
本文以一个简单的例子,说明什么样的数据可以输入到ForEach中。在该例子中,输入的数据集合里的每个元素都是一个字母,这些元素被一一加入到List里,形成一个列表如图显示。
下面说明几种将数据输入ForEach的方法:
(一)将字符串列表直接输入ForEach
Swift
@State var alphaList = ["a", "b", "c"]
var body: some View {
List{
Section(header: Text("Invaild list")){
ForEach(alphaList){alphaEle in
Text(alphaEle)
}
} //This is not legal. ForEach should be inputed as a range, or an identifiable. normal list is not ok
}
}
这一段代码在Swift中将无法运行,因为Swift里的列表里的元素只是字符串,而这些元素未指定id号,所以Swift里无法根据元素的任何信息唯一确定该元素。
所以,不能直接将字符串的列表输入ForEach。
(二)将字符串元素的字符串本身设为其在列表中的id
Swift
@State var alphaList = ["a", "b", "c"]
var body: some View {
List{
Section(header: Text("Normal list")){
ForEach(alphaList, id: \.self){alphaEle in
//alphaList itself is not identifiable, so need to define id. Here the id is just the element title. This is not good because the id can repeat
Text(alphaEle)
}
}
}
}
这段代码可以正常运行。因为在ForEach函数里,虽然输入的数据集未能提供每个元素的id,但在ForEach函数的id参数里,对这个信息进行了补充,使用\.self这个Key Path表明每个元素在这个数据集的id号就是这个字符串本身。关于Key Path的概念,见[3]。另外,博文[4]中讲解了Key Path如何使用。
这个方法虽然可行,但并不建议,因为不同元素的字符串本身一旦出现重复,Swift就无法唯一确定每一个id对应的元素了。
(三)直接用区间作为索引号数据集,然后根据索引号提取元素
Swift
@State var alphaList2 = ["a", "b", "c"]
var body: some View {
List{
Section(header: Text("Normal list")){
ForEach(0..<alphaList2.count){idx in
Text(alphaList2[idx])
//This is not good. ForEach, if using a integer range, the range should be constant.
}
}
}
Button(action: {
alphaList2.append("ff")
}, label: {
Text("Add for extract idx")
})//This button will fail.
}
这段代码可以正常运行,因为Swift里的ForEach函数支持区间输入。但这样的输入,要求区间必须固定不变。如果在该程序运行时,alphaList2是一个固定不变的列表,那么这样使用ForEach函数是可以的。但如果在程序运行中,需要添加或删除元素,则不应使用区间输入。
在以上代码中,界面上除了定义一个ForEach的List外,还定义了一个按钮,按下后就会在列表中添加元素。但这样的编程方式,按钮按下后,屏幕上也不会有任何变化,因为ForEach函数如果输入的是区间,则不支持变动的区间。
从动画中可看出,无论如何点击按钮"Add for extract idx",列表里的内容都没有变化。
(四)用区间作为索引号数据集,但添加索引号作为id
Swift
@State var alphaList3 = ["a", "b", "c"]
var body: some View {
List{
Section(header: Text("Extract Idx with id")){
ForEach(0..<alphaList3.count, id: \.self){idx in
Text(alphaList3[idx])
//this is good, because although integer range is used, an id is specified so that the whole input together can be an identifiable
}
}
}
Button(action: {
alphaList3.append("ff")
}, label: {
Text("Add for extract idx with id")
})
}
这段代码可以正常运行,而且列表添加可以正常进行,因为输入ForEach的区间里的每一个元素已经被赋予了id。
从动画中可看出,点击按钮"Add for extract idx with id"后,列表会被添加。
(五)创建一个Identifiable的类,让元素使用这个类
Swift
class alpha: Identifiable{
public var letter:String
init(_ l:String){
letter = l
}
}
@State var alphaList4 = [alpha("a"), alpha("b"), alpha("c")]
var body: some View {
List{
Section(header: Text("identifiable letter")){
ForEach(alphaList4){alphaEle in
Text(alphaEle.letter)
//this is good, because alphaList4 is identifiable
}
}
}
Button(action: {
alphaList4.append(alpha("ff"))
}, label: {
Text("Add for identifiable objects")
})
}
在这段代码中,alphaList4里面的每一个元素都是Identifiable的alpha类元素,所以alphaList4可以直接输入ForEach函数。该代码可以正常运行,且列表添加功能可正常使用。
(六)仍然使用方法(一)但把String类型延伸一个id
Swift中可以对一个已有类型添加一个extension,从而扩充它的属性[5]。这里对String进行扩充。
Swift
extension String: Identifiable{
public var id: String {UUID().description}
//public var id: String{self} //This kind of id is not suggested
}
这样一来,方法(一)就不再报错了。
二、整个程序及总结
Swift
import SwiftUI
class alpha: Identifiable{
public var letter:String
init(_ l:String){
letter = l
}
}
extension String: Identifiable{
public var id: String {UUID().description}
//public var id: String{self} //This kind of id is not suggested
}
struct ListLab: View {
@State var alphaList = ["a", "b", "c"]
@State var alphaList2 = ["a", "b", "c"]
@State var alphaList3 = ["a", "b", "c"]
@State var alphaList4 = [alpha("a"), alpha("b"), alpha("c")]
@State var alphaList5 = ["a", "b", "c"]
var body: some View {
List{
//Section(header: Text("Invaild list")){
// ForEach(alphaList){alphaEle in
// Text(alphaEle)
// }
//} //This is not legal. ForEach should be inputed as a range, or an identifiable. normal list is not ok
Section(header: Text("Normal list")){
ForEach(alphaList, id: \.self){alphaEle in
//alphaList itself is not identifiable, so need to define id. Here the id is just the element title. This is not good because the id can repeat
Text(alphaEle)
}
}
Section(header: Text("Extract Idx")){
ForEach(0..<alphaList2.count){idx in
Text(alphaList2[idx])
//This is not good. ForEach, if using a integer range, the range should be constant.
}
}
Section(header: Text("Extract Idx with id")){
ForEach(0..<alphaList3.count, id: \.self){idx in
Text(alphaList3[idx])
//this is good, because although integer range is used, an id is specified so that the whole input together can be an identifiable
}
}
Section(header: Text("identifiable letter")){
ForEach(alphaList4){alphaEle in
Text(alphaEle.letter)
//this is good, because alphaList4 is identifiable
}
}
Section(header: Text("identifiable letter with UUID")){
ForEach(alphaList5){alphaEle in
Text(alphaEle)
}
}
}
Button(action: {
alphaList.append("ff")
}, label: {
Text("Add for normal list")
})
Button(action: {
alphaList2.append("ff")
}, label: {
Text("Add for extract idx")
})//This button will fail.
Button(action: {
alphaList3.append("ff")
}, label: {
Text("Add for extract idx with id")
})
Button(action: {
alphaList4.append(alpha("ff"))
}, label: {
Text("Add for identifiable objects")
})
Button(action: {
alphaList5.append("ff")
}, label: {
Text("Add for identifiable objects with uuid")
})
}
}
struct ListLab_Previews: PreviewProvider {
static var previews: some View {
ListLab()
}
}
总之,在SwiftUI中,输入ForEach的数据集里的元素必须Identifiable,即有独一无二的id属性。如果数据本身没有这样的属性,则需要通过函数的id参数自定义属性。
参考资料
[1]ForEach | Apple Developer Documentation