Catching errors in Combine

栏目: IT技术 · 发布时间: 5年前

内容简介:The Combine framework provides a declarativeWe use publishers to emit values over time. In the end, the publisher can finish its work by sending the completion event or fail with an error. Neither after completion nor after failure, the publisher can not e

The Combine framework provides a declarative Swift API for processing values over time. It is another excellent framework that released side-by-side with SwiftUI . I already covered it multiple times on my blog, but today I want to talk about one of the key aspects of data processing. Today we will learn how to handle errors during data processing using the Combine framework.

The lifecycle of publisher

We use publishers to emit values over time. In the end, the publisher can finish its work by sending the completion event or fail with an error. Neither after completion nor after failure, the publisher can not emit new values. Let’s take a look at the typical use-case that we might have in our apps.

import SwiftUI

struct SearchView: View {
    @ObservedObject var store: SearchStore

    var body: some View {
        NavigationView {
            List {
                TextField("type something...", text: $store.query)
                ForEach(store.repos, id: \.id) { repo in
                    Text(repo.name)
                }
            }.navigationBarTitle("Search")
        }
    }
}

In the example above, we have a view that allows users to enter a keyword and the list that renders search results. We use a store object to bind a query and repos array that holds search results. The main goal of the store object is fetching repos matching the query using Github API.

final class SearchStore: ObservableObject {
    @Published var query: String = ""
    @Published private(set) var repos: [Repo] = []
    private var cancellable: AnyCancellable?

    init(service: GithubService) {
        cancellable = $query
            .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
            .setFailureType(to: Error.self)
            .flatMap { service.searchPublisher(matching: $0) }
            .subscribe(on: DispatchQueue.global())
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { completion in
                    switch completion {
                    case .failure(let error): print("Error \(error)")
                    case .finished: print("Publisher is finished")
                    }
            },
                receiveValue: { [weak self] in self?.repos = $0}
        )
    }
}

I don’t want to produce too many requests as user types a query. That’s why I debounce it for 500ms . It means the publisher will wait at least 500ms whenever the user stops typing and then publish a value. Then I can use a flatMap operator to run a search request using a query. In the end, I use the sink subscriber to assign search results to a store variable. As soon as published variables change, SwiftUI will update the view to respect new data.

To learn more about store concept, take a look at my “Modeling app state using Store objects in SwiftUI” post.

We have one problem here, whenever the publisher fails with an error, nothing will happen in the view. Sink subscriber will just print the message in the console.

Replace error with the value

Publishers provide us a few ways to handle errors in the chain. Let’s start with the easiest one. Every publisher that can fail allows us to replace the error with some default value using replaceError operator. Let’s take a look at how we can use it.

init(service: GithubService) {
    cancellable = $query
        .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
        .setFailureType(to: Error.self)
        .flatMap { service.searchPublisher(matching: $0) }
        .replaceError(with: [])
        .subscribe(on: DispatchQueue.global())
        .receive(on: DispatchQueue.main)
        .sink(receiveValue: { [weak self] in self?.repos = $0})
}

As you can see, I have inserted replaceError operator with an empty array. Publisher will replace any error that might occur with an empty array and then terminate. The downside here is that the publisher completes its work after an error. It means our binding will not process new values. The user will type further queries, but nothing will happen. Let’s see how we can fix that.

init(service: GithubService) {
    cancellable = $query
        .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
        .flatMap { service.searchPublisher(matching: $0).replaceError(with: []) }
        .subscribe(on: DispatchQueue.global())
        .receive(on: DispatchQueue.main)
        .sink(receiveValue: { [weak self] in self?.repos = $0})
}

To keep your publisher alive, you have to handle all errors outside of the main chain. We can fix it by moving replaceError operator inside the flatMap . As you can see, the publisher inside the flatMap replaces an error with a value and then terminate its work. The main chain is still alive and emits new values.

Retry operator

Another useful operator that can help to solve an issue during value processing is retry . Retry operator tries to process the value again and again as many times as you specify.

init(service: GithubService) {
    cancellable = $query
        .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
        .flatMap {
            service.searchPublisher(matching: $0)
                .retry(3)
                .replaceError(with: [])
        }
        .subscribe(on: DispatchQueue.global())
        .receive(on: DispatchQueue.main)
        .sink(receiveValue: { [weak self] in self?.repos = $0})
}

As you can see in the example above, I ask the publisher to retry three times before replacing an error with an empty array.

Catch operator

Both retry and replace error operators are built on top of the catch operator. The catch operator allows us to replace a failed publisher with a new one. After that, the chain will use the new publisher to emit values.

init(service: GithubService) {
    cancellable = $query
        .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
        .flatMap {
            service
                .searchPublisher(matching: $0)
                .catch { error -> AnyPublisher<[Repo], Never> in
                    if error is URLError {
                        return Just([])
                            .eraseToAnyPublisher()
                    } else {
                        return Empty(completeImmediately: true)
                            .eraseToAnyPublisher()
                    }
            }
        }
        .subscribe(on: DispatchQueue.global())
        .receive(on: DispatchQueue.main)
        .sink(receiveValue: { [weak self] in self?.repos = $0})
}

Another great thing about the catch operator is the opportunity to access the error and return a new publisher after inspecting the error.

Conclusion

The Combine is the framework that I use in all of my apps. It has a high performance and friendly Swift API . Sometimes it is hard to debug errors in the Combine publisher chains. I hope this post gave you more information on the publisher’s lifecycle and explained to you how to catch the errors. Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading, and see you next week!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

你凭什么做好互联网

你凭什么做好互联网

曹政 / 中国友谊出版公司 / 2016-12 / 42.00元

为什么有人可以预见商机、超越景气,在不确定环境下表现更出色? 在规则之外,做好互联网,还有哪些关键秘诀? 当环境不给机会,你靠什么翻身? 本书为“互联网百晓生”曹政20多年互联网经验的总结,以严谨的逻辑思维分析个人与企业在互联网发展中的一些错误思想及做法,并给出正确解法。 从技术到商业如何实现,每个发展阶段需要匹配哪些能力、分解哪些目标、落实哪些策略都一一点出,并在......一起来看看 《你凭什么做好互联网》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具