Combine 常见运算符(一)

1,366 阅读6分钟

Combine 框架中的运算符

Combine 提供了一系列强大的运算符来处理数据流,以下是一些常用的运算符及其含义和操作

转换运算符

  1. map

将发布者产生的每个值映射为另一个值。

import Combine

let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher

let cancellable = publisher
    .map { $0 * 2 }
    .sink(receiveValue: { value in
        print("Mapped value: (value)")
    })

// 输出:
// Mapped value: 2
// Mapped value: 4
// Mapped value: 6
// Mapped value: 8
// Mapped value: 10
  1. flatMap

将发布者产生的值转换为新的发布者,并扁平化处理。

import Combine

let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher

let cancellable = publisher
    .flatMap { number in
        return Just(number * 2)
    }
    .sink(receiveValue: { value in
        print("FlatMapped value: (value)")
    })

// 输出:
// FlatMapped value: 2
// FlatMapped value: 4
// FlatMapped value: 6
// FlatMapped value: 8
// FlatMapped value: 10
  1. compactMap

map 类似,但会过滤掉转换后为 nil 的值。

import Combine

let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher

let cancellable = publisher
    .compactMap { number -> Int? in
        return number % 2 == 0 ? number : nil
    }
    .sink(receiveValue: { value in
        print("CompactMapped value: (value)")
    })

// 输出:
// CompactMapped value: 2
// CompactMapped value: 4
  1. allSatisfy

检查发布者的所有输出是否满足指定条件。

let publisher = [1, 2, 3, 4, 5].publisher
let cancellable = publisher.allSatisfy { $0 > 0 }
                           .sink { print("All greater than 0: ($0)") }
// 输出: All greater than 0: true

过滤运算符

  1. filter

只允许通过满足特定条件的值。

import Combine

let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher

let cancellable = publisher
    .filter { $0 % 2 == 0 }
    .sink(receiveValue: { value in
        print("Filtered value: (value)")
    })

// 输出:
// Filtered value: 2
// Filtered value: 4
  1. removeDuplicates

移除连续重复的值。

import Combine

let numbers = [1, 2, 2, 3, 3, 3, 4, 5]
let publisher = numbers.publisher

let cancellable = publisher
    .removeDuplicates()
    .sink(receiveValue: { value in
        print("Unique value: (value)")
    })

// 输出:
// Unique value: 1
// Unique value: 2
// Unique value: 3
// Unique value: 4
// Unique value: 5
  1. dropFirst

跳过前 n 个值。

import Combine

let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher

let cancellable = publisher
    .dropFirst(3)
    .sink(receiveValue: { value in
        print("Value after dropFirst: (value)")
    })

// 输出:
// Value after dropFirst: 4
// Value after dropFirst: 5
  1. dropWhile

当条件为真时,跳过值。

import Combine

let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher

let cancellable = publisher
    .dropWhile { $0 < 3 }
    .sink(receiveValue: { value in
        print("Value after dropWhile: (value)")
    })

// 输出:
// Value after dropWhile: 3
// Value after dropWhile: 4
// Value after dropWhile: 5
  1. prefix

只取前 n 个值。

import Combine

let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher

let cancellable = publisher
    .prefix(3)
    .sink(receiveValue: { value in
        print("Value with prefix: (value)")
    })

// 输出:
// Value with prefix: 1
// Value with prefix: 2
// Value with prefix: 3
  1. ignoreOutput

ignoreOutput 是一个操作符,用于忽略发布者发出的所有元素,仅关注完成事件或错误事件的发生。它在某些情况下非常有用,例如当我们只关心发布者是否成功完成,而不关心它发出的具体值时使用。

以下是一个简单的示例,展示了如何使用 ignoreOutput 操作符来忽略发布者发出的元素,只关注完成事件的发生。

import Combine

// 创建一个简单的发布者,每隔一秒发出一个整数,共发出三个元素
let publisher = Timer.publish(every: 1, on: .main, in: .default)
    .autoconnect()
    .prefix(3) // 限制只发出三个元素,用于示例简化

// 创建了一个 sink 订阅者,用于接收和打印发布者发出的每一个值。
let cancellable = publisher
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Publisher finished successfully")
        case .failure(let error):
            print("Publisher failed with error:", error)
        }
    }, receiveValue: { value in
        print("Received value:", value)
    })

