置顶

菜鸟入门,各位大佬轻喷,如有谬误之处欢迎讨论建议,也欢迎各位道友与我同行

“不积跬步,无以至千里;不积小流,无以成江海”

继续

上文中已经实现将 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 等,前文中已有使用,此处不做赘述。

文章链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。