Swift Combine 开发小册 - 3. 使用操作符加工数据流

4,519 阅读15分钟

在这个章节里,我们将学习如何使用不同类型的操作符来处理从发布者发送出来的数据,它们是Swift Combine中最强大的工具,但是可能需要一点时间去学习。所以,坐稳了,让我们开始吧。

操作符是在Swift Combine中处理数据流的重要工具。它们是一些方法集合,可以转换、过滤和组合数据流,它们返回新的发布者,让你可以将它们串联起来创建复杂的数据转换,这个过程也被称为重新发布。

我们将这些操作符分为四类:

  • 过滤和转换数据
  • 合并多个数据流
  • 控制时序
  • 错误处理和调试

A. 过滤和转换数据

在本节中,我们将向您展示如何使用这些操作符来过滤和转换数据流,并在实际应用中将它们串联起来:

  • filter
  • map
  • reduce
  • flatMap
  • scan
  • encode
  • decode

filter

在Swift Combine中,filter操作符用于根据提供的条件选择性地从发布者中发出值。filter操作符采用返回布尔值的闭包,并将其应用于发布者发出的每个值。仅当闭包返回true时,才将值转发到下游的订阅者。

以下是使用filter操作符从PassthroughSubject发布者中仅发出偶数整数的示例:

let subject = PassthroughSubject<Int, Never>()

let subscription = subject
    .filter { $0 % 2 == 0 } // 只发出偶数值
    .sink { print($0) }

subject.send(1) // 奇数不会发出
subject.send(2) // 偶数会发出
subject.send(3) // 奇数不会发出
subject.send(4) // 偶数会发出

// 输出: 2 4

在这个例子中,我们创建了一个发出整数的PassthroughSubject发布者。我们使用filter操作符只将偶数整数下发给订阅者。当我们向发布者发送整数时,订阅者只会打印偶数整数。

map

在Swift Combine中,map操作符用于转换发布者发出的值。map操作符采用转换发布者发出的每个值的闭包,并将转换后的值向下游发出给订阅者。

以下是使用map操作符将发出整数的PassthroughSubject发布者转换为发出这些整数的平方值的发布者的示例:

let subject = PassthroughSubject<Int, Never>()

let subscription = subject
    .map { $0 * $0 } // 计算平方
    .sink { print($0) }

subject.send(1)
subject.send(2)
subject.send(3)

// 输出: 1 4 9

在这个例子中,我们创建了一个发出整数的PassthroughSubject发布者。我们使用map操作符将每个发出的整数转换为其平方值,向下游发出给订阅者。当我们向发布者发送整数时,订阅者打印这些整数的平方值。

reduce

在Swift Combine中,reduce操作符用于将发布者发出的值累加到单个值中。它需要一个初始值和一个闭包,该闭包将先前累加的值与发布者发出的当前值结合起来。闭包的结果向下游发出给订阅者,并成为下一个发射的新累积值。

reduce操作符要求发布者在发出最终累加值之前完成。这意味着只有在发布者完成发出值后,订阅者才会收到最终结果。

以下是使用reduce操作符累加由PassthroughSubject发布者发出的所有整数的总和的示例:

let subject = PassthroughSubject<Int, Never>()

let subscription = subject
    .reduce(0, +) // 累加所有值
    .sink { print($0) }

subject.send(1)
subject.send(2)
subject.send(3)
subject.send(completion: .finished)

// 输出: 6

在这个例子中,我们创建了一个发出整数的PassthroughSubject发布者。我们使用reduce操作符将所有发出的整数的总和累加到向下游发出给订阅者的单个值中。当我们向发布者发送整数时,订阅者在发布者完成发出值后打印这些整数的累加和。

reduce操作符用于将发布者发出的值累加到单个值中,但它要求发布者在发出最终结果之前完成。它可以与其他操作符结合使用,以在Swift的响应式编程中创建更复杂的处理管道。

flatMap

在Swift Combine中,flatMap操作符用于将发布者发出的值转换为新的发布者,然后将这些发布者展开为单个值的流。

flatMap操作符采用一个闭包,该闭包返回每个原始发布者发出的发布者。操作符订阅每个内部发布者,并将它们的值向下游转发给订阅者。内部发布者被展平为单个值的流,这些值按接收顺序发出。

