为了方便使用,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
*/