合并:如何在不完成原始发布者的情况下替换/捕获错误?



给定以下代码:

enum MyError: Error {
case someError
}
myButton.publisher(for: .touchUpInside).tryMap({ _ in
if Bool.random() {
throw MyError.someError
} else {
return "we're in the else case"
}
})
.replaceError(with: "replaced Error")
.sink(receiveCompletion: { (completed) in
print(completed)
}, receiveValue: { (sadf) in
print(sadf)
}).store(in: &cancellables)

每当我点击按钮时,我都会we're in the else case直到Bool.random()为真 - 现在抛出错误。我尝试了不同的事情,但我无法捕获/替换/忽略错误并在点击按钮后继续。

在代码示例中,我希望有例如以下输出

we're in the else case
we're in the else case
replaced Error
we're in the else case
...

相反,我在replaced error后得到finished,并且没有发出任何事件。

编辑给定一个具有AnyPublisher<String, Error>的发布者,当发生错误时,我如何将其转换为AnyPublisher<String, Never>而不完成,即忽略原始发布者发出的错误?

有一部WWDC电影提到过,我相信是2019年的"结合实践",在6:24左右开始观看:https://developer.apple.com/wwdc19/721

是的,.catch()终止上游发布者(电影 7:45),并在.catch的参数中将其替换为给定的发布者,因此在使用Just()作为替代发布者时,通常会导致.finished交付。

如果原始发布者在失败后继续工作,则需要涉及.flatMap()的结构(电影 9:34)。导致可能故障的算子需要在.flatMap内执行,必要时可以在那里处理。诀窍是使用

.flatMap { data in
return Just(data).decode(...).catch { Just(replacement) }
}

而不是

.catch { return Just(replacement) } // DOES STOP UPSTREAM PUBLISHER

.flatMap内部,您总是替换发布者,因此不在乎该替换发布者是否被.catch终止,因为它已经是替换,并且我们原来的上游发布者是安全的。这个例子来自电影。