以下是使用flatMap操作符展开发出整数数组的PassthroughSubject发布者的示例:

let subject = PassthroughSubject<[Int], Never>()

let subscription = subject
    .flatMap { array in
        return array.map({ $0 + 1 }).publisher // 变换每个元素并转换为发布者
    }
    .sink { print($0) }

subject.send([1, 2, 3])
subject.send([4, 5, 6])

// 输出: 2 3 4 5 6 7

在这个例子中,我们创建了一个发出整数数组的PassthroughSubject发布者。我们使用flatMap操作符将发出的数组中的每个整数加1进行转换。然后,我们使用publisher属性将每个转换后的整数转换为发布者,并将结果发布者展开为单个值的流。当我们向发布者发送整数数组时,订阅者按接收顺序打印展开的整数流。

flatMap操作符用于将发布者发出的值转换为新的发布者,并将它们展开为单个值的流。它可以与其他操作符结合使用,以在Swift的响应式编程中创建更复杂的处理管道。

scan

在Swift Combine中,scan操作符用于将发布者发出的值累加到中间结果序列中。它类似于reduce操作符,但不是发出最终累加值,而是在计算每个中间结果时将其发出。

scan操作符需要一个初始值和一个闭包,该闭包将先前累加的值与发布者发出的当前值结合起来。闭包的结果将作为中间结果向下游发出给订阅者,并成为下一个发射的新累积值。

以下是使用scan操作符累加由PassthroughSubject发布者发出的整数的运行总和的示例:

let subject = PassthroughSubject<Int, Never>()

let subscription = subject
    .scan(0, +) // 累加所有发出的数
    .sink { print($0) }

subject.send(1)
subject.send(2)
subject.send(3)

// 输出: 1 3 6

在这个例子中,我们创建了一个发出整数的PassthroughSubject发布者。我们使用scan操作符将所有发出的整数的运行总和累加到向下游发出给订阅者的中间结果序列中。当我们向发布者发送整数时,订阅者在每个值被发出后打印运行总和的中间结果。

scan操作符用于将发布者发出的值累加到中间结果序列中。它可以与其他操作符结合使用,以在Swift的响应式编程中创建更复杂的处理管道。

encode

在Swift Combine中,encode操作符用于将发布者发出的值编码为指定格式,例如JSON或XML。

encode操作符需要一个遵循TopLevelEncoder协议的编码器实例,并返回一个发布者,该发布者使用提供的编码器将由上游发布者发出的值编码为指定格式,这些值需要符合Encodable协议。

以下是使用encode操作符将自定义结构体编码为JSON的示例:

struct Person: Codable {
    var name: String
    var age: Int
}

let person = Person(name: "John", age: 30)

let publisher = Just(person)
    .encode(encoder: JSONEncoder()) // 将 person 编码为 JSON Data
    .map { String(data: $0, encoding: .utf8) } // 将 Data 转换为 String
    .sink(receiveCompletion: { print($0) }, receiveValue: { print($0 ?? "") })

// 输出: {"name":"John","age":30}

在这个例子中,我们使用Just发布者发出一个自定义的Person结构体。然后,我们使用encode操作符将Person结构体编码为JSON数据,使用JSONEncoder。为了将结果Data对象转换为String,我们使用map操作符将String(data:encoding:)初始化器应用于每个发出的Data对象。最后,生成的String被向下游发出给订阅者,订阅者打印编码的JSON字符串。

encode操作符用于将发布者发出的值编码为指定格式以进行进一步处理或通信。它可以与其他操作符结合使用,以在Swift的响应式编程中创建更复杂的处理管道。

decode

在Swift Combine中,decode操作符用于将由发布者发出的特定格式(例如JSON或XML)的数据流解码为符合Decodable协议的Swift类型的实例。

decode操作符需要一个遵循TopLevelDecoder协议的解码器实例,并返回一个发布者,该发布者使用提供的解码器对上游发布者发出的值进行解码。

以下是使用decode操作符将JSON数据流解码为自定义Person结构体实例的示例:

struct Person: Codable {
    var name: String
    var age: Int
}

