Swift Combine 框架学习系列二:错误处理

上一篇讲解了 Combine 中的两大概念:Publisher 和 Subscriber 以及其基本使用。这一篇主要讲解一下,当 Combine 流中发生错误,我们应该怎么处理。

错误主要是分为两大类,一种是错误类型不匹配,一种则是上下游操作发生异常。

错误类型不匹配

假设,我们要实现一个加载网络图片的一个需求。

首先,定义一个 RequestError 的枚举,用来表示请求过程中发生的错误:

vbnet 复制代码
enum RequestError: Error {
    case sessionError(error: Error)
}

接着,声明两个实例对象:

swift 复制代码
private let imageURLPublisher = PassthroughSubject<URL, RequestError>() // 图片加载请求发布者
private var cancel: AnyCancellable?  

然后,将数据与接受者进行绑定:

scss 复制代码
cancel = imageURLPublisher.flatMap { requestURL in
    return URLSession.shared.dataTaskPublisher(for: requestURL)
}.sink { error in
    print(error)
} receiveValue: { result in
    let image = UIImage(data: result.data)
    print(image)
}

但此时你会发现编译报错: 原因在于 DataTaskPublisher 的错误类型为 URLError, 与 RequestError 类型是不一致。 DataTaskPublisher 源码:

swift 复制代码
public struct DataTaskPublisher : Publisher, Sendable {
    public typealias Output = (data: Data, response: URLResponse)
    public typealias Failure = URLError
    public let request: URLRequest
    public let session: URLSession
    public init(request: URLRequest, session: URLSession)

    public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == URLError, S.Input == (data: Data, response: URLResponse)
}

我们用 mapError 将错误类型进行一下转换就可以了:

go 复制代码
cancel = imageURLPublisher.flatMap { requestURL in
    return URLSession.shared.dataTaskPublisher(for: requestURL)
        .mapError { error -> RequestError in
            return RequestError.sessionError(error: error)
        }
}.sink { error in
    print(error)
} receiveValue: { result in
    let image = UIImage(data: result.data)
    print(image)
}

最后,就是在 touchesBegan 中触发图片加载:

swift 复制代码
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    imageURLPublisher.send(URL(string: "https://httpbin.org/image/jpeg")!)
    
    // RequestError.sessionError(error: Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made."
    imageURLPublisher.send(URL(string: "https://unknown.url/image")!) // 这个会走 Error 的分支,打印上述错误信息
}

运行一下项目,点击一下屏幕就可以看到控制台对 image 的打印了。

上下游操作发生异常

当操作流发生异常的时候,比如:网络加载失败、JSON 字符串解析失败等。我们可以通过下述的几个函数来进行错误处理。

assertNoFailure(_:file:line:)

当上游发布者发生错误时,抛出异常。主要用在测试版本中,用来提前发现某些场景下的错误。

代码示例:

swift 复制代码
public enum SubjectError: Error {
    case genericSubjectError
}

let subject = PassthroughSubject<String, Error>()
cancel = subject
    .assertNoFailure()
    .sink(receiveCompletion: { print ("completion: \($0)") },
          receiveValue: { print ("value: \($0).") }
    )

subject.send("数据")
subject.send(completion: Subscribers.Completion<Error>.failure(SubjectError.genericSubjectError))

当 subject 第一次调用 send 会发送一个 "数据" 的字符串,第二次调用则是发送了一个 SubjectError 类型的错误。

因为 subject 调用了 assertNoFailure() 函数,所以,当第二次调用 send 的时候会直接抛出异常。

catch 和 replaceError

当在 Combine 流中发生错误时,如果你想捕获错误并且忽略它,可以使用 catch 操作符。该操作符可以在发生错误的时候,允许你返回一个默认值。比如下面这两种场景:

  • 当搜索结果发生错误时,返回一个空数组。
  • 当加载网络图片失败时,返回一个占位图。 以下是加载网络图片失败的示例代码:
kotlin 复制代码
cancel = imageURLPublisher.flatMap { requestURL in
    ...与上文一致
}.map({ result -> UIImage? in
    return UIImage(data: result.data)
}).catch({ error -> Just<UIImage?> in
    return Just(notFoundImage)
}).sink(receiveValue: { image in
    print(image)
})

当 Combine 发生错误的时候,也可以使用 replaceError(with:) 来进行处理。它的作用是将错误替换成提供的值。当你想要通过发送单个替换元素来处理错误并结束流时是非常有用的。

它和 catch 的区别就是:完全忽略错误,不会去关心错误信息。而 catch 我们是可以根据错误信息来进行逻辑处理的。它的使用就是将上述的 catch 替换为 .replaceError(with: notFoundImage) 即可。

相关推荐
Keya19 小时前
lipo 命令行指南
ios·xcode·swift
zhangmeng19 小时前
SwiftUI中如何实现子视图向父视图传递数据?
ios·swiftui·swift
Saafo19 小时前
迁移至 Swift Actors
ios·swift
杂雾无尘2 天前
告别构建错误, iOS 开发架构难题全面解析, 避免 CPU 架构陷阱
ios·swift·客户端
大熊猫侯佩2 天前
探秘 WWDC 25 全新 #Playground 宏:提升 Swift 开发效率的超级神器
xcode·swift·wwdc
移动端小伙伴3 天前
10.推送的扩展能力 — 打造安全的通知体验
swift
移动端小伙伴3 天前
推送的扩展能力 — 打造个性化的通知体验
swift
移动端小伙伴3 天前
远程推送(Remote Push Notification)
swift
移动端小伙伴3 天前
本地通知的精准控制三角:时间、位置、情境
swift
移动端小伙伴3 天前
本地通知内容深度解析 — 打造丰富的通知体验
swift