let ignoreCancellable = publisher
    // 使用ignoreOutput 操作符创建了一个新的订阅者,它会忽略发布者发出的所有元素,仅关注完成事件。
    .ignoreOutput()
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Ignore Output: Publisher finished successfully")
        case .failure(let error):
            print("Ignore Output: Publisher failed with error:", error)
        }
    }, receiveValue: { value in
        print("Received ignore value:", value) // 这里会收到xcode的⚠️:Will never be executed
    })

// 取消订阅
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
    cancellable.cancel()
    ignoreCancellable.cancel()
}

// 输出结果
// Received value: 0
// Received value: 1
// Received value: 2
// Publisher finished successfully
// Ignore Output: Publisher finished successfully

组合运算符

  1. merge

将多个发布者合并为一个。

import Combine

let publisher1 = [1, 2, 3].publisher
let publisher2 = [4, 5, 6].publisher

let mergedPublisher = Publishers.Merge(publisher1, publisher2)

let cancellable = mergedPublisher
    .sink(receiveValue: { value in
        print("Merged value: (value)")
    })

// 输出:
// Merged value: 1
// Merged value: 2
// Merged value: 3
// Merged value: 4
// Merged value: 5
// Merged value: 6
  1. combineLatest

当任一发布者产生新值时,组合所有发布者的最新值。

import Combine

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()

let combinedPublisher = Publishers.CombineLatest(publisher1, publisher2)

let cancellable = combinedPublisher
    .sink(receiveValue: { value1, value2 in
        print("Combined latest values: (value1) and (value2)")
    })

publisher1.send(1)
publisher2.send("A")
publisher1.send(2)
publisher2.send("B")

// 输出:
// Combined latest values: 1 and A
// Combined latest values: 2 and A
// Combined latest values: 2 and B
  1. zip

当所有发布者都产生新值时,组合这些值。

import Combine

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()

let zippedPublisher = Publishers.Zip(publisher1, publisher2)

let cancellable = zippedPublisher
    .sink(receiveValue: { value1, value2 in
        print("Zipped values: (value1) and (value2)")
    })

publisher1.send(1)
publisher2.send("A")
publisher1.send(2)
publisher2.send("B")

// 输出:
// Zipped values: 1 and A
// Zipped values: 2 and B
  1. switchToLatest

switchToLatest 操作符用于处理嵌套的 Publisher 结构,它能够监视多个 Publishers,并在最新的 Publisher 发出新元素时切换到该 Publisher 的输出。这个操作符在处理一些需要动态切换订阅者的场景下非常有用,例如处理用户操作、网络请求等异步任务。

假设我们有一个需求,需要从不同的网络请求中获取数据,这些请求依赖于先前请求的结果。下面的示例将演示如何使用 switchToLatest 来处理这种情况。

import Combine
import Foundation

// 定义一个用于模拟网络请求的函数,返回一个 Future Publisher
func fetchDataPublisher(requestNumber: Int) -> AnyPublisher<String, Error> {
    return Future<String, Error> { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + Double(requestNumber)) {
            promise(.success("Response from request (requestNumber)"))
        }
    }
    .eraseToAnyPublisher()
}

// 创建一个序列,依次发起三个网络请求
let sequencePublisher = Publishers.Sequence(sequence: [1, 2, 3])
    .map { requestNumber in
        fetchDataPublisher(requestNumber: requestNumber)
    }
    .switchToLatest() // 切换到最新的 Publisher 输出

// 订阅并处理结果
let cancellable = sequencePublisher
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("All requests completed")
        case .failure(let error):
            print("Error:", error)
        }
    }, receiveValue: { value in
        print("Received value:", value)
    })

// 添加到集合中以确保不会被释放
var cancellables = Set<AnyCancellable>()
cancellable.store(in: &cancellables)

// 输出
// Received value: Response from request 3
// All requests completed

在这个示例中switchToLatest() 操作符在每次 map 产生的新 Publisher 发出新元素时,自动切换到最新的 Publisher 的输出。

  • 当第一个请求(编号为1)的 fetchDataPublisher 返回其 Future Publisher 时,Combine 开始监听并等待其结果。
  • 然后,当第二个请求(编号为2)的 fetchDataPublisher 返回其 Future Publisher 时,由于使用了 switchToLatest(),Combine 将取消之前的订阅(即第一个请求的 Publisher),开始等待第二个请求的结果。
  • 最后,当第三个请求(编号为3)的 fetchDataPublisher 返回其 Future Publisher 时,Combine 又会取消前一个订阅(即第二个请求的 Publisher),开始等待第三个请求的结果。