let jsonData = """
    {"name": "John", "age": 30}
""".data(using: .utf8)!

let publisher = Just(jsonData)
    .decode(type: Person.self, decoder: JSONDecoder()) // 解码为 Person 类型
    .sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })

// 输出: Person(name: "John", age: 30)

在这个例子中,我们创建一个Just发布者,该发布者发出一个JSON数据流。然后,我们使用decode操作符将JSON数据解码为自定义Person结构体的实例,使用JSONDecoder。生成的Person对象被向下游发出给订阅者,订阅者打印解码后的值。

decode操作符用于将发布者发出的数据流解码为自定义类型的实例,以进行进一步处理或在应用程序中使用。它可以与其他操作符结合使用,以在Swift的响应式编程中创建更复杂的处理管道。

操作符链接

可以将操作符链接在一起,以创建更复杂的数据转换,每个步骤的转换都会创建一个新的发布者。现在,让我们进行一些操作符链接。

  • 链接mapfilter
let publisher = PassthroughSubject<Int, Never>()
var cancellables = Set<AnyCancellable>()

publisher
    .map { value in
        return value * 2
    }
    .filter { value in
        return value % 3 == 0
    }
    .sink { value in
        print("Received value: \(value)")
    }
    .store(in: &cancellables)

publisher.send(1)
publisher.send(2)
publisher.send(3)
publisher.send(4)
publisher.send(5)

// 输出: Received value: 6

这将创建一个发布者(publisher),它会发出整数值,然后链接mapfilter操作符。map操作符会将发出的值加倍,filter操作符仅传递可被3整除的值。然后订阅者接收到过滤和映射的值(6),并将它们打印到控制台。

  • 链接flatMapfilter
let publisher = PassthroughSubject<String, Never>()
var cancellables = Set<AnyCancellable>()

publisher
    .flatMap { value in
        return Just(value.count)
    }
    .filter { count in
        return count % 2 == 0
    }
    .sink { count in
        print("Received value with even count: \(count)")
    }
    .store(in: &cancellables)

publisher.send("Hello")
publisher.send("World!")
publisher.send("Swift")

// 输出: Received value with even count: 6

这将创建一个发布者(publisher),它会发出字符串值,然后链接flatMapfilter操作符。flatMap操作符将每个字符串值转换为一个发出该字符串计数的新发布者。filter操作符仅传递计数为偶数的值。然后订阅者接收到过滤和映射的值(6),并将它们打印到控制台。

  • 链接mapfilterreduce
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

let result = numbers.publisher
    .map { $0 * 2 } // Multiply each number by 2
    .filter { $0 % 3 == 0 } // Filter out any numbers that aren't divisible by 3
    .reduce(0, +) // Sum all of the remaining numbers
    .sink { print($0) }

print(result) // 输出: 36

在这个例子中,我们有一个从1到10的数字数组。我们使用publisher属性从该数组创建一个发布者。我们然后链接三个运算符:

  • map:我们使用map运算符将每个数字乘以2,以便我们最终得到一个数字从2到20的数组。
  • filter:我们使用filter运算符删除任何不能被3整除的数字,以便我们最终得到一个数字6、12和18的数组。
  • reduce:最后,我们使用reduce运算符对所有剩余的数字进行求和,从0开始。结果是36。

B. 合并多个数据流

在某些情况下,您可能需要在应用程序中组合多个数据流。这可能对于合并用户输入和来自服务器的数据或基于多个数据源更新UI组件非常有用。Swift Combine提供了各种运算符来帮助您组合多个数据流。

zip

在Swift Combine中,zip运算符用于将两个发布者组合成一个新的发布者,该发布者发出包含每个输入发布者的最新值的元组。

返回的发布者等待两个发布者都发出事件,然后将每个发布者的最旧未消耗事件一起作为元组传递给订阅者。如果任何一个上游发布者成功完成或因错误而失败,则zip发布者也会如此。

以下是使用zip运算符将接收用户输入值的两个发布者组合的示例:

let username = PassthroughSubject<String, Never>()
let password = PassthroughSubject<String, Never>()

let credentials = Publishers.Zip(username, password)
    .map { (username: $0, password: $1) }

let cancellable = credentials
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print("Username: \($0.username), Password: \($0.password)") })

