置顶
菜鸟入门,各位大佬轻喷,如有谬误之处欢迎讨论建议,也欢迎各位道友与我同行
“不积跬步,无以至千里;不积小流,无以成江海”
继续
上文中已经实现将 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
等,前文中已有使用,此处不做赘述。