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

时间:2018-04-17 17:50:54

标签: ios swift reactive-cocoa reactive-swift

将ReactiveSwift SignalProducer<A, NetworkError>转换为Signal<A, NoError>的最优雅方式是什么?

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

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

(为什么?因为我试图be as MVVM as possible

到目前为止,我最终编写了大量类似以下内容的样板文件:

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. 它感觉很脏,并使代码如此交织在一起,这有点像被动反应
  4. (A)保持我的信号不可选的任何帮助,(B)优雅地将它们分成2个NoError信号将非常感激。

1 个答案:

答案 0 :(得分:5)

编辑 - 第二次尝试

我将尽力回答您的所有问题/意见。

  

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

您是对的,这是因为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)的任何错误都不会终止流程
  3. 我认为解决这两个问题的最简单方法是使用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
        }
    }
    

    如果有帮助,请告诉我!我实际上认为它看起来比第一次尝试更好:)

    第一次尝试

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

    即可
    // 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)
    }