username.send("john.doe@example.com")
password.send("password123")
password.send("password456")
username.send("mike.gould@example.com")
username.send(completion: .finished)
/*
  Output:
  Username: john.doe@example.com, Password: password123
  Username: mike.gould@example.com, Password: password456
  finished
*/

在这个例子中,我们创建了两个PassthroughSubject发布者,usernamepassword,用于接收用户输入的值。我们使用Zip操作符将这两个发布者组合成一个新的发布者credentials,它发出包含每个输入发布者的最新值的元组。然后我们使用map操作符将元组转换为更有意义的类型,以适应我们的用例。

最后,我们使用sink操作符订阅credentials发布者并打印它发出的最新值。然后,我们向usernamepassword发布者分别发送两对值,并观察sink操作符的输出。

这个例子的输出是两个元组,分别是发送到usernamepassword发布者的用户名和密码值。然后,username发送了完成事件,导致zipped发布者也完成。

类似地,您可以使用Publishers.Zip3Publishers.Zip4来 zip 三个或四个发布者(没有Zip5)。

merge

在 Swift Combine 中,merge 操作符用于将多个发布者合并为一个单一的发布者,以非确定性的顺序发出所有输入发布者的值。

merge 操作符接受可变数量的输入发布者,并返回一个新的发布者,该发布者会在接收到所有输入发布者的值时发出这些值。每当一个输入发布者发出新值时,merge 操作符就会将该值下游发出。

下面是一个将两个 PassthroughSubject 发布者使用 merge 方法合并在一起并发出其值的示例:

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

let mergedPublisher = publisher1
    .merge(with: publisher2)
    .sink { print($0) }

publisher1.send(1)
publisher2.send(2)
publisher1.send(3)
publisher2.send(4)

// Output: 1, 2, 3, 4

在这个例子中,我们创建了两个 PassthroughSubject 发布者 publisher1publisher2,它们都发出整数值。我们使用 merge 方法将这两个发布者合并成一个名为 mergedPublisher 的新发布者,它会按接收到的顺序从两个发布者中发出值。然后我们使用 sink 运算符订阅 mergedPublisher 并打印它发出的最新值。

最后,我们向 publisher1publisher2 分别发送四个整数值,并观察 sink 运算符的输出。正如我们所预期的,输出包括按发送顺序发送的所有四个整数值。

您可以合并任意数量的发布者,merge 运算符在您有多个发出相关数据流的发布者需要一起处理时非常有用。

combineLatest

在 Swift Combine 中,combineLatest 运算符用于将多个发布者的最新值组合成一个新的发布者。每当任何一个输入发布者发出新值时,combineLatest 运算符就会将每个发布者的最新值组合并作为元组向下游发送组合结果。

以下是使用 combineLatest 运算符将两个发出整数值的发布者组合的示例:

let numbers1 = PassthroughSubject<Int, Never>()
let numbers2 = PassthroughSubject<Int, Never>()

let combinedPublisher = numbers1
    .combineLatest(numbers2)
    .sink { print("Numbers: \($0)") }

numbers1.send(1)
numbers2.send(2)
numbers1.send(3)
numbers2.send(4)

// Output: Numbers: (1, 2), Numbers: (3, 2), Numbers: (3, 4)

在这个例子中,我们创建了两个 PassthroughSubject 发布者,分别是 numbers1numbers2,它们都会发出整数值。我们使用 combineLatest 操作符将两个发布者的最新值组合起来,并将组合后的结果作为元组向下游发布。然后我们使用 sink 操作符订阅 combinedPublisher 并打印它发出的最新组合值。

最后,我们向 numbers1numbers2 分别发送了四个整数值,并观察 sink 操作符的输出。正如预期的那样,输出包含 numbers1numbers2 的最新组合值,每当其中任何一个发布者发出新值时都会更新。

combineLatest 操作符可以在需要将多个发布者的最新值组合在一起并一起处理它们的情况下发挥作用。例如,它可以用于在任何输入数据源更改时更新用户界面。

C. 控制时序

measureInterval

该运算符测量了发布者的每次发出动作之间的时间间隔,并将该间隔作为 TimeInterval 值进行再次发布。它非常适用于计时相关任务,例如测量游戏或动画的帧率。

