我如何创建 Swift Combine 来自两个发布商 A 和 B 的发布商,其中发布商 B 使用发布商 A 的值?

How can I create a Swift Combine publisher from two publishers A and B where publisher B consumes the value from publisher A?

我想创建一个 Swift Combine 发布者,它实现了以下目标:

这是我目前尝试过的简化版本:

func tasksPublisher() -> AnyPublisher<[Task], Never> {
    Defaults.publisher(.myUserDefault)
        .flatMap { change in
            let myUserDefault = change.newValue

            return ValueObservation
                .tracking { db in
                    try Task.
                        .someFilter(myUserDefault)
                        .fetchAll(db)
                }
                .removeDuplicates()
                .publisher(in: database)
                .eraseToAnyPublisher()
        }
        .eraseToAnyPublisher()
}

但是,这个发布者产生了以下错误(根据我上面的发布者的简化版本编辑):

Cannot convert return expression of type 'AnyPublisher<Publishers.FlatMap<_, AnyPublisher<Defaults.KeyChange<Int>, Never>>.Output, Publishers.FlatMap<_, AnyPublisher<Defaults.KeyChange<Int>, Never>>.Failure>' (aka 'AnyPublisher<_.Output, Never>') to return type 'AnyPublisher<[Task], Never>'

我敢打赌,这两个发布商具有不同的值:[Task]Defaults.KeyChange<Int> 存在问题。但是,我找不到解决此问题的方法。

假设您希望在每次默认发布者发出更改时启动一个新的数据库发布者,您需要 switchToLatest() 运算符。

该运营商需要协调两个发布商的错误。在这里,由于 Defaults.publisher 具有 Never 故障类型,我们可以使用 setFailureType(to:) 运算符来收敛数据库发布者故障类型:Error.

这给出:

func tasksPublisher() -> AnyPublisher<[Task], Error> {
    Defaults
        .publisher(.myUserDefault)
        .setFailureType(to: Error.self)
        .map({ change -> DatabasePublishers.Value<[Task]> in
            let myUserDefault = change.newValue
            return ValueObservation
                .tracking { db in
                    try Task
                        .someFilter(myUserDefault)
                        .fetchAll(db)
                }
                .removeDuplicates()
                .publisher(in: database)
        })
        .switchToLatest()
        .eraseToAnyPublisher()
}

请注意,返回的发布者具有 Error 故障类型,因为数据库并非 100% 可靠,因为所有 I/O 外部性。在 Stack Overflow 的回答中,建议此时隐藏错误(例如,通过将它们变成一个空的 Task 数组)是很困难的,因为隐藏错误会阻止您的应用知道哪里出了问题并做出相应的反应。

然而,下面是一个捕获数据库错误的版本。这是我会使用的版本,假设应用程序不能 运行 当 SQLite 不工作时:假装这样的低级错误可以以用户友好的方式被捕获和处理有时是没有用的。

// Traps on database error
func tasksPublisher() -> AnyPublisher<[Task], Never> {
    Defaults
        .publisher(.myUserDefault)
        .map({ change -> AnyPublisher<[Task], Never> in
            let myUserDefault = change.newValue
            return ValueObservation
                .tracking { db in
                    try Task
                        .someFilter(myUserDefault)
                        .fetchAll(db)
                }
                .removeDuplicates()
                .publisher(in: database)
                .assertNoFailure("Unexpected database failure")
                .eraseToAnyPublisher()
        })
        .switchToLatest()
        .eraseToAnyPublisher()
}