SwiftUI里的ForEach使用的注意事项

在用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](https://developer.apple.com/documentation/swiftui/foreach "ForEach | Apple Developer Documentation") \[2\][SwiftUI 基础篇之 ForEach](https://juejin.cn/post/6984753286058344456 "SwiftUI 基础篇之 ForEach") \[3\][Documentation (Key path)](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions/#Key-Path-Expression "Documentation (Key path)") \[4\][Swift 中强大的 Key Paths(键路径)机制趣谈(上)_swift keypath-CSDN博客](https://blog.csdn.net/mydo/article/details/140093658 "Swift 中强大的 Key Paths(键路径)机制趣谈(上)_swift keypath-CSDN博客") \[5\][Swift - 基础之extension - 简书](https://www.jianshu.com/p/783df05a9b59 "Swift - 基础之extension - 简书")

相关推荐
自学AI的鲨鱼儿25 分钟前
mac npm 安装 codex 报错 npm ENOTEMPTY
macos·npm·codex
Digitally3 小时前
如何将真我(realme)手机数据传输至 iPhone
ios·智能手机·iphone
Sephiroth.Ma8 小时前
Mac 提示“Docker 已损坏,无法打开”?我这样排查后 10 分钟修好
macos·docker·容器
量子炒饭大师8 小时前
【OpenClaw修炼宝典】—— 【macOS安装篇】想玩《爪子船长》复刻版却卡在安装?OpenClaw 从零环境搭建与编译全攻略 (小白避坑指南)
macos·openclaw·小龙虾·龙虾
JFSJHFZJ8 小时前
解密iPhone核心技术,读懂苹果的硬实力
ios·cocoa·iphone
不才小强9 小时前
macOS 屏幕录制开发完全指南:ScreenCaptureKit与音频采集实战
macos·音视频
JXSJHF10 小时前
iPhone隐藏功能大盘点,免费好用不占内存
ios·iphone
ShiLuoHeroKing18 小时前
Mole:面向专业用户的Mac系统清理开源方案
macos
ZZH_AI项目交付1 天前
为什么很多复杂跳转,最后都得先回首页?
flutter·ios
vx-bot5556661 天前
企业微信ipad协议在客户画像构建中的应用实践
ios·企业微信·ipad