var cancellables = Set<AnyCancellable>()
let publisher = Timer.publish(every: 1, on: .main, in: .default).autoconnect()

publisher
    .measureInterval(using: DispatchQueue.global())
    .sink { interval in
        print("Received interval: \(interval)")
    }
    .store(in: &cancellables)

这个示例创建了一个计时器发布者(publisher),每隔一秒钟发出一个值,并使用measureInterval 运算符测量每次发出之间的时间间隔。订阅者接收每个发出之间的时间间隔,并将其打印到控制台。

debounce

这个操作符会推迟一个发布者(Publisher)发出值的时间,确保在指定的时间内只发出最后一个值,在这之前的值都将被丢弃。这对于过滤噪声或重复值非常有用,或者用于处理用户输入,因为它们可能会在短时间内生成多个事件。

let publisher = PassthroughSubject<String, Never>()

publisher
    .debounce(for: .seconds(1), scheduler: DispatchQueue.main)
    .sink { value in
        print("Received value: \(value)")
    }
    .store(in: &cancellables)

publisher.send("H")
publisher.send("He")
publisher.send("Hel")
publisher.send("Hell")
publisher.send("Hello")
try? await Task.sleep(nanoseconds: 1000_000_000)
publisher.send("World")
try? await Task.sleep(nanoseconds: 1000_000_000)
publisher.send(completion: .finished)
try? await Task.sleep(nanoseconds: 1000_000_000)
/*
  output:
  Received value: Hello
  Received value: World
*/

这个例子中,我们创建了一个发布者(publisher),它发出字符串类型的值,然后使用防抖操作符(debounce operator)只传递在一秒内没有被另一个值跟随的值。订阅者会接收到这些经过防抖后的值(Hello 和 World),并将它们打印到控制台上。这个操作符通常用于过滤掉噪声或重复的值,或者处理用户输入时可能在短时间内生成多个事件的情况。

delay

这个操作符可以让一个发布者(publisher)延迟一段指定的时间再发送值。这对于模拟网络延迟或其他基于时间的效果很有用。

var cancellables = Set<AnyCancellable>()
let publisher = Just("Hello, world!")

publisher
    .delay(for: .seconds(2), scheduler: DispatchQueue.main)
    .sink { value in
        print("Received value: \(value)")
    }
    .store(in: &cancellables)

这个例子中,我们创建了一个只会发出一个字符串值 (Hello, world!) 的 publisher (publisher),接着使用 delay 运算符来延迟这个值的发出,延迟时间为两秒钟。然后订阅者会接收到这个延迟后的值 (Hello, world!) 并将其打印到控制台上。这个运算符通常用于模拟网络延迟或者其他需要按时间延迟的效果。

throttle

这个操作符限制了一个发布者每隔一段指定的时间间隔只能发出最新或最早的值。这对于处理用户输入或其他快速变化的数据流非常有用。

var cancellables = Set<AnyCancellable>()
let publisher = PassthroughSubject<String, Never>()

publisher
    .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true)
    .sink { value in
        print("Received value: \(value)")
    }
    .store(in: &cancellables)

publisher.send("H")
publisher.send("He")
publisher.send("Hel")
publisher.send("Hell")
publisher.send("Hello")
publisher.send("World")
publisher.send(completion: .finished)

这里创建了一个发布者(publisher),它会不断地发出字符串类型的值,然后使用 throttle 操作符,仅在指定时间内没有后续值时,才将值传递给下游订阅者。latest 参数设置为 true,这意味着在时间窗口内最新的值将被传递给订阅者。订阅者最后会收到被节流后的值(World),并将其打印到控制台上。如果将 latest 改为 false,则订阅者会收到最早的值 H

timeout

timeout 运算符在一定时间内没有接收到任何数据时,会将 Publisher 的流转化为错误流,并在订阅者处触发错误事件。这个运算符非常适合处理网络请求或其他时间敏感的任务,因为如果在规定的时间内没有收到回应,则可以中断任务并给出错误提示。

enum TimeoutError: Error {
    case timeout
}

var cancellables = Set<AnyCancellable>()
let publisher = PassthroughSubject<String, TimeoutError>()

