发布者协议区分和总结
Publisher
Publisher是一个协议,所有具体的发布者类型都遵循这个协议。它定义了发布者的基本行为,如发布元素和处理订阅者的需求。
Publishers
Publishers是一个命名空间,包含了许多具体的发布者类型。这些发布者类型遵循Publisher协议,并提供了创建和操作数据流的具体实现。
AnyPublisher
AnyPublisher是一种类型擦除的发布者,它可以将任何具体的发布者类型封装为一个通用的发布者。这样可以隐藏具体的发布者类型,只暴露Publisher协议定义的接口。这在需要返回不同发布者类型的函数中非常有用,因为它统一了返回类型。
import Combine
// 示例函数,返回 AnyPublisher
func fetchData(from url: URL) -> AnyPublisher<Data, URLError> {
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.eraseToAnyPublisher()
}
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
let publisher = fetchData(from: url)
let subscription = publisher.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed")
case .failure(let error):
print("Error: (error)")
}
}, receiveValue: { data in
print("Received data: (data)")
})
在这个示例中,fetchData 函数返回一个 AnyPublisher,从而隐藏了具体的发布者类型(URLSession.DataTaskPublisher)。
Published
Published 是一个属性包装器,用于将属性声明为 Combine 框架中的发布者。当属性的值发生变化时,它会自动发布新值。它常用于 SwiftUI 的 ViewModel 中,以便视图在属性变化时自动更新。
@Published var value:声明一个Published属性。projectedValue:返回Published.Publisher,可以订阅以接收属性变化的通知。
综合使用实例:
import Combine
import SwiftUI
// 定义一个模型来表示 API 响应的数据
struct Todo: Codable {
let title: String
}
class ViewModel: ObservableObject {
@Published var text: String = ""
private var cancellables = Set<AnyCancellable>()
// 将 fetchData 方法改为返回 AnyPublisher
func fetchData(from url: URL) -> AnyPublisher<String, Never> {
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Todo.self, decoder: JSONDecoder())
.map { $0.title }
.replaceError(with: "Error")
.eraseToAnyPublisher()
}
// 使用 fetchData 方法,并将结果赋值给 Published 属性
func updateText(from url: URL) {
fetchData(from: url)
.receive(on: DispatchQueue.main)
.assign(to: &$text)
}
}
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
Text(viewModel.text)
.onAppear {
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
viewModel.updateText(from: url)
}
}
}
在这个示例中:
- 使用
@Published属性包装器声明一个text属性,当text的值发生变化时,视图将会自动更新。 fetchData(from:)方法返回一个AnyPublisher,它类型擦除具体的发布者类型。该方法发起网络请求,将请求结果解码为Todo对象,然后提取title并发布。updateText(from:)方法使用fetchData(from:),并将结果赋值给@Published的text属性。通过使用assign(to:)操作符,将发布者的输出直接绑定到text属性。
常用发布者
-
Just
Just 是一个简单的发布者,它在订阅时只会发布一个值,然后完成。这对于测试和简单数据流非常有用。
let justPublisher = Just("Hello, Combine!")
let cancellable = justPublisher
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed")
case .failure(let error):
print("Error: (error)")
}
}, receiveValue: { value in
print("Received value: (value)")
})
// 输出:
// Received value: Hello, Combine!
// Completed
-
Empty
Empty 是一个发布者,它不发布任何值并立即完成。这通常用于需要返回一个发布者但不实际发布任何值的情况。
let emptyPublisher = Empty<String, Never>()
let cancellable = emptyPublisher
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed")
case .failure(let error):
print("Error: (error)")
}
}, receiveValue: { value in
print("Received value: (value)")
})
// 输出:
// Completed
-
Fail
Fail 是一个发布者,它不发布任何值,只发布一个错误并立即完成。这在测试错误处理路径时非常有用。
enum MyError: Error {
case somethingWentWrong
}
let failPublisher = Fail<String, MyError>(error: MyError.somethingWentWrong)
let cancellable = failPublisher
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed")
case .failure(let error):
print("Error: (error)")
}
}, receiveValue: { value in
print("Received value: (value)")
})
// 输出:
// Error: somethingWentWrong
-
Deferred
Deferred 是一个发布者,它将订阅延迟到某个条件满足时才执行。该发布者在每次订阅时会创建一个新的发布者。这在你需要根据某些条件动态创建发布者时非常有用。<也可以查看上面的retry 运算符的demo>
import Combine
// 一个简单的函数,返回当前时间
func getCurrentTime() -> String {
let formatter = DateFormatter()
formatter.timeStyle = .medium
return formatter.string(from: Date())
}
// 使用 Deferred 包装一个 Just 发布者,确保每次订阅时调用 getCurrentTime
let deferredPublisher = Deferred {
return Just(getCurrentTime())
}
let cancellable1 = deferredPublisher
.sink(receiveValue: { value in
print("First subscription received value: (value)")
})
// 延迟1秒后再订阅第二次,以便观察不同的时间
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let cancellable2 = deferredPublisher
.sink(receiveValue: { value in
print("Second subscription received value: (value)")
})
}
// 输出示例:
// First subscription received value: 10:00:00 AM
// Second subscription received value: 10:00:01 AM
是的,Future 是 Combine 框架中的一种发布者。它用于表示一个可能在将来某个时间点产生值或失败的异步操作。Future 只会发布一个值或错误,然后完成。
-
Future
它用于表示一个可能在将来某个时间点产生值或失败的异步操作。Future 只会发布一个值或错误,然后完成。
import Combine
// 定义一个错误类型
enum MyError: Error {
case somethingWentWrong
}
// 创建一个 Future 发布者
let futurePublisher = Future<String, MyError> { promise in
// 模拟异步操作
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
// 这里可以决定是成功还是失败
let success = true
if success {
promise(.success("Hello from the future!"))
} else {
promise(.failure(MyError.somethingWentWrong))
}
}
}
// 订阅 Future 发布者
let cancellable = futurePublisher
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed")
case .failure(let error):
print("Error: (error)")
}
}, receiveValue: { value in
print("Received value: (value)")
})
// 输出:
// Received value: Hello from the future! (如果成功)
// Error: somethingWentWrong (如果失败)
// Completed
在这个示例中,Future 发布者在其初始化块中接收一个闭包。这个闭包包含一个 promise 参数,开发者可以在这个闭包中执行异步操作,并在异步操作完成后调用 promise 的 success 或 failure 方法。
-
PassthroughSubject
PassthroughSubject 是一种主动发布者,允许外部调用其 send(_:) 方法来发布新值。
在用户输入表单数据时,我们可以使用 PassthroughSubject 来主动发布输入值,并在每次输入时进行数据验证。
import Combine
import UIKit
class FormViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
private let textField = UITextField()
private let validationSubject = PassthroughSubject<String, Never>()
override func viewDidLoad() {
super.viewDidLoad()
textField.borderStyle = .roundedRect
textField.placeholder = "Enter your email"
view.addSubview(textField)
textField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
textField.centerYAnchor.constraint(equalTo: view.centerYAnchor),
textField.widthAnchor.constraint(equalToConstant: 200)
])
textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
validationSubject
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.sink { text in
print("Validating input: (text)")
// 假设进行简单的 email 验证
let isValid = text.contains("@") && text.contains(".")
print("Is valid email: (isValid)")
}
.store(in: &cancellables)
}
@objc private func textFieldDidChange() {
if let text = textField.text {
validationSubject.send(text)
}
}
}
-
CurrentValueSubject
CurrentValueSubject 是另一种主动发布者,维护并发布其当前值,并允许外部调用其 send(_:) 方法来发布新值。它非常适合用于表示当前值,并允许订阅者获取最新的值。以下示例展示了如何使用 CurrentValueSubject 实现实时更新用户设置。
import Combine
import UIKit
class SettingsViewModel {
private var cancellables = Set<AnyCancellable>()
let usernameSubject = CurrentValueSubject<String, Never>("")
init() {
usernameSubject
.sink { newUsername in
print("Username updated to: (newUsername)")
}
.store(in: &cancellables)
}
func updateUsername(_ newUsername: String) {
usernameSubject.send(newUsername)
}
}
class SettingsViewController: UIViewController {
private let viewModel = SettingsViewModel()
private let usernameTextField = UITextField()
override func viewDidLoad() {
super.viewDidLoad()
usernameTextField.borderStyle = .roundedRect
usernameTextField.placeholder = "Enter new username"
view.addSubview(usernameTextField)
usernameTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
usernameTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
usernameTextField.centerYAnchor.constraint(equalTo: view.centerYAnchor),
usernameTextField.widthAnchor.constraint(equalToConstant: 200)
])
usernameTextField.addTarget(self, action: #selector(usernameTextFieldDidChange), for: .editingChanged)
// 绑定 ViewModel 的 usernameSubject 到 TextField
viewModel.usernameSubject
.assign(to: .text, on: usernameTextField)
.store(in: &viewModel.cancellables)
}
@objc private func usernameTextFieldDidChange() {
if let newUsername = usernameTextField.text {
viewModel.updateUsername(newUsername)
}
}
}
-
Timer
Timer 发布者会在指定的时间间隔发布值。使用 Timer 发布者定时刷新数据,例如每秒更新一次当前时间显示。
import Combine
import UIKit
class TimerViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
private let timeLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
timeLabel.font = UIFont.systemFont(ofSize: 24)
view.addSubview(timeLabel)
timeLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
timeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
timeLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
timerPublisher
.map { DateFormatter.localizedString(from: $0, dateStyle: .none, timeStyle: .medium) }
.sink { [weak self] currentTime in
self?.timeLabel.text = currentTime
}
.store(in: &cancellables)
}
}
-
Record
Record 是 Combine 框架中的一个发布者,用于记录一系列值和完成事件,然后在订阅时发布这些值。Record 通常用于测试和调试目的,它允许你预先定义一个数据流,并在需要时重放这些数据。
主要属性和方法
- output:
[Output]一个数组,包含发布者将发布的所有值。 - completion:
Subscribers.Completion<Failure>发布者完成时的状态(完成或失败)。
使用场景
- 测试:在单元测试中模拟发布者行为,预先定义发布的值和完成状态。
- 调试:重放特定的数据流,验证管道中的操作符是否按预期工作。
- 预定义数据流:当你希望在某个点发布一组预定义的值时。
下面是一些使用 Record 的示例,展示它的基本用法和在测试中的应用。
基本用法
import Combine
// 创建一个 Record 发布者,包含一组预定义的值和完成事件
let recordPublisher = Record<Int, Never>(output: [1, 2, 3, 4, 5], completion: .finished)
let subscription = recordPublisher.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed")
case .failure(let error):
print("Error: (error)")
}
},
receiveValue: { value in
print("Received value: (value)")
}
)
// 获取记录的数据和完成事件
let recordedData = recordPublisher.recording
print("Recorded output: (recordedData.output)")
print("Recorded completion: (recordedData.completion)")
// 输出结果
// Received value: 1
// Received value: 2
// Received value: 3
// Received value: 4
// Received value: 5
// Completed
// Recorded output: [1, 2, 3, 4, 5]
// Recorded completion: finished
测试用例
在测试中使用 Record 可以模拟发布者的行为,预先定义测试所需的数据和完成状态。
import XCTest
import Combine
class MyPublisherTests: XCTestCase {
var cancellables: Set<AnyCancellable> = []
func testRecordPublisher() {
// 创建一个 Record 发布者,用于模拟数据流
let recordPublisher = Record<Int, Never>(output: [1, 2, 3], completion: .finished)
// 订阅发布者
var receivedValues: [Int] = []
var receivedCompletion: Subscribers.Completion<Never>?
recordPublisher.sink(
receiveCompletion: { completion in
receivedCompletion = completion
},
receiveValue: { value in
receivedValues.append(value)
}
).store(in: &cancellables)
// 使用 recording 属性验证接收到的数据
let recordedData = recordPublisher.recording
XCTAssertEqual(recordedData.output, [1, 2, 3])
XCTAssertEqual(recordedData.completion, .finished)
XCTAssertEqual(receivedValues, [1, 2, 3])
XCTAssertEqual(receivedCompletion, .finished)
}
}
自定义发布者
除了使用 Combine 框架提供的标准发布者外,有时我们需要创建自定义发布者。自定义发布者可以满足一些特定的需求,例如特殊的事件源或复杂的逻辑。
struct CustomPublisher: Publisher {
typealias Output = String
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber, CustomPublisher.Failure == S.Failure, CustomPublisher.Output == S.Input {
let subscription = CustomSubscription(subscriber: subscriber)
subscriber.receive(subscription: subscription)
}
}
class CustomSubscription<S: Subscriber>: Subscription where S.Input == String, S.Failure == Never {
private var subscriber: S?
init(subscriber: S) {
self.subscriber = subscriber
}
func request(_ demand: Subscribers.Demand) {
_ = subscriber?.receive("Custom Publisher Value")
subscriber?.receive(completion: .finished)
}
func cancel() {
subscriber = nil
}
}
let customPublisher = CustomPublisher()
let cancellable = customPublisher
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed")
case .failure(let error):
print("Error: (error)")
}
}, receiveValue: { value in
print("Received value: (value)")
})
// 输出:
// Received value: Custom Publisher Value
// Completed