Section 19 : Combine and Edit Data

Section 19 : Combine and Edit Data - 使用 Combine 和编辑数据(12’38")

Learn how to manipulate external data with built-in functions.

学习使用内置函数处理外部数据。

1. 关于 Combine

Combine 是 Swift 中提供的框架,通过组合事件处理操作符自定义异步事件的处理。

Combine框架提供了一个声明性的 Swift API,用于处理随时间变化的值。这些值可以表示多种异步事件。Combine 声明*publisher(发布者)以公开随时间变化的值,而subscriber(订阅者)*从发布者接收这些值。

  • Publisher 协议声明了一种类型,这种类型可以随时间传递一系列值。Publisher 用“操作符(operator)”作用于从上游 publisher 那里收到的值并重新发布它们。
  • 在发布链的末端,Subscriber在接收元素时对其进行操作。Publisher 仅在 subscriber 显式请求时才发出值。这将使 subscriber 代码参与控制从所连接的发布者接收事件的速度。

某些基础类型通过发布者暴露功能,包括TimeNotificationCenterURLSession等。Combine 还为符合键值观察的任何属性提供一个内置发布程序。

可以组合多个发布者的输出并协调它们的交互。例如,可以从 Text Field 的发布者订阅更新,并使用文本执行URL请求。然后,可以使用另一个发布者来处理响应,并用来更新应用程序。

通过采用Combine,可以将事件处理代码集中起来,并消除诸如嵌套闭包和基于约定的回调等麻烦的技术,从而使代码更易于阅读和维护。

2. 关于 ObservableObject

ObervableObject 是一个协议,表示在对象更改之前由发布者发出的对象类型。

默认情况下,ObservableObject对象合成一个 objectWillChange发布者,后者会将其使用 @Published声明的属性发生变化之前发布变动的值。

class Contact: ObservableObject {
    @Published var name: String
    @Published var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    func haveBirthday() -> Int {
        age += 1
        return age
    }
}

let john = Contact(name: "John Appleseed", age: 24)
john.objectWillChange.sink { _ in print("\(john.age) will change") }
print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"

3. 定义一个类来管理更新信息

(1)新建文件 UpdateStore。保留第一行代码,然后删除其他内容。

(2)引入 Combine

(3)声明一个 类 Class,名字为 UpdateStore,类型为 ObservableObject

(4)在类中,使用 @Published 声明一个 publisher,名字为 updates,类型是 Update 数组,默认值是 updateData。

import SwiftUI
import Combine                                     // 导入 Combine

class UpdateStore: ObservableObject {              // 定义用于存储更新信息的类
    @Published var updates: [Update] = updateData  // 声明publisher,类型为 Update 数组,默认值为 updateData
}

4. 使用 store 来管理更新信息列表

(1)在 UpdateList 文件的 body 之前,声明一个 UpdateStore 的对象 store,注意这里使用的声明关键字@ObservedObject,与类定义中的类型关键字是不同的。

@ObservedObject var store = UpdateStore()     // 声明 UpdateStore 类对象

(2)修改 List 组件中的参数,使用刚刚声明的对象。

List(store.updates) { update in
	// 代码略                    
}

现在预览一下,可以看到显示正常,但实际上数据已经由 store 接管了。

5. 实现新增更新信息

(1)先要在 body 外面编写一个函数,实现新增更新信息的功能。

// 增加一条新的更新信息
func addUpdate() {
	store.updates.append(	// 使用数组追加函数
        Update(image: "WXAvatar", title: "新增课程", text: "课程详细信息", date: "1月1日")
    )
}

(2)为 List 组件添加 navigationBarItems 修饰,添加按钮调用上面的函数。

List(store.updates) { update in
	// 代码略                    
}
.navigationBarTitle(Text("课程更新信息"))                     // 导航视图的标题
.navigationBarItems(leading: Button(action: addUpdate){     // 在导航标题上方增加按钮,单击调用 addUpdate 函数
	Text("增加")
})

预览一下,可以看见在标题的上方左侧(因为用的是 leading,右侧貌似应该用 trailing)出现一个按钮,单击后列表会增加一项新的更新信息。

6. 完善列表功能

(1)现在来实现滑动删除功能。但是这个功能没法在 List 中实现。所以要使用 ForEach 将导航项目重复列出,再用 List 包裹(保留导航标题和新增功能)。对应代码修改后如下:

NavigationView {                                // 导航视图
    List {
        ForEach(store.updates) { update in      // 使用 ForEach 遍历 store
			NavigationLink(destination: UpdateDetail(update: update)) { // 导航项目
				// 代码略
			}
		}
	}
	.navigationBarTitle(Text("课程更新信息"))                     // 导航视图的标题
	.navigationBarItems(leading: Button(action: addUpdate){     // 新增按钮
		Text("新增")
	})
}

(2)为 NavigationLink 组件增加 onDelete 修饰,传入当前元素的数组下标(索引),执行移除,这样数据就被删除了。

NavigationLink(destination: UpdateDetail(update: update)) { // 导航项目
    // 代码略
}
.onDelete { index in
	self.store.updates.remove(at: index.first!)
}

这里要注意的是,index.first 可能为 nil(空值),所以需要使用!进行强行解包。这么做有一定的风险,不过这里貌似肯定是有值的,所以不必额外编写那些判断是否为空的处理。(源码中说 onDelete 如果传了空值意味着删除无效)

预览一下,现在可以看到向左滑动更新信息会出现删除按钮,如果一直滑动到最左侧,则会直接删除。但是那个删除按钮写的英文,如果是要中文……本地化构建吗?

  • 让内置的文字变为中文

(1)打开项目导航栏,快捷键是⌘ + 1

(2)单击第一行,即项目配置文件

(3)在编辑器单击 Info 标签

(4)在Custom iOS Target Properties 列表的 key 中找到 Localization native development region,单击对应的 value 列后面的箭头,选择 China

(5)无论是预览还是在模拟器上的运行都显示为中文的删除了。注意,模拟器可能需要先设定语言为中文哦,对了,要先把 SceneDelegate.swift 中的 ContentView() 改成 UpdateList(),不然模拟器上显示的是 ContentView。别忘记测试完了要改回来。

这肯定不是一劳永逸的方法,如果需要 app 同时支持多种语言,一定不是这么简单粗暴的处理,看看后面是否有讲

7. 增加编辑按钮

与增加新增按钮一样,只需要在 navigationBarItems 中添加一个参数,trailing(好像刚才我还在说这个),记得在leading的新增按钮结束花括号后面先加上一个逗号,。使用内置的函数 EditButton() 就可以创建好这个按钮。因为刚才已经改了开发的语言,所以预览看见的是编辑而不是edit

.navigationBarItems(leading: Button(action: addUpdate){
                    Text("新增")
                }, trailing: EditButton())

单击编辑按钮,会发现已经内置好了删除功能。实际上,这是 NavigationLink 的 onDelete 在起作用。注释了这个修饰,功能就没有了。这里能体会到的是这个编辑按钮能检测到 onDelete 或者 onMove 修饰并且相应增加了界面上需要的组件。好强大。

8. 实现拖动排序

在 onDelete 下面,给NavigationLink组件再增加一个修饰 onMove。

.onMove { (source: IndexSet, destination: Int) in
	self.store.updates.move(fromOffsets: source, toOffset: destination)
}

预览一下,单击编辑按钮,可以看见每个列表项的行尾多一个拖动按钮,按住可以拖拽列表项到想要的位置。

我测试发现最后一个飘到屏幕外面的不大好用啊,新增若干个之后,还是屏幕最下方的项目拖拽有问题。删除功能貌似没事,这是为什么?也不是总无效……六脉神剑啊(时灵时不灵)……就是可点击的范围非常小,后面再研究吧。

9. 修改 HomeView

找到 HomeView 中头像边上的那个按钮组件,将 sheet 修饰器中调用的 ContentView() 修改为 UpdateList()。

本节小结

本节代码请参见 GitHub码云

  • Combine 框架
  • 遵循 ObservableObject 的类通过 @Published 定义 publisher,使用这个类的对象要用 @ObservedObject 声明
  • 通过遍历数组,在 NavigationLink 上展示所有的元素,再将其用 List 包裹起来。
  • 对每一个元素都可以使用数组的方法修饰,实现删除和移动等功能
  • 对包裹 NavigationLink 的 List 使用 NavigationBarItems,增加导航栏按钮。
  • 使用内置的函数实现导航栏按钮功能(当然也可以自定义其他功能)

接下来

使用预览和测试

发布了51 篇原创文章 · 获赞 15 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/hh680821/article/details/105113359