publisher
    .timeout(.seconds(2), scheduler: DispatchQueue.main, customError: { () -> TimeoutError in
        return TimeoutError.timeout
    })
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("Finished")
            case .failure(let error):
                print("Error: \(error)")
            }
        },
        receiveValue: { value in
            print("Received value: \(value)")
        }
    )
    .store(in: &cancellables)

publisher.send("Hello")
publisher.send("World")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    publisher.send("Timeout")
}

这段代码创建了一个发布器(publisher),它会发出一些字符串类型的值,并使用timeout操作符来设置每个值的发射时间限制为2秒。如果一个值在时间限制内未被发出,发布器会向订阅者发送一个自定义的错误(TimeoutError)。订阅者会接收到超时的值和错误,并将它们打印到控制台上。

当代码执行时,它会在控制台上输出以下内容:

Received value: Hello
Received value: World
Error: timeout

D. 错误处理和调试

在使用响应式编程和异步数据流时,处理错误并有效地调试代码非常重要。Swift Combine 提供了各种用于错误处理和调试的工具。以下是一些可用的技巧:

catch

  • catch操作符允许您处理由发布者发出的错误。
  • 当发出错误时,catch操作符可以用新值替换错误或发出新错误。
  • 使用此操作符处理错误并从中恢复。

下面是一个示例,演示如何使用catch操作符处理错误:

enum MyError: Error {
    case invalidValue
}

let numbers: [Any] = [1, 2, 3, 4, 5, "Six", 7, 8, 9, 10]
let publisher = numbers.publisher
let filteredPublisher = publisher.tryMap { value -> Int in  
	guard let intValue = value as? Int else {  
		throw MyError.invalidValue  
	}  
	return intValue
}.catch { error in  
	return Just(0)
}

filteredPublisher.sink { print($0) }

在这个例子中,我们创建了一个发布者,它会发出整数和一个字符串值。我们使用 tryMap 运算符将每个值转换为整数,如果值不是整数则抛出错误,并且当 map 的闭包抛出错误时,发布将停止发布。我们使用 catch 运算符将任何错误替换为默认值 0。当我们使用 sink 方法订阅 filteredPublisher 时,我们会打印出它所发出的每个值,包括替换的值,最终还会打印出错误的发射值。

1
2
3
4
5
0

catch 运算符可以用于在发布者链中优雅地处理错误,并通过返回一个新的发布者来从错误中恢复。

print

  • print 操作符允许你打印有关发布者的调试信息。
  • 你可以打印每个值,以及任何发生的错误。
  • 使用此操作符可以了解数据流中发生了什么。

以下是一个示例,演示如何使用 print 操作符打印调试信息:

var cancellables = Set<AnyCancellable>()
let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher
publisher
    .print("Numbers")
    .sink { value in
        print("Received value: \(value)")
    }
    .store(in: &cancellables)

在这个例子中,我们使用 print 运算符在 publisher 发出值之前打印调试信息。当我们使用 sink 方法订阅该 publisher 时,我们打印出它发出的每个值。这段代码的输出如下:

Numbers: receive subscription: ([1, 2, 3, 4, 5])
Numbers: request unlimited
Numbers: receive value: (1)
Received value: 1
Numbers: receive value: (2)
Received value: 2
Numbers: receive value: (3)
Received value: 3
Numbers: receive value: (4)
Received value: 4
Numbers: receive value: (5)
Received value: 5
Numbers: receive finished

print 运算符将调试信息添加到我们的数据流中,显示我们何时订阅、请求值和完成。通过使用 print 运算符,我们可以更好地理解我们的数据流并进行调试。

breakpoint

breakpoint 操作符允许你在 Xcode 的调试器中暂停代码执行,当一个 publisher 发送一个值时,让你可以方便地调试复杂的数据流,并理解代码的运行方式。使用这个操作符可以帮助你识别和解决 Combine 代码中的问题。

下面是一个示例,展示如何使用 breakpoint 操作符在 Xcode 的调试器中暂停代码执行:

var cancellables = Set<AnyCancellable>()
let numbers = [1, 2, 3, 4, 5]
let publisher = numbers.publisher

