【问题标题】:SwiftUI + Combine - Publisher terminates upon first errorSwiftUI + Combine - Publisher 在第一个错误时终止
【发布时间】:2020-11-23 19:45:57
【问题描述】:

我想在用户在搜索字段中输入 3 个或更多字母时执行 API 调用。

我终于让它工作了,但不幸的是,当我关闭服务器并在服务器上发现Publisher 在第一个错误时终止,并且当用户再次在搜索字段中键入文本时,没有进行 API 调用。

我在Combine 上观看了 WWDC 2019 视频并阅读了一些博客文章,但似乎Combine API 经常变化,每个来源的所有内容都不同,当我修补它时,编译器经常抛出无用的错误,例如Fix: Replace type X with type X(见截图)

PS:我知道我可以使用 filter 过滤掉少于 3 个字母的查询,但不知何故我无法让发布者和类型正常工作。我觉得我在 Combine 上遗漏了一些基本内容。 ..

代码如下:

DictionaryService.swift

class DictionaryService {

    func searchMatchesPublisher(_ query: String,
                                inLangSymbol: String,
                                outLangSymbol: String,
                                offset: Int = 0,
                                limit: Int = 20) -> AnyPublisher<[TranslationMatch], Error> {
        
    ...
}

DictionarySearchViewModel.swift

class DictionarySearchViewModel: ObservableObject {
    @Published var inLang = "de"
    @Published var outLang = "en"
    
    @Published var translationMatches = [TranslationMatch]()
    @Published var text: String = ""
    
    private var cancellable: AnyCancellable? = nil
    
    init() {
        cancellable = $text
            .debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
            .removeDuplicates()
            .map { [self] queryText -> AnyPublisher<[TranslationMatch], Error> in
                if queryText.count < 3 {
                    return Future<[TranslationMatch], Error> { promise in
                        promise(.success([TranslationMatch]()))
                    }
                    .eraseToAnyPublisher()
                } else {
                    return DictionaryService.sharedInstance()
                        .searchMatchesPublisher(queryText, inLangSymbol: self.inLang, outLangSymbol: self.outLang)
                }
            }
            .switchToLatest()
            .eraseToAnyPublisher()
            .replaceError(with: [])
            .receive(on: DispatchQueue.main)
            .assign(to: \.translationMatches, on: self)
    }
}

【问题讨论】:

  • 是的.. 我没见过那个。明天将通过您书中的示例,但老实说,我在这里偶然发现了这个问题:swiftwithmajid.com/2020/04/22/catching-errors-in-combine(建议在flatMap 中捕获错误)。我尝试使用.replaceError(with: [])catch,但无法正确输入类型。我只是觉得我误解了发布的值/错误的类型在哪里改变以及如何处理。不过,感谢您的链接。
  • 我会在这里添加一个答案...
  • 正如您所说的那样,部分问题在于您无法正确获取类型(当然,来自编译器的错误消息并不是很有帮助)。但那是另一回事。学习在 Combine 中正确输入类型是一门艺术。我在我的在线教程中给出了一些开发管道的提示:apeth.com/UnderstandingCombine/tricksandtips.html
  • 请注意:显示解决方案的最佳 Stack Overflow 方式是将其作为答案,而不是将其包含在问题中.

标签: ios swift swiftui reactive-programming combine


【解决方案1】:

这是预期的行为。一旦错误被发布到管道中,管道就完成了。它可以作为失败完成,或者如果您使用replaceError,它可以作为单个最终值完成,但无论哪种方式,它都完成了。

我将使用flatMap而不是map加上switchToLatest来说明,但原理完全相同。

这里的解决方案是捕获或替换flatMap 闭包中的错误,防止它从flatMap 中渗出。这样,完成的是flatMap内部的二级管道,而不是整个外部管道。

我将用一个大大简化的示意图来说明你的情况。我有一个正在输入的文本字段,我的视图控制器是它的代表:

import UIKit
import Combine

enum Oops : Error { case oops }

class ViewController: UIViewController, UITextFieldDelegate {
    @IBOutlet weak var tf: UITextField!
    @Published var currentText = ""
    
    var pipeline : AnyCancellable!
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.pipeline = self.$currentText
            .debounce(for: 0.2, scheduler: DispatchQueue.main)
            .filter { $0.count > 3 }
            .flatMap { s -> AnyPublisher<String,Never> in
                Future<String,Error> { promise in
                    if Bool.random() {
                        promise(.success(s))
                    } else {
                        promise(.failure(Oops.oops))
                    }
                }
                .replaceError(with: "yoho")
                .eraseToAnyPublisher()
            }
            .sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
    }

    func textFieldDidChangeSelection(_ textField: UITextField) {
        self.currentText = textField.text ?? ""
    }

}

如您所见,我已将 .flatMap 中的 Future 配置为随机失败。但由于故障被替换.flatMap,该故障不会导致整个管道停止工作。因此,当您键入和退格等时,您有时会看到控制台中打印的文本字段文本,有时还会看到我用来指示错误的"yoho",但无论如何管道都会继续工作。

如果您想改用.map.switchToLatest,则代码完全相同。我在上面的代码中有flatMap,我们将改为:

        .map { s -> AnyPublisher<String, Never> in
            Future<String,Error> { promise in
                if Bool.random() {
                    promise(.success(s))
                } else {
                    promise(.failure(Oops.oops))
                }
            }
            .replaceError(with: "yoho")
            .eraseToAnyPublisher()
        }
        .switchToLatest()

【讨论】:

  • 酷!感谢您的帮助——我接受并赞成您的回答 :) 我让它工作了,通过使用 flatMap 而不是 map and switchToLatest 并使其产生 Never 类型的错误。我将包含工作代码作为帖子的编辑。
  • 其实我保留了mapswitchToLatest。我需要更多地了解Combine 中的不同运算符,因为它们会根据发布的值影响其他运算符在流中的应用。
  • 是的,正如我的教程所解释的,switchToLatestflatMap 是表亲:他们都产生了一个管道并设置它。 apeth.com/UnderstandingCombine/operators/…
  • 但无论哪种方式,答案都是相同的:需要在内部管道中以某种方式捕获错误,无论是由flatMap还是map产生的,这样它就不会泄漏进入外部管道并终止它。
  • 添加了 mapswitchToLatest 的重写,所以你看到的答案是一样的。
【解决方案2】:

根据马特的回答,不会终止上游发布者的更新和工作代码:

init() {
    cancellable = $text
        .debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
        .filter { $0.count >= 3 }
        .removeDuplicates()
        .map { [self] queryText -> AnyPublisher<[TranslationMatch], Never> in
            DictionaryService.sharedInstance()
                .searchMatchesPublisher(queryText, inLangSymbol: self.inLang, outLangSymbol: self.outLang)
                .replaceError(with: [TranslationMatch]())
                .eraseToAnyPublisher()
        }
        .switchToLatest()
        .eraseToAnyPublisher()
        .receive(on: DispatchQueue.main)
        .assign(to: \.translationMatches, on: self)
}

【讨论】:

    猜你喜欢
    • 2021-02-03
    • 1970-01-01
    • 2019-12-03
    • 1970-01-01
    • 1970-01-01
    • 2018-08-30
    • 2020-10-13
    • 2014-12-16
    • 1970-01-01
    相关资源
    最近更新 更多