置顶
菜鸟入门,各位大佬轻喷,如有谬误之处欢迎讨论建议,也欢迎各位道友与我同行
“不积跬步,无以至千里;不积小流,无以成江海”
继续
上文中我们解决了 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
,尚不清楚会导致那些影响,后续再研究。 - 在与朋友
@公众号【蜗牛iOS】
交流时,得知可以使用extension
来实现自定义事件,查了一下extension
的作用是用来进行扩展,按理说它不应该超出类和结构体本身的范围,后续再进行研究。
网友评论