Combine 学习笔记(七):Foundation 中的 Publisher

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

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注