为了方便使用,Foundation 对一些常用的操作扩充了基于 Publisher 的 API,我们在开发中可以直接进行调用。本文就逐个讨论这些 API 的具体用法。
一、Sequence Publisher
Sequence Publisher 用于通过序列构造 Publisher,如数组、区间、字典等,只要在序列后面加上 .publisher 即可。
import Combine // 数组 let p1: Publishers.Sequence<[String], Never> = ["a", "b", "c"].publisher // 区间 let p2: Publishers.Sequence<(ClosedRange<Int>), Never> = (1...10).publisher // 步进,可以理解为一个等差数列 let p3: Publishers.Sequence<StrideTo<Int>, Never> = stride(from: 0, to: 10, by: 2).publisher // 字典 let p4: Publishers.Sequence<[String : String], Never> = ["name" : "zhangsan", "age" : "15"].publisher
二、URLSession Publisher
URLSession Publisher 是 URLSession 在 iOS 13 以后新增的一种网络 API,通过这个 API 可以更加简单的完成网络请求、数据转换等操作。
import Foundation import Combine let url = URL(string: "https://www.baidu.com") let publisher = URLSession.shared.dataTaskPublisher(for: url!) // 订阅 let subscripton = publisher.sink(receiveCompletion: { print($0) }) { (data, response) in print(String(data: data, encoding: .utf8)!) }
下面我们以请求杭电红色家园的蹭课接口为例,熟悉一下 URLSession Publisher 的用法。
import UIKit import Combine // 服务器返回数据Model struct CourseModel: Codable { var status: Status var data: [DataItem] } // 请求状态 struct Status: Codable { var code: Int var msg: String } struct DataItem: Codable, Hashable { var KCMC: String //课程名称 var KCDM: String //课程代码 var XKKH: String //选课课号 var JSXM: String //教师姓名 var KKXY: String //开课学院 var SKSJ: String //上课时间 var SKDD: String //上课地点 var XF: String //学分 var KHFS: String //考核方式 var KCXZ: String //课程性质 var QSJSZ: String //起止周 var RL: Int //课程容量 var YL: Int //课程余量 } let url = URL(string: "https://api.redhome.cc/selection/v5.0.0/search?xn=2020-2021&xq=1&condition=kcmc&keyWord=ACM") let request = URLRequest(url: url!) let session = URLSession.shared let backgroundQueue = DispatchQueue.global() let dataPublisher = session.dataTaskPublisher(for: request) .retry(5) .timeout(10, scheduler: backgroundQueue) .map{$0.data} .decode(type: CourseModel.self, decoder: JSONDecoder()) .subscribe(on: backgroundQueue) .eraseToAnyPublisher() let subscription = dataPublisher.receive(on: DispatchQueue.main) .sink(receiveCompletion: {_ in }) { (courseModel) in print(courseModel.data) }
注意,上面的代码不知为何无法在 PlayGround里面获得运行结果,请在常规工程中运行。
输出结果如下:
三、Notification Publisher
Notification 在 iOS 13 以后也扩展了创建 Publisher 的辅助 API。
● 系统通知
import UIKit import Combine let subscription = NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification) .sink { _ in print("App进入后台") }
● 自定义通知
import UIKit import Combine extension Notification.Name { static var myNotiName = Notification.Name("LouYu") } let subscription = NotificationCenter.default.publisher(for: .myNotiName) .sink(receiveValue: { notification in print(notification.object as? String) }) let noti = Notification(name: .myNotiName, object: "发送通知", userInfo: nil) NotificationCenter.default.post(noti)
● 在 SwiftUI 中监听 App 进入后台与返回前台
import SwiftUI struct ContentView: View { var body: some View { Text("") .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification), perform: { _ in print("App进入后台") }) .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification), perform: { _ in print("App返回前台") }) .onAppear { print("应用初始化") } } }
四、KVO Publisher
任何 NSObject 对象一旦被 KVO 监听,则可以成为一个 Publisher。
import Combine import UIKit class Person: NSObject { @objc dynamic var age: Int = 0 } let person = Person() let subscription = person.publisher(for: \.age) .sink { newValue in print("person的age改成了\(newValue)") } person.age = 20 // 改变时subscirber收到消息
在开发中常用的 KVO Publisher 如下:
let scrollView = UIScrollView() scrollView.publisher(for: \.contentOffset) let avPlayer = AVPlayer() avPlayer.publisher(for: \.status) let operation = Operation() operation.publisher(for: \.queuePriority)
五、Timer Publisher
当 Subscriber 开始订阅后,大部分的 Publisher 会立即提供数据, 如 Just。但有一种遵守 ConnectablePublisher 协议的 Publisher,它需要某种机制来启动数据流,Timer Publisher 就是这种类型的 Publisher。ConnectablePublisher 不同于普通的 Publisher,需要明确地对其调用 connect() 与 autoconnet() 方法,才会开始发送数据。
● autoconnect()
import UIKit import Combine // every:间隔时间 on:在哪个线程 in:在哪个Runloop let subscription = Timer.publish(every: 1, on: .main, in: .default) .autoconnect() .sink { _ in print("Hello") } // 可以取消订阅 // subscription.cancel()
● connect()
import UIKit import Combine // every:间隔时间 on:在哪个线程 in:在哪个Runloop let timerPublisher = Timer.publish(every: 1, on: .main, in: .default) let cancellablePublisher = timerPublisher .sink { _ in print("World") } let subscription = timerPublisher.connect() // 可以取消订阅 // subscription.cancel()
六、@Published
@Published 是属性包装(Property Wrapper),可以为任何一个属性生成其对应类型的 Publisher,这个 Publisher 会在属性值发生变化时发送消息。用 @Published 修饰的属性通过 $属性名 即可得到该属性对应的 Publisher。
import Combine // 在类中使用 class Student { @Published var name: String = "zhangsan" @Published var age: Int = 20 } let stu = Student() let subscription1 = stu.$name.sink { // $name作为Publisher print($0) } stu.name = "lisi" let subscription2 = stu.$age.sink { // $age作为Publisher print($0) } stu.age = 30 /* 输出: zhangsan lisi 20 30 */
import Combine // 在协议中使用,通过Published将实际类型包装起来 protocol ModelProtocol { var namePublisher: Published<String>.Publisher { get } } // 遵守协议,将name的值返回给namePublisher class Student: ModelProtocol { @Published var name: String var namePublisher: Published<String>.Publisher { $name } init(name: String) { self.name = name } } let student = Student(name: "zhangsan") let subscription = student.namePublisher .sink { print("hello \($0)") } student.name = "lisi" student.name = "wangwu" /* 输出: hello zhangsan hello lisi hello wangwu */