这也是您的问题的答案 编辑:问题,关于如何将<Output, Error>转换为<Output, Never>,因为.flatMap不会输出任何错误,因此在flatMap之前和之后都从未输出过。所有与错误相关的步骤都封装在flatMap中。 (检查Failure=Never的提示:如果您获得 Xcode 自动完成.assign(to:)那么我相信您有一个 Failure=Never 流,否则该订阅者不可用。 最后是完整的游乐场代码

PlaygroundSupport.PlaygroundPage.current.needsIndefiniteExecution = true
enum MyError: Error {
case someError
}
let cancellable = Timer.publish(every: 1, on: .main, in: .default)
.autoconnect()
.flatMap({ (input) in
Just(input)
.tryMap({ (input) -> String in
if Bool.random() {
throw MyError.someError
} else {
return "we're in the else case"
}
})
.catch { (error) in
Just("replaced error")
}
})
.sink(receiveCompletion: { (completion) in
print(completion)
PlaygroundSupport.PlaygroundPage.current.finishExecution()
}) { (output) in
print(output)
}

我相信E. Coms的答案是正确的,但我会说得简单得多。在不导致管道在出错后停止处理值的情况下处理错误的关键是将错误处理发布商嵌套在flatMap中:

import UIKit
import Combine
enum MyError: Error {
case someError
}
let cancel = [1,2,3]
.publisher
.flatMap { value in
Just(value)
.tryMap { value throws -> Int in
if value == 2 { throw MyError.someError }
return value
}
.replaceError(with: 666)
}
.sink(receiveCompletion: { (completed) in
print(completed)
}, receiveValue: { (sadf) in
print(sadf)
})

输出:

1
666
3
finished

您可以在操场上运行此示例。


关于OP的编辑:

编辑给定一个具有AnyPublisher<String, Error>的发布者,当发生错误时,我如何在不完成的情况下将其转换为AnyPublisher<String, Never>,即忽略原始发布者发出的错误?

你不能。

为此,您可以使用catch运算符和Empty发布者:

let stringErrorPublisher = Just("Hello")
.setFailureType(to: Error.self)
.eraseToAnyPublisher() // AnyPublisher<String, Error>
let stringPublisher = stringErrorPublisher
.catch { _ in Empty<String, Never>() }
.eraseToAnyPublisher() // AnyPublisher<String, Never>

只需按如下方式插入平面地图,即可实现您想要的效果

self.myButton.publisher(for: .touchUpInside).flatMap{
(data: Bool) in
return Just(data).tryMap({ _ -> String in
if Bool.random() {
throw MyError.someError
} else {
return "we're in the else case"
}}).replaceError(with: "replaced Error")
}.sink(receiveCompletion: { (completed) in
print(completed)
}, receiveValue: { (sadf) in
print(sadf)
}).store(in: &cancellables)

工作模型如下所示:

Just(parameter).
flatMap{ (value)->AnyPublisher<String, Never> in 
return MyPublisher(value).catch { <String, Never>() } 
}.sink(....)

如果我们使用上面的例子,它可能是这样的:

let firstPublisher    = {(value: Int) -> AnyPublisher<String, Error> in
Just(value).tryMap({ _ -> String in
if Bool.random() {
throw MyError.someError
} else {
return "we're in the else case"
}}).eraseToAnyPublisher()
}
Just(1).flatMap{ (value: Int) in
return  firstPublisher(value).replaceError(with: "replaced Error")
}.sink(receiveCompletion: { (completed) in
print(completed)
}, receiveValue: { (sadf) in
print(sadf)
}).store(in: &cancellables)

在这里,您可以将firstPublisher替换为接受一个参数的 AnyPublisher。

同样在这里,firstPublisher只有一个值,它只能产生一个值。但是,如果您的发布商可以生成多个值,则它不会在发出所有值之前完成。

我也在为这个问题而苦苦挣扎,我终于找到了解决方案。需要了解的一件事是,您无法从已完成的助焊剂中恢复。解决方案是返回Result而不是Error

let button = UIButton()
button.publisher(for: .touchUpInside)
.map({ control -> Result<String, Error> in
if Bool.random() {
return .failure(MyError.someError)
} else {
return .success("we're in the else case")
}
}).sink (receiveValue: { (result) in
switch(result) {
case .success(let value):
print("Received value: (value)")
case .failure(let error):
print("Failure: (String(describing: error))")
}
})

对于阅读此线程并尝试编译代码的任何其他人,您需要从本文导入代码(对于button.publisher(for: .touchUpIsinde)部分)。

奖励,这是使用PassthroughSubject处理错误并且永远不会完成通量的代码:

let subscriber = PassthroughSubject<Result<String, MyError>, Never>()
subscriber
.sink(receiveValue: { result in
switch result {
case .success(let value):
print("Received value: (value)")
case .failure(let error):
print("Failure: (String(describing: error))")
}
})

不能直接使用PassthroughSubject<String, MyError>(),否则,当发生错误时,通量将完成。

不完成管道和优雅处理错误的最佳方法 我在这篇 Peter Friese 文章中发现: https://peterfriese.dev/posts/swiftui-combine-networking-errorhandling/通过使用此发布服务器扩展方法将我们收到的内容映射到结果:

extension Publisher {
func asResult() -> AnyPublisher<Result<Output, Failure>, Never> {
self
.map(Result.success)
.catch { error in
Just(.failure(error))
}
.eraseToAnyPublisher()
}
}

然后你可以这样使用它:

publisher.flatMap {term in
somePublisherWhichGivesError
.asResult()
}
.sink(receiveValue: { value in
switch value {
case let .failure(error): self.handleError(error)
case let .success(data): self.handleSuccess(data)
}
}).store(in: bag)

我建议将Publishertypealias Failure = Never一起使用 并输出为可选结果:typealias Output = Result<YourSuccessType, YourFailtureType>

发布服务器发出,直到完成或失败(出现错误),之后流将被终止。

解决此问题的一种方法是使用结果作为发布者类型

发行人

protocol SerivceProtocol {
var value: Published<Result<Double, MyError>>.Publisher { get }
}

订户

service.value
.sink { [weak self] in
switch $0 {
case let .success(value): self?.receiveServiceValue(value)
case let .failure(error): self?.receiveServiceError(error)
}
}
.store(in: &subscriptions)

最新更新