Swift Combine 开发小册 - 2. 基础用法

5,448 阅读7分钟

A. 导入Combine

在任何想要使用Combine的Swift文件中,可以在文件顶部添加以下导入语句:

import Combine

这将使得Combine中的所有类、结构体和函数在该文件中可用。

需要注意的是,Combine只能在iOS 13.0+ / macOS 10.15+中使用。

B. 发布者和订阅者

  • 理解Combine中发布者和订阅者的作用
  • 创建一个发布者以发出数据
  • 创建一个订阅者以接收来自发布者的数据
  • 连接发布者和订阅者以创建数据流

1. 理解发布者和订阅者

在Combine中,发布者和订阅者在促进应用程序不同部分之间的数据流时起着至关重要的作用。发布者是一种对象,它会随着时间推移发出一系列值,而订阅者则是接收和处理这些值的对象。

发布者负责定义它将发出的值序列,并在新值可用时通知其订阅者。发布者可以发出单个值、一系列值或错误,并且在没有更多值要发出时也可以完成值序列。

另一方面,订阅者负责接收和处理由发布者发出的值。它们可以在每个值发出时进行处理,或随着时间的推移累积值并将它们作为一组进行处理。订阅者还可以处理由发布者发出的错误和完成通知。

为了让订阅者从发布者接收值,它必须首先建立与发布者的连接。这种连接是使用subscribe方法建立的,该方法接受一个闭包,定义了订阅者的行为。

2. 创建一个发布者

有两种类型的发布者,一种是直接创建,另一种是通过操作第一个发布者创建的,我们称之为操作符:

  • 直接创建的发布者:
    • 使用Just发布者发出单个值。
    • 使用Future发布者在将来的某个时间发出单个值。
    • 使用PassthroughSubject发布者根据用户事件或其他触发器发出值。
    • 使用CurrentValueSubject发布者发出值并保留最近发布的值的缓冲区。
  • 使用mapfilter或其他操作符重新发布原始的发布者。
  • 通过实现Publisher协议创建自定义发布者。

下面来看看如何创建发布者:

  • Just
let publisher = Just(42)

这创建了一个发布者,它发布了一个单一的值(42),然后立即结束。

  • Future
let publisher = Future<String, Error> { promise in
    // Perform an asynchronous task
    DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
        // When the task completes, fulfill the promise
        promise(.success("Hello, world!"))
    }
}

这创建了一个发布者,执行异步任务并在任务完成时发出单一的值("Hello, world!")。如果任务遇到错误,发布者会发出一个错误。

还可以使用 async-await 语法来获取 Future 中的值(或错误):

do {
    let value = try await publisher.value
    print("Received value: \(value)")
} catch {
   print("Error occurred with info: \(error.localizedDescription)")
}
  • PassthroughSubject
class MyViewModel {
    private let usernameSubject = PassthroughSubject<String, Never>()

    var usernamePublisher: AnyPublisher<String, Never> {
        return usernameSubject.eraseToAnyPublisher()
    }

    func updateUsername(_ username: String) {
        usernameSubject.send(username)
    }
}

这创建了一个自定义发布者(usernameSubject),每当调用updateUsername方法时,它就会发出一个字符串值。发布者被类型擦除为AnyPublisher,以便可以安全地暴露给视图模型的公共接口。

  • CurrentValueSubject
// Define a CurrentValueSubject with an initial value of 0
let subject = CurrentValueSubject<Int, Never>(0)

// Subscribe to the subject and print out each emitted value
let cancellable = subject.sink { value in
    print("Value: \(value)")
}

// Update the value of the subject
subject.send(1)
subject.send(2)

// Print out the current value of the subject
print("Current value: \(subject.value)")

// Cancel the subscription
cancellable.cancel()
/* output:
Value: 0
Value: 1
Value: 2
Current value: 2
*/
  • 何时使用Just、Future、PassthroughSubject、CurrentValueSubject:

    • Just:当你想要创建一个发出单一值然后立即完成的发布者时,可以使用Just。当你想要创建一个发出常量值或在订阅时可以计算出值的发布者时,这是非常有用的。Just发布者也适用于用Publishers.Catch替换值时,Just总是会产生一个值而不会出错。
    • Future:当你想要创建一个在未来某个时间发出单一值的发布者时,可以使用Future。当你想要执行一些异步工作,例如网络请求或磁盘I/O,并在工作完成时发出值时,这是非常有用的。
    • PassthroughSubject:当你想要创建一个允许你手动向其订阅者发送值的发布者时,可以使用PassthroughSubject。当你想要创建一个基于某些外部事件或用户交互发出值的自定义发布者时,这是非常有用的。
    • CurrentValueSubject:当你想要创建一个将单个值封装起来,并在值发生变化时向其订阅者发布它的发布者时,可以使用CurrentValueSubject。当你想要创建一个表示可变状态的发布者时,如用户界面元素的状态或共享数据模型,这是非常有用的。

    通常,你应该选择最适合你特定用例的发布者类型。例如,如果你需要执行一些异步工作,然后发出一个值,你将使用Future。如果你需要创建一个基于用户交互发出值的自定义发布者,你将使用PassthroughSubject。如果你需要表示可以由应用程序的多个部分更新的可变状态,则将使用CurrentValueSubject

