鉴于我有一个提供以下功能的SDK
class SDK {
static func upload(completion: @escaping (Result<String, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion(.success("my_value"))
}
}
}
我能够创建一个包装器,以使其使用更加实用
class CombineSDK {
func upload() -> AnyPublisher<String, Error> {
Future { promise in
SDK.upload { result in
switch result {
case .success(let key):
promise(.success(key))
case .failure(let error):
promise(.failure(error))
}
}
}.eraseToAnyPublisher()
}
}
现在,我想了解如果SDK上传方法还提供了如下所示的进度块,我的CombineSDK.upload方法应如何显示:
class SDK {
static func upload(progress: @escaping (Double) -> Void, completion: @escaping (Result<String, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
progress(0.5)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
progress(1)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion(.success("s3Key"))
}
}
}
答案 0 :(得分:2)
我们需要为您的发布商提供一种Output
类型,该类型必须代表进度或最终值。因此,我们应该使用enum
。由于Foundation框架已经定义了名为Progress
的类型,因此我们将其命名为Progressable
以避免名称冲突。我们也可以使其通用:
enum Progressable<Value> {
case progress(Double)
case value(Value)
}
现在,我们需要考虑发布者的行为方式。典型的发布商(例如URLSession.DataTaskPublisher
)在获得订阅之前不会执行任何操作,并且会为每个订阅重新开始工作。 retry
运算符仅在上游发布者的行为如此时起作用。
所以我们的发布者也应该这样做:
extension SDK {
static func uploadPublisher() -> UploadPublisher {
return UploadPublisher()
}
struct UploadPublisher: Publisher {
typealias Output = Progressable<String>
typealias Failure = Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
<#code#>
}
}
}
创建发布者(通过调用SDK.uploadPublisher()
)不会启动任何工作。我们将<#code#>
替换为代码以开始上传:
extension SDK {
static func uploadPublisher() -> UploadPublisher {
return UploadPublisher()
}
struct UploadPublisher: Publisher {
typealias Output = Progressable<String>
typealias Failure = Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subject = PassthroughSubject<Output, Failure>()
subject.receive(subscriber: subscriber)
upload(
progress: { subject.send(.progress($0)) },
completion: {
switch $0 {
case .success(let value):
subject.send(.value(value))
subject.send(completion: .finished)
case .failure(let error):
subject.send(completion: .failure(error))
}
}
)
}
}
}
请注意,在开始上载之前,我们先叫subject.receive(subscriber: subscriber)
。这个很重要!如果upload
在返回之前同步调用其回调之一怎么办?通过在调用上传之前将订阅者传递到主题,我们确保即使upload
同步调用其回调,订阅者也有机会得到通知。
答案 1 :(得分:1)
注意:开始编写与@robmayoff的答案在很大程度上相似的答案,但是使用
Deferred
,因此请在此处发布以保持完整性。
Swift Combine仅适用于值和错误-没有单独的进度类型。但是您可以将进度建模为输出的一部分,或者作为另一个答案中建议的元组建模,或者作为进度和结果都作为案例的自定义枚举,这将是我的首选方法。
class CombineSDK {
enum UploadProgress<T> {
case progress(Double)
case result(T)
}
func upload() -> AnyPublisher<UploadProgress<String>, Error> {
Deferred { () -> AnyPublisher<UploadProgress<String>, Error> in
let subject = PassthroughSubject<UploadProgress<String>, Error>()
SDK.upload(
progress: { subject.send(.progress($0)) },
completion: { r in
let _ = r.map(UploadProgress.result).publisher.subscribe(subject)
})
return subject.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
编辑
基于@robmayoff的评论,上述解决方案无法处理在返回subject.send
之前调用subject
的同步情况。
解决方案大致相同,但以防万一,确实引入了一些复杂的问题,必须捕获这些值。可以使用Record
完成,这将为subject
func upload() -> AnyPublisher<UploadProgress<String>, Error> {
Deferred { () -> AnyPublisher<UploadProgress<String>, Error> in
let subject = PassthroughSubject<UploadProgress<String>, Error>()
var recording = Record<UploadProgress<String>, Error>.Recording()
subject.sink(
receiveCompletion: { recording.receive(completion: $0) },
receiveValue: { recording.receive($0) })
SDK.upload(
progress: { subject.send(.progress($0)) },
completion: { r in
let _ = r.map(UploadProgress.result).publisher.subscribe(subject)
})
return Record(recording: recording).append(subject).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
答案 2 :(得分:0)
这是可行的方法
extension CombineSDK {
func upload() -> AnyPublisher<(Double, String?), Error> {
let publisher = PassthroughSubject<(Double, String?), Error>()
SDK.upload(progress: { value in
publisher.send((value, nil))
}, completion: { result in
switch result {
case .success(let key):
publisher.send((1.0, key))
publisher.send(completion: .finished)
case .failure(let error):
publisher.send(completion: .failure(error))
}
})
return publisher.eraseToAnyPublisher()
}
}