如何使用 ReactiveSwift 将有错误的信号转换为 NoError 信号?(并且要优雅)



将我的 ReactiveSwiftSignalProducer<A, NetworkError>转换为Signal<A, NoError>的最优雅的方法是什么?

大多数时候,我的信号生产者是网络调用的结果,所以我想将结果分为两种情况:

  • 如果值可用,请发送Signal<A, NoError>
  • 如果发生错误,请发送包含错误本地化说明的Signal<String, NoError>

(为什么?因为我试图成为尽可能的MVVM)

到目前为止,我最终写了很多样板文件,如下所示:

let resultsProperty = MutableProperty<SearchResults?>(nil)
let alertMessageProperty = MutableProperty<String?>(nil)
let results = resultsProperty.signal // `Signal<SearchResults?, NoError>`
let alertMessage = alertMessageProperty.signal // `Signal<String?, NoError>`
// ...
searchStrings.flatMap(.latest) { string -> SignalProducer<SearchResults, NetworkError> in
return MyService.search(string)
}
.observe { event in 
switch event {
case let .value(results):
resultsProperty.value = results
case let .failed(error):
alertMessageProperty.value = error
case .completed, .interrupted:
break
}
}

即:

  1. 使用MutableProperty实例,我必须将其设置为可选才能初始化它们
  2. 从中创建信号,即获取信号发送选项
  3. 它感觉很脏,使代码如此交织在一起,以至于有点破坏了反应性的观点。

任何关于 (A) 保持我的信号非可选和(B)将它们优雅地分成 2 个NoError信号的帮助将不胜感激。

编辑 - 第二次尝试

我将尝试在这里回答您的所有问题/评论。

errors = part 不起作用,因为 flatMapError 需要一个 SignalProducer(即你的示例代码工作只是因为 searchString 是一个信号字符串,巧合的是,它与我们想要的错误相同:它不适用于任何其他类型的输入)

你是对的,这是因为flatMapError不会更改value类型。(其签名为func flatMapError<F>(_ transform: @escaping (Error) -> SignalProducer<Value, F>) -> SignalProducer<Value, F>)。如果需要将其更改为其他值类型,则可以在此之后添加另一个对map的调用。

结果 = 部件的行为很奇怪,因为它在我的现实生活中一旦遇到错误就会终止信号(这是我不想要的行为)

是的,这是因为flatMap(.latest)将所有错误转发到外部信号,外部信号上的任何错误都将终止它。

好的,这是代码的更新版本,具有额外的要求

  1. errors应该具有与searchStrings不同的类型,假设Int
  2. 来自MyService.search($0)的任何错误都不会终止流

我认为解决这两个问题的最简单方法是使用materialize().它的作用基本上是将所有信号事件(新值、错误、终止)"包装"到一个Event对象中,然后在信号中转发这个对象。因此,它将Signal<A, Error>类型的信号转换为Signal<Event<A, Error>, NoError>(您可以看到返回的信号不再有错误,因为它被包裹在Event中)。

在我们的例子中,这意味着您可以使用它来轻松防止信号在发出错误后终止。如果错误被包装在Event中,那么它不会自动终止发送它的信号。(实际上,只有信号调用materialize()完成,但我们会将其包装在flatMap内,因此外部的呼叫不应完成。

下面是它的外观:

// Again, I assume this is what you get from the user
let searchStrings: Signal<String, NoError>
// Keep your flatMap
let searchResults = searchStrings.flatMap(.latest) {
// Except this time, we wrap the events with `materialize()`
return MyService.search($0).materialize()
}
// Now Since `searchResults` is already `NoError` you can simply
// use `filterMap` to filter out the events that are not `.value`
results = searchResults.filterMap { (event) in
// `event.value` will  return `nil` for all `Event` 
// except `.value(T)` where it returns the wrapped value
return event.value
}
// Same thing for errors
errors = searchResults.filterMap { (event) in
// `event.error` will  return `nil` for all `Event` 
// except `.failure(Error)` where it returns the wrapped error
// Here I use `underestimatedCount` to have a mapping to Int
return event.error?.map { (error) in 
// Whatever your error mapping is, you can return any type here
error.localizedDescription.characters.count
}
}

让我知道这是否有帮助!我实际上认为它看起来比第一次尝试更好:)


第一次尝试

您是否需要访问视图模型的状态,或者您是否正在尝试完全无状态?如果是无状态,则不需要任何属性,只需执行

// I assume this is what you get from the user
let searchStrings: Signal<String, NoError>
// Keep your flatMap
let searchResults = searchStrings.flatMap(.latest) {
return MyService.search($0)
}
// Use flatMapError to remove the error for the values
results = searchResults.flatMapError { .empty }
// Use flatMap to remove the values and keep the errors
errors = searchResults.filter { true }.flatMapError { (error) in
// Whatever you mapping from error to string is, put it inside
// a SignalProducer(value:)
return SignalProducer(value: error.localizedDescription)
}

最新更新