因此,由于 switchToLatest() 操作符的特性,它只会处理最新的 Publisher 输出,也就是最后一个请求(编号为3)的结果。前两个请求的结果由于被后续请求的结果所取代,因此不会被打印出来。

  1. collect

收集一组发布者的值,并将其作为数组发布。

let publisher = [1, 2, 3, 4, 5].publisher
let cancellable = publisher.collect()
                           .sink { print("Collected values: ($0)") }
// 输出: Collected values: [1, 2, 3, 4, 5]

序列运算符

  1. scan

累积计算发布者产生的值,类似于 reduce

import Combine

let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher

let cancellable = publisher
    .scan(0) { accumulated, value in
        accumulated + value
    }
    .sink(receiveValue: { value in
        print("Scanned value: (value)")
    })

// 输出:
// Scanned value: 1
// Scanned value: 3
// Scanned value: 6
// Scanned value: 10
// Scanned value: 15
  1. reduce

完成所有值后,进行一次性累积计算。

import Combine

let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher

let cancellable = publisher
    .reduce(0) { accumulated, value in
        accumulated + value
    }
    .sink(receiveValue: { value in
        print("Reduced value: (value)")
    })

// 输出:
// Reduced value: 15
  1. prepend

在发布者之前插入一个值或一组值。

let publisher = [3, 4, 5].publisher
let cancellable = publisher.prepend(1, 2)
                           .sink { print("Prepended value: ($0)") }
// 输出: 1, 2, 3, 4, 5

时间运算符

  1. debounce

在指定的时间间隔内,只取最后一个值。

import Combine
import Foundation

let subject = PassthroughSubject<String, Never>()

let cancellable = subject
    .debounce(for: .seconds(1), scheduler: RunLoop.main)
    .sink(receiveValue: { value in
        print("Debounced value: (value)")
    })

subject.send("A")
subject.send("B")
subject.send("C")

DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
    subject.send("D")
}

// 输出:
// Debounced value: C
// Debounced value: D
  1. throttle

在指定的时间间隔内,只允许产生一个值。

import Combine
import Foundation

let subject = PassthroughSubject<String, Never>()

let cancellable = subject
    .throttle(for: .seconds(1), scheduler: RunLoop.main, latest: true)
    .sink(receiveValue: { value in
        print("Throttled value: (value)")
    })

subject.send("A")
subject.send("B")
subject.send("C")

DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
    subject.send("D")
}

// 输出:
// Throttled value: C
// Throttled value: D

首先,字符串 "A" , "B" 和 "C" 被发送到 subject,但在 1 秒内它们都被发送了,并且设置了latest = truethrottle操作符只会接收最后一个值,即 "C"。

在延迟 1.5 秒后,节流操作会接收到 "D" 并打印。

  1. delay

延迟发布者产生的值。

import Combine
import Foundation

let subject = PassthroughSubject<String, Never>()

let cancellable = subject
    .delay(for: .seconds(1), scheduler: RunLoop.main)
    .sink(receiveValue: { value in
        print("Delayed value: (value)")
    })

subject.send("A")

// 输出 (延迟1秒后):
// Delayed value: A
  1. timeout

如果发布者未在指定时间内发出值,则发出超时错误。

let publisher = PassthroughSubject<Int, Never>()
let cancellable = publisher.timeout(.seconds(3), scheduler: DispatchQueue.main, customError: { URLError(.timedOut) })
                           .sink(receiveCompletion: { print("Completed with: ($0)") }, receiveValue: { print("Value: ($0)") })
// 输出: Completed with: failure(URLError)
  1. measureInterval

measureInterval 是一个用于测量时间间隔的操作符。它可以帮助开发者监测两次元素发出之间的时间间隔,以及订阅者接收到元素之间的时间间隔。这对于性能监控、调试和数据流分析非常有用。

func measureInterval(using scheduler: Scheduler) -> Publishers.MeasureInterval<Self, Scheduler>

它接受一个调度器(Scheduler),返回一个 Publishers.MeasureInterval 类型的发布者。这个操作符会在每次接收到元素时记录当前时间,计算出与上次元素接收之间的时间间隔,并将间隔作为元素发出。

  • scheduler: 用于计算时间间隔的调度器。通常情况下,可以使用 .main(主队列调度器)或者 .immediate(立即执行)等内置调度器。也可以传入自定义的调度器来控制时间间隔的计算行为。

