Swift中发布-订阅框架Combine的使用
Combine简介
Combine是一个苹果用来处理事件的新的响应式框架,支持iOS 13及以上版本。
你可以使用Combine去统一和简化在处理类似于target-action,delegate,kvo等事情的代码。
iOS目前已经有第三方的响应式框架了,如:RxSwift、ReactiveCocoa,但是苹果现在发布了自己的新的框架。
它提供了一种声明式、函数式的编程方式,可以让开发者更加简单、高效地处理异步数据流。
Combine核心概念
Combine提供的核心概念:
Publisher(发布者) 和 Subscriber(订阅者)
Operators(操作者)
Subjects (对象)
发布者Publisher:可以将值或错误发送给订阅者,并且在完成或取消时,向订阅者发送相应的消息。一个发布者如果没有订阅,则不会发布任何数据。
当你在描述一个发布者时,你会用两种相关类型(associatedtype)来表述他:Output 和 Failure 。比如发布者返回 String实例 ,并且可能以 URLError实例 的形式返回失败,那么发布者可以用 <String, URLError> 来描述。
订阅者Subscriber:接收发布者发送的值、错误和完成信息,并进行相应的处理。
订阅者用两种相关类型进行描述:Input 和 Failure 。订阅者发起数据请求,并控制接收到的数据量。在 Combine 中,他可以看作是“行为的驱动者”,没有了订阅者,其他的组成部分将闲置。
操作符Operator:用来对数据流进行转换、过滤、合并等处理,Combine框架中提供了很多操作符,例如map、filter、merge等。既实现了 Publisher协议 ,又实现了 Subscriber协议 。他们支持订阅一个发布者,并接收订阅者的请求。
一般的数据流是这样处理的:
发布者 -> 操作者1 -> 操作者2 -> ... -> 操作者n -> 订阅者
操作者可以被用来转换数值或者值的类型 -- Output 和 Failure 均可。操作者也可以分割、复制、合并数据流。操作者之间的 Output/Failure类型 必须一致,否则编译器会报错。
发布者和订阅者是相互连接的,并构成 Combine 的核心。当你连接一个订阅者到发布者上,Input 和 Output 类型必须一致,两者的 Failure 也需要一致。
流程介绍
一个简单的数据流举例:
1 2 3 4 5 | let _ = Just(5).map { 值 -> String in return "五" }.sink { 接收的值 in print( "最终结果接收到的值为 \(接收的值)" ) } |
1.数据流从 发布者“Just”(一种发布者,他的生命周期中只提供一次值,然后结束)开始,且他关联了一个数值5,且他的关联类型为 <Int, Never> 。
2.数据流中有个 操作者“map” ,用来转换值和类型。在这个例子中,数值5 被转换为 "五" ,且将关联类型由 <Int, Never> 转换为 <String, Never> 。
3.数据流以 订阅者“sink” 结束。
有许多的操作者以 try 作为前缀,表示他们将返回一个 <Error> 失败类型。例如 map 和 tryMap 。map 会将 Output 和 Failure 一同传输过去。而 tryMap 接受 Input、Output 和 Failure 类型,但总是输出一个 <Error> 失败类型。
你可以将发布者、操作者、订阅者看作是拥有两条类型流,一条是功能型(Input、Output)的,一条是错误型(Failure)的。
生命周期
发布者-订阅者的生命周期如下:
1.当一个订阅者附加到一个发布者时,发布者会调用 subscribe(_: Subscriber)
2.订阅者会调用 receive(subscription: Subscription) 来接收发布者创建的订阅(subscription)
3.发布者会调用 request(_: Demand) 来接收订阅者的需求信息(数据接收量等)
4.订阅者会调用 receive(_: Input) 来接收来自发布者的数据,且数据符合第三步中的需求信息
5.在订阅创建后,订阅者可以调用 cancel() 来取消订阅
6.发布者可以选择发送 receive(completion:) 来进行订阅终止工作。这样的终止工作可能是正常的终止,也可能是以失败终止。
系统提供的常见发布者,订阅者,操作者
Combine 之外也有许多苹果的API提供发布者:
SwiftUI中使用 @Published 和 @ObservedObject 属性包装器(由 Combine 提供),来创建一个发布者来支持他的声明式视图机制。
1 2 3 4 5 6 | Foundation URLSession.dataTaskPublisher KVO 实例中的 .publisher NotificationCenter Timer Result |
发布者对象 Subjects
Subjects对象是发布者的一种特殊情况(Subject协议 继承了 Publisher协议)。该协议要求实现 send(_:) ,来允许开发者向订阅者发送特定的值。
对象可以通过 send方法 ,来“注入”一个值到数据流中。
Combine 内置了两种对象:CurrentValueSubject 和 PassthroughSubject 。这两个对象比较相似,不同在于,CurrentValueSubject 需要一个初始的数值。
CurrentValueSubject 和 PassthroughSubject 为 实现了ObservableObject协议的对象 创建发布者提供了帮助。这个协议被SwiftUI中许多组件所支持。
订阅者 Subscriber
Combine 内置了两种订阅者:Assign 和 Sink 。SwiftUI 中还有一种订阅者:onReceive 。
订阅者支持取消订阅,并在发布者发布任何 Completion完成 之前关闭流处理。Assign 和 Sink 均实现了 Cancellable协议。
当你保留了一个订阅者的引用,你很有可能想要一个其订阅的引用来取消订阅。可以用这个引用的 cancel()方法来取消订阅。存储订阅者的引用很重要,因为当释放引用时,订阅者会取消自己的订阅。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | Assign 将接收到的值应用(赋值)到一个对象的键路径(keypath)上。这是一个例子: .assign(to: \.isEnabled, on : 登录按钮) Sink 接受一个闭包,一个 处理接收到的值 的闭包。 .sink { 接收的值 in print( "最终接收到的值为 \(String(describing: 接收的值))" ) } 其他的苹果API中也提供了订阅者。SwiftUI 中的 View协议 定义了一个 onReceive(publisher)方法 来将视图作为订阅者来使用。 struct MyView : View { @State private var 当前状态值 = "ok" var body: some View { Text( "当前状态: \(当前状态值)" ) .onReceive(我的订阅者) { 接收的值 in self.当前状态值 = 接收的值 } } } |
操作者
当一个Publisher发布一个事件时,Subscriber会被通知这个事件,并执行相应的操作。
一个Subscriber可以订阅多个Publisher,并且每个Publisher都可以同时拥有多个Subscriber。当所有的Subscriber都取消订阅时,Publisher会停止发布事件。
Operator可以对事件流进行各种操作,包括数据转换、过滤、组合等。比如,map操作可以将一个事件流中的元素转换成另一个类型,filter操作可以过滤出符合条件的元素,merge操作可以将多个事件流合并成一个事件流等。
map操作符可以将数据流中的值进行转换,返回一个新的数据流。例如,将一个字符串转换成大写形式:
1 2 | let stringPublisher = Just ( "hello, world!" ) let uppercasedPublisher = stringPublisher . map { $ 0 . uppercased () } |
filter对数据流中的值进行过滤,返回一个新的数据流。例如,过滤掉数组中的偶数:
1 2 | let arrayPublisher = [ 1 , 2 , 3 , 4 , 5 ]. publisher let oddPublisher = arrayPublisher . filter { $ 0 % 2 != 0 } |
merge对数据流进行合并,生成一个数据流。例如,将两个发布者合并成一个:
1 2 3 | let publisher1 = Just ( "hello" ) let publisher2 = Just ( "world" ) let mergedPublisher = Publishers . Merge ( publisher1 , publisher2 ) |
下面是一个简单的例子,展示了如何使用Combine来监听一个UITextField的文本输入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import UIKit import Combine class ViewController : UIViewController { @IBOutlet weak var textField : UITextField ! var cancellables = Set < AnyCancellable > () override func viewDidLoad () { super . viewDidLoad () // 创建一个订阅者,打印出文本输入的内容 let subscriber = Subscribers . Assign ( object : self . textField , keyPath : \. text ) // 订阅文本输入框的文本变化事件 self . textField . textPublisher ( for : . editingChanged ) . map { $ 0 . text ?? "" } . subscribe ( subscriber ) . store ( in : & cancellables ) } } extension UITextField { func textPublisher () - > AnyPublisher < String , Never > { NotificationCenter . default . publisher ( for : UITextField . textDidChangeNotification , object : self ) . map {($ 0 . object as ? UITextField )?. text ?? "" } . eraseToAnyPublisher () } } |
AnyCancellable
Combine 中还定义了一个 AnyCancellable 类,实现了 Cancellable 协议,特点是会在该类析构时自动执行 cancel 方法。
在实际开发中,当 Subscriber 在某个时候不想接收 Publisher 发布的数据时,可以取消订阅以释放资源。
Combine 中提供了 Cancellable 协议,该协议中定义了一个 cancel 方法,用于取消订阅流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import Combine let subject = PassthroughSubject < String , Never > () // 创建PassthroughSubject // 订阅 let subscription = subject . sink ( receiveCompletion : { _ in print ( "receiveCompletion" ) }, receiveValue : { value in print ( value ) }) // 发送数据 subject . send ( "hello" ) // 中途取消订阅 subscription . cancel () // 后面发送的数据都会失败 subject . send ( "world" ) subject . send ( completion : . finished ) /* 输出: hello */ |
可以理解为 AnyCancellable是一种管理订阅状态的工具,能根据开发者需要在某个时段切断 Publisher和 Subscriber的联系。
AnyCancellable 的一个应用就是可以在某种情况下中断网络请求,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import UIKit import Combine let dataPublisher = URLSession . shared . dataTaskPublisher ( for : URL ( string : "https://louyu.cc" )!) let cancellableSink = dataPublisher . sink { completion in switch completion { case . finished : break case . failure ( let error ): print ( "error: \( error )" ) break } } receiveValue : { value in print ( "received \( value )" ) } cancellableSink . cancel () //取消网络请求 |
SwiftUI中的Combine
SwiftUI的属性修饰符, 是根据Combine作为底层实现的。 在APP中创建一个全局变量,让不同深度的子视图在不经过传递就可以直接访问共享数据。 代码举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import SwiftUI // 创建一个遵循 ObservableObject 协议的类 // ObservableObject 是一种用于创建可以在视图之间共享数据的类的协议。当类遵循 ObservableObject 协议时,它可以定义具有 @Published 属性包装器的属性,这些属性可以在更改时通知视图更新。 class UserData: ObservableObject { //使用@Published属性修饰符的类必须要遵守ObservableObject协议, 这样在SwiftUI中使用这个属性时,如果这个属性被修改了,那么对应的SwiftUI视图会自动刷新。 @Published var name: String = "John" } // 在应用的顶层视图中设置 EnvironmentObject @main struct MyApp: App { //StateObject 属性包装器用于在视图的生命周期内创建和维护一个对象,确保视图和其状态之间的一致性,当视图销毁时,由 StateObject 创建的对象也将被销毁。 //与 @ObservedObject 和 @EnvironmentObject 类似,StateObject 用于管理私有或局部状态。 @StateObject private var userData = UserData() var body: some Scene { WindowGroup { ContentView() .environmentObject(userData) // 将 userData 设置为 EnvironmentObject } } } // 在任何需要访问 UserData 的视图中,使用 @EnvironmentObject 来获取它 struct ContentView: View { @EnvironmentObject var userData: UserData var body: some View { VStack { Text( "Hello, \(userData.name)!" ) // 在这里使用 userData Button( "Change Name" ) { userData.name = "Alice" // 在点击按钮时更改 userData 的值 } } } } |
参考文章地址:
https://www.jianshu.com/p/1dc27229a533
https://louyu.cc/articles/ios-swift/2021/03/?p=2865/
分类:
Swift
· 分享4款.NET开源、免费、实用的商城系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了