本文是阅读 Combine: Asynchronous Programming with Swift 后的学习笔记,有需要的请点击链接购买。
Failure
类型为 Never
的 publisher 表示该 publisher 永远不会发出错误。Failure
类型为 Never
的 publisher 允许您集中精力使用 publisher 的值,同时确保 publisher 永远不会发出错误。
例如 Just
publisher:
Just("Hello")
Command-click 点进去查看定义你会发现,它的 Failure
类型为 Never
。
public typealias Failure = Never
Combine 提供了一些只能在不发出错误的 publisher 上使用的操作符。例如:
Just("Hello")
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
运行结果为:
Hello
在上面的例子中,我们使用 sink(receiveValue:)
,它是 sink
的其中一个重载方法,并且他只能在不发出错误的 publisher 上使用。Combine 非常智能,如果是会发出错误的 publisher,它会强制你使用另外一个带有 receiveCompletion
的操作符。
为了验证它,我们先来看一个操作符 setFailureType
。
可以还是用 setFailureType
来把一个不发出错误的 publisher 变成会发出错误的 publisher。
例如:
enum MyError: Error {
case ohNo
}
Just("Hello")
.setFailureType(to: MyError.self)
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(.ohNo):
print("Finished with Oh No!")
case .finished:
print("Finished successfully!")
}
},
receiveValue: { value in
print("Got value: \(value)")
})
运行结果:
Got value: Hello
Finished successfully!
例子中虽然使用 setFailureType
修改了 Failure
的类型,并且使用 sink
的带有 receiveCompletion
的重载方法来订阅 Just
。如果改为用另外一个重在方法 sink(receiveValue:)
,则会报错:
Referencing instance method 'sink(receiveValue:)' on 'Publisher' requires the types 'MyError' and 'Never' be equivalent
要求 Failure
为 Never
。
这个操作符也是仅适用于不会发出错误的 publisher,用于把 publisher 发出的值赋给某个对象的的某个属性。
例如:
class Person {
let id = UUID()
var name = "Unknown"
}
let person = Person()
print("1", person.name)
Just("Shai")
.handleEvents(
receiveCompletion: { _ in print("2", person.name) }
)
.assign(to: \.name, on: person)
运行结果:
1 Unknown
2 Shai
当希望在开发过程中保护自己并确认 publisher 无法以发出错误事件而结束时,assertNoFailure
操作符非常有用。它不会阻止上游发出错误事件。但是,如果它检测到一个错误,它将以 fatalError
的形式 crash。
例如:
Just("Hello")
.setFailureType(to: MyError.self)
.assertNoFailure()
.sink(receiveValue: { print("Got value: \($0) ")})
运行结果:
Got value: Hello
如果在 assertNoFailure
前面加上:
.tryMap { _ in throw MyError.ohNo }
那么,运行之后将会报错。
这个操作符在开发调试的时候非常有用。
在 Combine 中,所有 try
前缀的运算符在出现错误时的行为都相同。所以这里只以 tryMap
为例。
首先看以下例子:
let names = ["Scott", "Marin", "Shai", "Florent"].publisher
names
.map { $0.count }
.sink(
receiveCompletion: { print("Completed with \($0)") },
receiveValue: { print("Got value: \($0)") }
)
运行结果:
Got value: 5
Got value: 5
Got value: 4
Got value: 7
Completed with finished
上面代码中,使用 map
把字符串转换为他的长度。
接下来我们有一个需求:如果字符串的长度小于 5,则抛出错误。我们把 map
里面的代码修改如下:
enum NameError: Error {
case tooShort(String)
case unknown
}
.map { value -> Int in
let length = value.count
guard length >= 5 else {
throw NameError.tooShort(value)
}
return value.count
}
然而,我们看到一个编译错误:
Invalid conversion from throwing function of type '(_) throws -> Int' to non-throwing function type '(String) -> _'
因为 map
是一个不会跑出错误的方法,所以不能在方法里面抛出错误。这时我们的 tryMap
就派上用场了。
把 map
改为 tryMap
,完整代码为:
enum NameError: Error {
case tooShort(String)
case unknown
}
let names = ["Scott", "Marin", "Shai", "Florent"].publisher
names
.tryMap { value -> Int in
let length = value.count
guard length >= 5 else {
throw NameError.tooShort(value)
}
return value.count
}
.sink(
receiveCompletion: { print("Completed with \($0)") },
receiveValue: { print("Got value: \($0)") }
)
运行结果如下:
Got value: 5
Got value: 5
Completed with failure(__lldb_expr_23.NameError.tooShort("Shai"))
那么如果处理抛出的错误呢?继续往下看。
首先看一下例子:
// 定义错误类型
enum NameError: Error {
case tooShort(String)
case unknown
}
Just("Hello")
.setFailureType(to: NameError.self) // 把 publisher 的错误类型改为 NameError
.map { $0 + " World!" }
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Done!")
case .failure(.tooShort(let name)):
print("\(name) is too short!")
case .failure(.unknown):
print("An unknown name error occurred")
}
},
receiveValue: { print("Got value \($0)") }
)
运行结果为:
Got value Hello World!
Done!
在 receiveCompletion
闭包的 completion
参数上进行 Option-click,你会发现 Completion 的错误类型为 NameError
,而这就是我们想要的错误类型。
如果我们把 .map { $0 + " World!" }
改为 .tryMap { $0 + " World!" }
,并再次对 receiveCompletion
闭包的 completion
参数上进行 Option-click,你会发现 Completion 的错误类型变成了 Swift 的 Error
类型,即使我们没有在 tryMap
中抛出错误。并且 switch
也无法正常进行了。
造成这个现象的原因是 Swift 并不支持抛出指定的错误类型,也就意味着,如果你使用 try 系列的操作符,抛出的错误类型都是 Swift 的 Error
类型。
这是我们可以使用 mapError
包通用的 Error
类型转换成我们自定义的错误类型。本例中在 tryMap
后面加上:
.mapError { $0 as? NameError ?? .unknown }
mapError
把 Error
转换成了 NameError
类型。
假设有一个 PhotoService
类,可以用于获取图片。
我们通过下面的例子来演示如何使用 catch
和 retry
:
var subscriptions = Set<AnyCancellable>()
let photoService = PhotoService()
photoService
.fetchPhoto(quality: .high) // 获取高质量的图片
.retry(3) // 如果获取失败,则重试三次
.catch { error -> PhotoService.Publisher in
print("Failed fetching high quality, falling back to low quality")
return photoService.fetchPhoto(quality: .low) // 获取低质量的图片
}
.replaceError(with: UIImage(named: "na.jpg")!)
.sink(
receiveCompletion: { print("\($0)") },
receiveValue: { image in
image
print("Got image: \(image)")
}
)
.store(in: &subscriptions) // 异步的订阅需要保存起来
解释代码逻辑:
- 首先通过
fetchPhoto(quality: .high)
获取高质量的图片。 - 如果获取失败,则通过
retry(3)
重试 3 次。 - 如果重试三次后还是失败,则
catch
会捕获到错误,然后通过photoService.fetchPhoto(quality: .low)
去获取低质量的图片。 - 如果获取低质量的图片还是失败,
replaceError
就会把错误替换成一个默认的图片,相当于我们在给UIImageView
设置图片时,通常会设置一个 placeholder 图片。 - 最后通过
sink
订阅。