SwiftUI + Combine - 发布者在第一次错误时终止
SwiftUI + Combine - Publisher terminates upon first error
我想在用户在搜索字段中键入 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)
}
}
这是预期的行为。一旦错误被发布到管道中,管道就完成了。可以作为失败完成,也可以作为单个最终值完成,如果你使用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 { [=10=].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([=10=]) }, receiveValue: { print([=10=]) })
}
func textFieldDidChangeSelection(_ textField: UITextField) {
self.currentText = textField.text ?? ""
}
}
如您所见,我已将 .flatMap
中的 Future 配置为 随机 失败。但是因为故障被替换在inside the .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()
根据马特的回答,更新后的工作代码不会终止上游发布者:
init() {
cancellable = $text
.debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
.filter { [=10=].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)
}
我想在用户在搜索字段中键入 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)
}
}
这是预期的行为。一旦错误被发布到管道中,管道就完成了。可以作为失败完成,也可以作为单个最终值完成,如果你使用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 { [=10=].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([=10=]) }, receiveValue: { print([=10=]) })
}
func textFieldDidChangeSelection(_ textField: UITextField) {
self.currentText = textField.text ?? ""
}
}
如您所见,我已将 .flatMap
中的 Future 配置为 随机 失败。但是因为故障被替换在inside the .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()
根据马特的回答,更新后的工作代码不会终止上游发布者:
init() {
cancellable = $text
.debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
.filter { [=10=].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)
}