3. 创建一个订阅者

在Combine中有两种类型的订阅者:

  • sink订阅者,允许你通过闭包处理发出的值、错误和完成通知。
  • assign订阅者,允许你将发出的值绑定到对象上的某个属性。
  • 你也可以通过实现Subscriber协议来创建自定义订阅者。

让我们看一些例子:

  • sink
let publisher = Just("Hello, world!")

publisher
    .sink(receiveCompletion: { print("Received completion: \($0)") },
          receiveValue: { print("Received value: \($0)") })

这创建了一个使用sink方法的订阅者,它打印发布者发出的任何值("Hello, world!"),并打印任何完成事件(在本例中是一个没有错误的完成)。

  • assign
class MyViewModel {
    let username = CurrentValueSubject<String, Never>("")
    
    var usernameLabelText: String {
        didSet {
			print(usernameLabelText)
        }
    }
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        username
            .assign(to: \.usernameLabelText, on: self)
            .store(in: &cancellables)
    }

	deinit {
        cancellables.forEach { $0.cancel() }
    }
}

这创建了一个使用assign方法的订阅者,将发布者发出的值(username)绑定到订阅者(usernameLabelText)上的属性。在这个例子中,订阅者是一个视图模型,被绑定的属性(usernameLabelText)是一个在用户界面中显示的字符串。订阅者还被添加到一个cancellables集合中,以确保在不再需要视图模型时将其销毁。

4. 连接发布者和订阅者

- 使用`subscribe`方法将发布者连接到订阅者。
- 使用`eraseToAnyPublisher`方法对发布者进行类型擦除。

以下是一些例子:

  • 使用自定义订阅者的subscribe
let publisher = Just("Hello, world!")

class MySubscriber: Subscriber {
    typealias Input = String
    typealias Failure = Never
    
    func receive(subscription: Subscription) {
        subscription.request(.max(1))
    }
    
    func receive(_ input: String) -> Subscribers.Demand {
        print("Received value: \(input)")
        return .none
    }
    
    func receive(completion: Subscribers.Completion<Never>) {
        print("Received completion: \(completion)")
    }
}

publisher.subscribe(MySubscriber())

这使用subscribe方法将发布者(publisher)与一个自定义订阅者连接。在这个例子中,订阅者是一个实现了Subscriber协议的自定义类(MySubscriber)。订阅者从发布者接收到一个字符串值并将其打印到控制台。订阅者还实现了receiveCompletion方法来打印发布者发出的任何完成事件。

  • 对已知输出和失败类型的发布者进行类型擦除:
let publisher = Just("Hello, world!")
let anyPublisher = publisher.eraseToAnyPublisher()

这创建了一个发出字符串值的发布者(publisher),然后使用eraseToAnyPublisher方法将其类型擦除为一个AnyPublisher<String, Never>

  • 对用于公共API的发布者进行类型擦除:
class MyViewModel {
    private let usernameSubject = PassthroughSubject<String, Never>()
    
    var usernamePublisher: AnyPublisher<String, Never> {
        return usernameSubject.eraseToAnyPublisher()
    }
    
    func updateUsername(_ username: String) {
        usernameSubject.send(username)
    }
}

这创建了一个自定义发布者(usernameSubject),每当调用updateUsername方法时就会发出一个字符串值。使用eraseToAnyPublisher方法将发布者类型擦除为一个AnyPublisher,以便可以安全地暴露给视图模型的公共接口。这使得视图模型的客户端可以订阅该发布者,而无需知道具体使用的发布者类型。

通过了解如何在Swift Combine中使用发布者和订阅者,你可以创建强大的数据流,轻松处理异步和事件驱动的编程任务。在下一节中,我们将介绍如何使用操作符来转换和操作Combine中的数据流。

文章首发发布地址:Github

Photo by Maxim Berg on Unsplash