主要使用场景有

  • 监测数据流的速度和频率: 可以用来监测数据流中元素的发出速度,或者计算连续元素之间的时间间隔。
  • 性能监控和调试: 可以用来分析和优化 Combine 数据流的性能,以及查找可能存在的延迟或者频率问题。

以下是一个简单的示例,展示了如何使用 measureInterval 操作符来测量元素发出之间的时间间隔。

import Combine

// 创建一个定时器发布者,每隔一秒发出一个整数
let publisher = Timer.publish(every: 1, on: .main, in: .default)
    .autoconnect()
    .prefix(5) // 限制只发出五个元素,用于示例简化

let cancellable = publisher
    .measureInterval(using: DispatchQueue.main)
    .sink(receiveCompletion: { completion in
        print("Publisher finished with completion:", completion)
    }, receiveValue: { interval in
        print("Received interval:", interval)
    })
    
// 输出结果
// Received interval: 1.0033320188522339
// Received interval: 1.0016639823913574
// Received interval: 1.0016380543708801
// Received interval: 1.0016300086975098
// Received interval: 1.0016199946403503
// Publisher finished with completion: finished

当然如果有时候我们希望将时间间隔和发布者的value同时输出,那么就需要通过flatmap将两者结合起来转化成新的publisher

import Combine
import Foundation

extension Date {
    func customFormatted() -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm:ss"
        let formattedTime = formatter.string(from: self)
        return formattedTime
    }
}

// 创建一个简单的发布者,每隔一秒发出一个整数
let publisher = Timer.publish(every: 1, on: .main, in: .default)
    .autoconnect() // 自动连接
    .prefix(5) // 限制只发出五个元素,用于示例简化

// 记录上一个值接收的时间
var lastReceivedTime: Date?

let cancellable = publisher
    // 使用 flatMap 操作符来转换每个原始值和时间间隔的元组 (Int, TimeInterval)。
    // 在 flatMap 的闭包中,我们计算了当前时间 currentTime,以及上一个值接收时间到当前时间的时间间隔 interval。
    // 然后将值和时间间隔封装成一个元组,并通过 Just 创建一个新的发布者。
    .flatMap { timestamp -> AnyPublisher<(value: String, interval: TimeInterval), Never> in
        let currentTime = Date()
        let interval: TimeInterval = lastReceivedTime.map { currentTime.timeIntervalSince($0) } ?? 0
        lastReceivedTime = currentTime
        return Just((timestamp.customFormatted(), interval)).eraseToAnyPublisher()
    }
    // 使用sink 订阅新的发布者发布的内容
    .sink(receiveCompletion: { completion in
        print("Publisher finished with completion:", completion)
    }, receiveValue: { (value, interval) in
        print("Received value:", value, ", Interval since last value:", interval)
    })

// 取消订阅
DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
    cancellable.cancel()
}

// 输出结果
// Received value: 17:48:06 , Interval since last value: 0.0
// Received value: 17:48:07 , Interval since last value: 0.9998550415039062
// Received value: 17:48:08 , Interval since last value: 0.999998927116394
// Received value: 17:48:09 , Interval since last value: 0.9999690055847168
// Received value: 17:48:10 , Interval since last value: 0.9999220371246338
// Publisher finished with completion: finished

其他运算符

  1. share

使发布者的多个订阅者共享一个订阅。

let publisher = Just("Hello, Combine!")
let sharedPublisher = publisher.share()
let cancellable1 = sharedPublisher.sink { print("Subscriber 1 received: ($0)") }
let cancellable2 = sharedPublisher.sink { print("Subscriber 2 received: ($0)") }
// 输出:
// Subscriber 1 received: Hello, Combine!
// Subscriber 2 received: Hello, Combine!
  1. multicast

multicast 操作符允许你使用指定的 Subject 将单个发布者的输出分发给多个订阅者。这对于需要多个订阅者共享相同的事件流非常有用。

import Combine

// 使用 PassthroughSubject 创建发布者。
let publisher = PassthroughSubject<String, Never>()

// 使用 PassthroughSubject 创建一个 Subject。
let subject = PassthroughSubject<String, Never>()

// 使用 multicast 操作符将发布者与 Subject 关联,创建一个多播发布者。
let multicastedPublisher = publisher.multicast(subject: subject)

// 创建第一个订阅者,并使用 sink 订阅多播发布者。
let cancellable1 = multicastedPublisher
    .sink(receiveCompletion: { completion in
        print("Subscriber 1 completed with: (completion)")
    }, receiveValue: { value in
        print("Subscriber 1 received value: (value)")
    })

