发布者发布操作进度和最终价值

时间:2020-07-29 09:35:36

标签: swift combine

鉴于我有一个提供以下功能的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"))
        }
    }
}

3 个答案:

答案 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()
    }
}