Publisher & Publishers(四)

211 阅读7分钟

发布者协议区分和总结

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:),并将结果赋值给 @Publishedtext 属性。通过使用 assign(to:) 操作符,将发布者的输出直接绑定到 text 属性。

常用发布者

  1. 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
  1. 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
  1. 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
  1. 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 只会发布一个值或错误,然后完成。

  1. 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 参数,开发者可以在这个闭包中执行异步操作,并在异步操作完成后调用 promisesuccessfailure 方法。

  1. 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)
        }
    }
}
  1. 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)
        }
    }
}
  1. 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)
    }
}
  1. Record

RecordCombine 框架中的一个发布者,用于记录一系列值和完成事件,然后在订阅时发布这些值。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