publisher
    .breakpoint(receiveOutput: { value in
        return value > 3
    })
    .sink { value in
        print("Received value: \(value)")
    }
    .store(in: &cancellables)

在这个例子中,我们使用breakpoint操作符来在Xcode调试器中暂停代码执行,当publisher发送大于3的值时就会触发。在Xcode中运行代码时,当它遇到大于3的值时,调试器将暂停在带有breakpoint操作符的行上,让我们可以检查数据流的状态并识别任何问题。

breakpoint操作符是一个强大的调试工具,它允许我们暂停代码执行并实时检查数据流,帮助我们快速、高效地识别和解决问题。

breakpointOnError

Swift Combine 中的 breakpointOnError 操作符是一个调试工具,它允许您暂停发布者流并检查导致流终止的错误。当调试复杂的发布者链以帮助确定错误的位置和可能导致错误的原因时,这将是有用的。

以下是在发布者链中使用 breakpointOnError 操作符的示例:

enum ExampleError: Error {
    case example
}

var cancellables = Set<AnyCancellable>()
let numbers = [1, 2, 3, 4, 5].publisher

numbers
    .map { $0 * 2 }
    .filter { $0 % 3 == 0 }
    .tryMap { value -> Int in
        if value == 6 {
            throw ExampleError.example
        }
        return value
    }
    .breakpointOnError()
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("Finished")
            case .failure(let error):
                print("Error: \(error)")
            }
        },
        receiveValue: { value in
            print(value)
        }
    )
    .store(in: &cancellables)

在此示例中,我们创建一个 Publisher,从数组中发出一系列整数。然后链接多个操作符,包括 mapfiltertryMap,来转换和过滤发出的值。

我们在 tryMap 操作符之后插入 breakpointOnError 操作符。如果在发布者链执行期间发生错误,则流会暂停等待你调试错误,然后才会继续进行。

在这个示例中,当遇到值 6 时,我们故意抛出一个错误。当 tryMap 操作符遇到这个错误时,breakpointOnError 操作符将暂停流,让你检查错误和当前发布者链的状态。一旦调试完成,你可以通过调试器中的继续按钮来恢复流。

breakpointOnError 操作符在调试复杂的发布者链时特别有用,尤其是在很难追踪错误源的情况下。

handleEvents

在 Swift Combine 中,handleEvents 运算符允许你在订阅、收到值、完成等不同阶段的生命周期中将闭包附加到一个发布者上。这对于调试和监视发布者的行为以及在发布者链中执行额外的副作用非常有用。

以下是在一个发布者链中使用 handleEvents 运算符的示例:

var cancellables = Set<AnyCancellable>()
let numbers = [1, 2, 3, 4, 5].publisher

numbers
    .handleEvents(
        receiveSubscription: { subscription in
            print("Received subscription: \(subscription)")
        },
        receiveOutput: { value in
            print("Received value: \(value)")
        },
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("Finished")
            case .failure(let error):
                print("Error: \(error)")
            }
        },
        receiveCancel: {
            print("Cancelled")
        },
        receiveRequest: { demand in
            print("Received demand: \(demand)")
        }
    )
    .map { $0 * 2 }
    .sink { print($0) }

在这个例子中,我们创建了一个Publisher,它从一个数组中发出整数序列。然后我们使用handleEvents操作符在不同的时刻附加闭包到该Publisher,例如在订阅和完成事件前后。这对于调试和监视Publisher的行为以及在Publisher链中执行附加副作用非常有用。

我们使用handleEvents操作符的不同闭包参数来打印关于该Publisher的订阅、输出、完成、取消和需求事件的信息。

然后,我们链式调用map操作符来使发出的值加倍,最后使用sink操作符打印出最终值。

通过使用handleEvents操作符,我们可以在Publisher的生命周期的不同阶段监视和调试其行为。例如,我们可以打印下游订阅者发出的需求请求或检测何时取消了订阅。我们还可以在Publisher链中执行其他附加副作用,例如日志记录或更新用户界面元素。

在下一章中,我们将探讨Swift Combine和传统编程模型之间的差异,并了解Swift Combine如何改进我们的编程流程。

文章首发发布地址:Github

Photo by KOMMERS on Unsplash