置顶
菜鸟入门,各位大佬轻喷,如有谬误之处欢迎讨论建议,也欢迎各位道友与我同行
“不积跬步,无以至千里;不积小流,无以成江海”
继续
上文中已经实现将 TODO 项分组,已完成的 todo 和未完成的 todo 理应分开展示。
并且在 todo 项为空的时候进行提示。
并且根据这个分组,我们已经将设置页面做了出来,类似于iOS原生的设置界面。
但是上文的实现中有一个问题,即两个分组的代码重复了。
所以,本文我们将进行封装,既然要封装,那么必然会涉及到传参的问题。
本次操作不会对UI和交互发生改变,因此本次没有演示图片,部分的演示放到了部分的讲解中。
最简单的封装:函数
观察我们实现的 TodoView.swift 页面,我们可以发现,主要重复的地方在于两个 Section 中。
其实两个 Section 基本上是一致的,只是对数据的过滤方式不一样。
所以我们可以将 Section 封装为一个函数。
同时,我们也能够看到,TodoItem 里面的层级过多,会导致很多个缩进,缩进最里面的代码,已经跑到了很右边去了,这很显然看着很难受。
因此我们可以将 TodoItem 也抽象为一个函数,由 Section 函数进行调用。
这样代码量进一步减少,并且相较之前更加直观、美观。
最终实现代码如下 :
import SwiftUI struct TodoView: View { // 省略一堆变量定义。。。 // 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 方法了 todoItemView(item: item) .contentShape(Rectangle()) .onTapGesture { // todos.toggle(item: item) showId = item.id; showDetail = true; } // 这个调用将实现横滑删除功能 }.onDelete{ IndexSet in todos.delete(offsets: IndexSet,isFinished: isFinished) } } } // todo项,将原来的 Todo 项的内容放到这儿来 func todoItemView(item:TodoItem) -> some View{ return HStack{ VStack{ HStack{ Text("\(item.name)") Spacer() } HStack{ Text("\(item.createdAt)").font(.subheadline) Spacer() } }.foregroundColor(item.isFinished ? .gray : .primary) Group{ item.isFinished ? Image(systemName: "circle.fill") : Image(systemName: "circle") }.onTapGesture { todos.toggle(item: item) } } } var body: some View { VStack{ // ...省略顶部的输入框部分 // 如果有todo项的时候才显示todo列表,否则提示没有数据 if(todos.todoList.count > 0){ List{ todoSectionView(isFinished: false); todoSectionView(isFinished: true ); }.animation(.default,value:todos.todoList) }else{ Text("请添加TODO项").foregroundColor(.gray) Spacer() } }.sheet(isPresented: $showDetail, content: { Text("String(showId)"); }) } } // ... 省略previewView 定义部分
组件封装:普通传参,父传子
首先,我们的 TodoView.swift 既是页面,同时也可以当做组件,它被 IndexView.swift 所调用。
然后,我们现在从 IndexView.swift 中传入一个title 到 TodoView.swfit 中,作为 section 的前缀名称使用
第一步
在IndexView.swift 中应该有一个传入的变量,给 TodoView.swfit
import SwiftUI struct IndexView: View{ // 。。。省略部分变量定义 // 给一个变量,用于传值给子组件 @State private var test:String = "test"; var body: some View{ // 。。。 省略 TabView { // 向子组件传参 TodoView(title:test) .tabItem { Image(systemName: "list.dash") Text("TODO") }.tag(0) .environmentObject(todos) SettingView() .tabItem { Image(systemName: "gear.circle") Text("设置") }.tag(1) } .font(.headline) } // 。。。省略 } }
第二步:在TodoView.swift 中应该有一个变量,接收 IndexView.swfit 传入的变量
struct TodoView: View { // 。。。省略无关变量定义部分 // 接收父组件传入的 title,一定要是个 public,不然外面没法传 @State public var title:String = "test"; // todo项分组 func todoSectionView(isFinished:Bool = false) -> some View{ return Section(isFinished ? title:"未完成") { // 。。。省略 } } // 。。。省略主体部分
最终效果如下:

组件传参:子传父
子传父时我们可以利用 @Binding 的特性,让子组件对变量的操作可以响应到父组件中
第一步:父组件传入一个 @Binding
import SwiftUI struct IndexView: View{ // 。。。省略部分变量定义 // 给一个变量,用于传值给子组件 @State private var test:String = "test"; var body: some View{ // 。。。 省略 TabView { // 向子组件传参 TodoView(title:$test) .tabItem { Image(systemName: "list.dash") Text(test) // 让这个变量显示出来 } } .font(.headline) } // 。。。省略 } }
第二步:子组件中接收 @Binding 参数
struct TodoView: View { // 。。。省略参数定义 // @Binding 也是一个 public,同时不能定义默认值,否则会报错 @Binding public var title:String; // 。。。省略 var body: some View { VStack{ HStack{ // 我们将 title 绑定到输入框中以便观察效果 TextField("请输入新的TODO",text:$title).onSubmit { todos.add(name: newItem) newItem = "" } Button("添加"){ todos.add(name: newItem) newItem = "" } }.padding() } // 。。。省略 }
得到以下结果
可以看到,TextField的绑定值的变化,同时影响了section的标题和父组件中TabItem的标题
组件传参:@EnvironmentObject
以上我们已经有了父子传递,那么假设我们现在有这么一个需求:
点击 TodoItem 的时候需要弹出一个表单,用来展示 TodoItem的所有信息,并且组件内所有的数据修改都会影响到点击的哪一条 TodoItem。
当然,我们可以只用 @Binding 传递,一个参数一个参数地处理,这很显然不是一个很好的处理方式。
最好的办法是让 TodoItem 的表单和外面可以共用一份数据,这样,List 就只需要传一个 id 到表单内部即可,由表单自己去处理。
此时,我们可以借助 @EnvironmentObject进行传递,顾名思义,这是一个环境对象,一旦有所引用,大家都是同一份数据模型。
第一步:定义 @EnvironmentObject
import SwiftUI struct IndexView: View{ // 省略。。。 let todos = TodoLists(todoList: []) var body: some View{ // 省略。。。 VStack{ // 一个简单的tabview,底部导航栏 TabView { TodoView() .tabItem { Image(systemName: "list.dash") Text("TODO") }.tag(0) // 此处将环境对象带上去 .environmentObject(todos) // 省略。。。 } .font(.headline) } // 省略。。。 } }
第二步 :子组件中获取
import SwiftUI struct TodoView: View { // 使用 @EnvironmentObject 获取即可 @EnvironmentObject var todos:TodoLists; // 省略。。。 }
本次修改不会对项目的UI和交互等造成任何影响
接下来将在 TodoView 中在点击 TodoItem 时弹出一个表单,并在表单中使用这个环境对象,以及找出要编辑的对象,将数据回传。
这些内容下章再进行讨论。
总结
函数式的组件片段封装与 react 中的渲染逻辑比较类似,可以把某一段view分离出来。
既然是函数式的封装,那么参数的传递自然遵从函数的参数传递方法。
暂时没有考虑事件的传递,既然一切都可以是数据,那么完全可以把事件视作一个数据的变化,有数据的子父级影响和全局影响我觉得大部分的场景已经足够了。
@EnvironmentObject 还有很多其他的用法,例如关闭 Sheet 等,前文中已有使用,此处不做赘述。