// 创建第二个订阅者,并使用 sink 订阅多播发布者。
let cancellable2 = multicastedPublisher
    .sink(receiveCompletion: { completion in
        print("Subscriber 2 completed with: (completion)")
    }, receiveValue: { value in
        print("Subscriber 2 received value: (value)")
    })

// 使用 connect 方法启动多播发布者,将事件传递给订阅者。
let connection = multicastedPublisher.connect()

// 通过 publisher.send 发送事件,所有订阅者会接收到相同的事件。
publisher.send("Hello")
publisher.send("Combine")

// 通过 publisher.send(completion: .finished) 完成发布者,所有订阅者会接收到完成事件。
publisher.send(completion: .finished)

// 通过 connection.cancel 断开多播连接。
connection.cancel()

// 输出结果
// Subscriber 1 received value: Hello
// Subscriber 2 received value: Hello
// Subscriber 1 received value: Combine
// Subscriber 2 received value: Combine
// Subscriber 1 completed with: finished
// Subscriber 2 completed with: finished

在这个例子中,multicast 确保了发布者的事件流被多个订阅者共享,所有订阅者都接收到了相同的事件。

  1. buffer

buffer 操作符用于缓存发布者的输出,确保发布者不会因订阅者处理速度慢而阻塞。它允许你设置缓存的大小和策略(如提前取值和缓存已满时的处理方式)。

