菜鸟入门,各位大佬轻喷,如有谬误之处欢迎讨论建议,也欢迎各位道友与我同行
“不积跬步,无以至千里;不积小流,无以成江海”
继续
上文中我们解决了 List 组件中 Section 分组导致的 onDelete 位置错误的 bug,并总结了调试的几种方法。
本文中我们将继续实现 todo 的详情表单,即在 List 中点击每一项弹出一个 todo 的表单,里面可以修改 todo 的名称等。
最终效果如下:
在这里插入图片描述
方案思考
点击todo项显示一个 sheet ,这些是之前已经有的内容,不再赘述。
很显然问题在于 TodoView.swift 里面的被点击项的数据怎么传入到详情中,并且详情中被修改的数据怎么传出来
根据之前的内容,首先想到的是利用 @EnvironmentObject 获取到 TodoLists 的实例,如此只需要传入一个 item.id ,在详情页面中即可定位到 todo 项的数据进行展示以及修改。
但是在实际使用中发现 .sheet 调用的组件中并不能使用 @EnvironmentObject 拿到全局变量,此方案只能作废。
针对这个问题我在网上搜索了一大圈,只得到一个结论:确实不支持,也许是为了隔断 sheet弹出之后的数据交互
既然如此,就只能以传参-事件的方式进行操作了。
准备工作
既然是要修改 todo 项,那么 TodoModel 里面理应有一个 update 方法:
// TodoModel.swift import SwiftUI; // 。。。省略 TodoItem 的 struct 定义 // ObservableObject 代表这是一个可以被观察的对象 class TodoLists : ObservableObject { // 。。。省略已有的部分 // 修改item func update(item:TodoItem){ // 找到 item 的序号 let index = todoList.firstIndex(where: {$0.id == item.id}) // 修改 item todoList[index!] = item; } }
自定义事件方案一:构造Binding (不推荐)
按照之前的文章所介绍,我们可以利用构造 Binding 的方式来实现类似 vue 里面的 watch 监控变量
所以我们可以构造一个 Binding 将其传入 TodoItemView 中,代码如下:
// TodoView.swift import SwiftUI struct TodoView: View { // 。。。省略 // 是否显示详情 @State private var showDetail:Bool = false; // 显示的详情数据 @State private var showItem:TodoItem = TodoItem(name: "test"); // todo项分组 func todoSectionView(isFinished:Bool = false) -> some View{ return Section(isFinished ? "已完成":"未完成") { ForEach(todos.todoList.filter{(item) -> Bool in return item.isFinished == isFinished; }){ item in todoItemView(item: item) .contentShape(Rectangle()) // 添加点击事件 .onTapGesture { showItem = item; showDetail = true; } }.onDelete{ IndexSet in todos.delete(offsets: IndexSet,isFinished: isFinished) } } } // 。。。省略todo项 var body: some View { // 构造Binding的方式走事件 let showItemBinding = Binding<TodoItem>(get: { return self.showItem; }, set: { if($0.id == showItem.id){ // 如果发生变化的是正在修改的那一条,就执行修改 todos.update(item: $0) } showItem = $0 }) VStack{ // 。。。省略内容 }.sheet(isPresented: $showDetail, content: { TodoItemView(showItem:showItemBinding) }) } }
新增一个 TodoItemView.swift 文件,作为 todo item 的详情表单使用
import SwiftUI; struct TodoItemView: View{ // 要处理的todo项内容 @Binding var showItem:TodoItem; var body: some View{ Spacer() Text("Todo详情").fontWeight(.bold).font(.title) Spacer() HStack{ Text("内容") Spacer() TextEditor(text:$showItem.name) .multilineTextAlignment(.center) .border(.gray) }.padding(.all) Spacer() } }
以上实现运行正常,但是在运行的时候发现 Xcode 报了一个 warning
2022-11-30 14:01:35.243189+0800 helloworld[9977:315519] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
意思为从子级 view 中发布修改到父级 view 中是不被允许的,会导致未定义的行为。
所以这种方案作为备选,应优先考虑以事件的方式进行传输。
自定义事件方案二:传入一个func作为参数
思考一下 vue 中的子父组件事件交互,实质上是在父组件中定义了一个方法,将其当做参数传入了子组件,当子组件需要触发事件的时候,调用这个方法,就相当于事件被父组件接收。
那么我们来实现这种形式,首先修改 TodoItemView.swift,让它可以接收这个方法。
// TodoItemView.swift import SwiftUI; struct TodoItemView: View{ // 要处理的todo项,此时就不需要Binding了 @State var showItem:TodoItem; // _ 代表在调用时这个参数可以没有外部名称,即可以直接调用 action(item),它一定是一个 TodoItem // 这是一个显式定义的事件,在外部传递了一个方法作为参数进来 @State var action:(_ item:TodoItem) -> Void; var body: some View{ Spacer() Text("Todo详情").fontWeight(.bold).font(.title) Spacer() HStack{ Text("内容") Spacer() TextEditor(text:$showItem.name) .multilineTextAlignment(.center) .border(.gray) .onChange(of: showItem, perform: {value in // 一旦值发生变化,那么直接调用这个方法 // 相当于触发事件 action(showItem) }) }.padding(.all) Spacer() } }
然后,修改父组件中传入事件参数的地方
// TodoView.swift // 。。。省略前面的代码 .sheet(isPresented: $showDetail, content: { TodoItemView(showItem:showItem,action: {item in // 这里是个方法,action相当于就是事件名,一旦触发这个事件 // 就执行 todos的update 方法 todos.update(item: item) }) })
最后进行运行,功能实现正常,没有再出现 warning。
总结
SwiftUI 的文档中没有关于自定义事件的说法,但究其原理,其实就是把方法当做参数传递。
Binding 类型传递到子组件中,虽功能运行正常,但会报出 warning,尚不清楚会导致那些影响,后续再研究。
在与朋友 交流时,得知可以使用 extension 来实现自定义事件,查了一下 extension 的作用是用来进行扩展,按理说它不应该超出类和结构体本身的范围,后续再进行研究。