SwiftUI入门 - 10.封装、传参、@EnvironmentObject

置顶

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

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

继续

上文中已经实现将 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 时弹出一个表单,并在表单中使用这个环境对象,以及找出要编辑的对象,将数据回传。

这些内容下章再进行讨论。

总结

  1. 函数式的组件片段封装与 react 中的渲染逻辑比较类似,可以把某一段view分离出来。
  2. 既然是函数式的封装,那么参数的传递自然遵从函数的参数传递方法。
  3. 暂时没有考虑事件的传递,既然一切都可以是数据,那么完全可以把事件视作一个数据的变化,有数据的子父级影响和全局影响我觉得大部分的场景已经足够了。
  4. @EnvironmentObject 还有很多其他的用法,例如关闭 Sheet 等,前文中已有使用,此处不做赘述。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我码玄黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值