let erratic = Just("A").delay(for: 0.0, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher()
    .merge(with: Just("B").delay(for: 0.1, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
    .merge(with: Just("C").delay(for: 0.2, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
    .merge(with: Just("D").delay(for: 0.3, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
    .merge(with: Just("E").delay(for: 2, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
    .merge(with: Just("F").delay(for: 5.0, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
    .merge(with: Just("G").delay(for: 10, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
    .merge(with: Just("H").delay(for: 20, tolerance: 0.001, scheduler: DispatchQueue.main).eraseToAnyPublisher())
    .handleEvents(
        receiveOutput: { print("erratic: (Date().hhmmss()) ($0)") },
        receiveCompletion: { print("erratic: (Date().hhmmss()) ($0)") }
    )
    .makeConnectable()

let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()

let bufferPublisher = erratic
    .buffer(size: 3, prefetch: .byRequest, whenFull: .dropOldest)
    .flatMap { values -> AnyPublisher<String, Never> in
        if values.isEmpty {
            print("Buffer is empty, no value published")
            // Return an empty value to ensure the sink receives a value every second
            return Just("").setFailureType(to: Never.self).eraseToAnyPublisher()
        } else {
            // Join all characters in the values sequence into a single String
            let combinedString = values.map { String($0) }.joined()
            return Just(combinedString).setFailureType(to: Never.self).eraseToAnyPublisher()
        }
    }
    .makeConnectable()

let bufferSubscription = bufferPublisher
    .sink(
        receiveCompletion: { print("paced completion: (Date().hhmmss()) ($0)") },
        receiveValue: { print("paced: (Date().hhmmss()) ($0)") }
    )
    .store(in: &subscribers)


erratic.connect().store(in: &subscribers)
bufferPublisher.connect().store(in: &subscribers)

RunLoop.main.run()
  1. makeConnectable

makeConnectable 是一个操作符,它将一个冷发布者(lazy publisher)转换为一个可连接的发布者(connectable publisher)。冷发布者只有在有订阅者订阅时才会开始发布元素。而可连接的发布者即使没有任何订阅者,在被连接后(调用 connect 方法)也会开始发布元素。

connect 是可连接发布者的方法。调用 connect 会启动数据流,使得发布者开始发布元素。这在需要多个订阅者共享同一个数据流的场景中非常有用。

  • 共享数据流:如果你有一个发布者,多个订阅者需要共享同一个数据流,但你不希望每个订阅者都触发一次新的数据流(例如,多个视图组件需要显示相同的数据),你可以使用 makeConnectableconnect
  • 控制发布开始时间:有时你需要在特定时间点启动数据流,而不是在订阅时立即启动。这时 makeConnectableconnect 就派上用场了。

假设我们有一个网络请求发布者,我们希望多个订阅者共享同一个网络请求的响应数据,而不是每个订阅者都触发一次新的网络请求。

import Combine
import Foundation

// 模拟网络请求的发布者
// 创建一个网络请求发布者,使用 URLSession.shared.dataTaskPublisher(for:) 来发起网络请求
// 并将响应数据解码为 Todo 对象。
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
let publisher = URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }
    .decode(type: Todo.self, decoder: JSONDecoder())
    // 将网络请求发布者转换为一个可连接的发布者。
    .makeConnectable()

struct Todo: Decodable {
    let id: Int
    let title: String
}

// 创建一个可连接的发布者
let connectablePublisher = publisher.makeConnectable()

// 创建两个订阅者,它们将共享同一个网络请求的数据流。
// 第一个订阅者
let subscription1 = connectablePublisher.sink(receiveCompletion: { completion in
    switch completion {
    case .finished:
        print("Subscription 1: Finished")
    case .failure(let error):
        print("Subscription 1: Error: (error)")
    }
}, receiveValue: { todo in
    print("Subscription 1: Received Todo: (todo)")
})

// 第二个订阅者
let subscription2 = connectablePublisher.sink(receiveCompletion: { completion in
    switch completion {
    case .finished:
        print("Subscription 2: Finished")
    case .failure(let error):
        print("Subscription 2: Error: (error)")
    }
}, receiveValue: { todo in
    print("Subscription 2: Received Todo: (todo)")
})

// 调用 connect 方法启动数据流。此时,网络请求将被发起,数据流将开始发布。
let connection = connectablePublisher.connect()

// 取消连接和订阅
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    connection.cancel()
    subscription1.cancel()
    subscription2.cancel()
    print("Cancelled subscriptions and connection")
}

Controlling Publishing with Connectable Publishers | Apple Developer Documentation

  1. receive

receive 操作符用于指定发布者在哪个调度队列(线程)上发布值。它允许将事件从一个队列转移到另一个队列,例如从后台队列转移到主队列。

import Combine

let publisher = Just("Hello, Combine!")
    .receive(on: DispatchQueue.main)

let subscription = publisher.sink { value in
    print("Received on main thread: (value)")
}
  1. send

在 Combine 中,send 操作符常见于 PassthroughSubjectCurrentValueSubject,用于向这些 Subjects 发布新值。

import Combine
import Foundation

// 创建 PassthroughSubject
let passthroughSubject = PassthroughSubject<String, Never>()

// 订阅 PassthroughSubject
let passthroughSubscription = passthroughSubject
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("PassthroughSubject completed")
        case .failure(let error):
            print("PassthroughSubject error: (error)")
        }
    }, receiveValue: { value in
        print("PassthroughSubject received value: (value)")
    })

// 调用 send 方法向订阅者发送值和完成事件。
passthroughSubject.send("Hello")
passthroughSubject.send("Combine")
passthroughSubject.send(completion: .finished)

// 创建 CurrentValueSubject
let currentValueSubject = CurrentValueSubject<String, Never>("Initial value")

// 订阅 CurrentValueSubject
let currentValueSubscription = currentValueSubject
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("CurrentValueSubject completed")
        case .failure(let error):
            print("CurrentValueSubject error: (error)")
        }
    }, receiveValue: { value in
        print("CurrentValueSubject received value: (value)")
    })

// 调用 send 方法向订阅者发送值和完成事件。
currentValueSubject.send("Updated value")
currentValueSubject.send("Another value")
currentValueSubject.send(completion: .finished)

详细实例

为了展示这些操作符的实际用法,以下是一个详细的综合示例,演示如何将它们结合使用:

import Combine
import Foundation

// 模拟网络请求的发布者
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
let publisher = URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }
    .decode(type: Todo.self, decoder: JSONDecoder())
    .receive(on: DispatchQueue.main)
    .makeConnectable()

struct Todo: Decodable {
    let id: Int
    let title: String
}

// ViewModel
class ViewModel: ObservableObject {
    @Published var todoTitle: String = ""
}

let viewModel = ViewModel()

// 订阅并绑定数据到 ViewModel
let connection = publisher.connect()

let subscription1 = publisher
    .map { $0.title }
    .assign(to: .todoTitle, on: viewModel)

// 订阅并打印数据
let subscription2 = publisher.sink(receiveCompletion: { completion in
    switch completion {
    case .finished:
        print("Finished")
    case .failure(let error):
        print("Error: (error)")
    }
}, receiveValue: { todo in
    print("Received Todo: (todo)")
})

// 模拟取消连接
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
    connection.cancel()
    subscription1.cancel()
    subscription2.cancel()
    print("Cancelled subscriptions and connection")
}

参考文档:

  1. Controlling Publishing with Connectable Publishers | Apple Developer Documentation