首页 > 解决方案 > 上传图像数据然后引用对象 Swift 组合模式?

问题描述

想要将图像数据上传到 S3 之类的东西,然后将引用对象写入数据库是一种常见的场景。我一直在学习 Apple 的 Combine 框架,并且一直试图想出一种模式来实现这一点。

假设我有一个对象,其中包含有关我的图像的信息和一个发布者来启动管道。

struct ObjectWithImage: Encodable {
    let id: UUID
    let name: String
    let imageData: Data
    let imageURL: String

    enum CodingKeys: CodingKey {
        case id
        case name
        case imageURL
    }
}

extension Publishers {
    static let uploadObjectQueue: PassthroughSubject<ObjectWithImage, Never> = PassthroughSubject<ObjectWithImage, Never>()
}

我还有一个使用 S3 SDK 或其他东西的旧图像上传器。对于示例,该定义保持简短和人为。

struct LegacyImageUpload {
    //https://heckj.github.io/swiftui-notes/#patterns-future
    public func upload(imageData: Data) -> Future<Bool, Error> {
        let future = Future<Bool, Error> { promise in
            self.upload(data: imageData) { (p_error) in
                guard let error = p_error else {
                    return promise(.success(true))
                }
                return promise(.failure(error))
            }
        }
        return future
    }

    private func upload(data: Data, completion: @escaping((_ error: Error?) -> Void)) {
        //Code not here to keep example short
        completion(nil)
    }
}

我有一个AppDelegate可以订阅发布者的结构。

struct ObjectUploadPipeline {

    var subscription: AnyCancellable

    init() {
        self.subscription = Publishers.uploadObjectQueue.tryMap({ (object) -> Future<Bool, Error> in
            let legacyImageUplaod = LegacyImageUpload()
            return legacyImageUplaod.upload(imageData: object.imageData)
        }).map({ (future) -> Bool in
            //How do I switch back to the origional object so that I can now upload the Encoded JSON or write to my DB?
            return true
        }).sink(receiveCompletion: { (pipelineCompletion) in
            switch pipelineCompletion {
            case .finished:
                break
            case .failure(_):
                break
            }
        }, receiveValue: { (endValue) in

        })
    }
}

然后最后把它们绑在一起。

struct ObjectGenerator {
    init(numberOfObjects: Int) {
        for item in 0..<numberOfObjects {
            let object = ObjectWithImage.init(id: UUID(), name: "Object \(item)", imageData: Data.init(), imageURL: "path/to/image")
            Publishers.uploadObjectQueue.send(object)
        }
    }
}

let uploadPipeline = ObjectUploadPipeline()
let objectGenerator = ObjectGenerator.init(numberOfObjects: 10)

如何保证图片先上传成功?如果成功,我如何让下一个操作员知道,ObjectWithImage以便我可以将其编码为数据,然后将其发送到我的云?最好使用内置的URLSession发布者?

我喜欢结合并看到力量,但在将所有概念串在一起以完成这条管道时遇到了麻烦。

标签: iosjsonswiftfile-uploadcombine

解决方案


Combine 的优点之一是它迫使我们思考当错误发生时我们想要做什么。我们可以忽略它们,但我们必须明确地忽略它们。在您的问题中,您还没有解决您想要对Future返回的失败执行的操作upload(imageData:)。在这种情况下,您还需要知道ObjectWithImage吗?

假设在成功的情况下,您想要输出ObjectWithImage(这样您就可以在数据库加载器的下游使用它),而在失败的情况下,您想要同时输出 theObjectWithImage和 an Error(这样您就可以显示一个神秘的错误消息) . 对于失败的情况,让我们创建一个结合了ObjectWithImage和 错误的类型:

extension ObjectWithImage {
    struct UploadError: Error {
        var object: ObjectWithImage
        var error: Error
    }
}

upload(imageData:)的方法OutputBool,但它只输出true。我们应该使用Void而不是Bool明确输出的具体值无关紧要:

struct LegacyImageUpload {
    public func uploadImageData(_ data: Data) -> Future<Void, Error> {
        return Future { promise in
            self.upload(data: data) { (error) in
                if let error = error { promise(.failure(error)) }
                else { promise(.success(())) }
            }
        }
    }

    private func upload(data: Data, completion: @escaping (_ error: Error?) -> Void) {
        fatalError("real code omitted")
    }
}

现在我们将扩展LegacyImageUpload一个ObjectWithImage直接获取并上传其图像的方法。成功后,它会输出对象,以便我们可以在下游使用它。出错时,它会输出一个UploadError,以便我们可以在下游正确处理错误:

extension LegacyImageUpload {
    public func upload(_ object: ObjectWithImage) -> AnyPublisher<ObjectWithImage, ObjectWithImage.UploadError> {
        return uploadImageData(object.imageData)
            .map { object }
            .mapError { ObjectWithImage.UploadError(object: object, error: $0) }
            .eraseToAnyPublisher()
    }
}

请注意,因为我们将上游输出类型从 更改BoolVoid,所以我们可以写.map { object }而不是.map { _ in object }

如果数据库加载器将 aObjectWithImage作为输入,并且具有相同Failure类型的UploadError.

假设我们使用DataTaskPublisher. 我们需要将 DataTaskPublisher.Failure(实际上URLError)映射回UploadError. 我们还希望将正常输出映射回输入对象。所以它可能看起来像这样:

struct DatabaseLoader {
    enum Errors: Error {
        case badStatusCode(Int)
    }

    private func request(for object: ObjectWithImage) throws -> URLRequest {
        let url = URL(string: "https://example.com/")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
            request.httpBody = try JSONEncoder().encode(object)
        return request
    }

    public func upload(_ object: ObjectWithImage) -> AnyPublisher<ObjectWithImage, ObjectWithImage.UploadError> {
        return Just(object)
            .tryMap(self.request(for:))
            .flatMap({
                URLSession.shared.dataTaskPublisher(for: $0)
                    .mapError { $0 as Error }
            })
            .map { $0.response as! HTTPURLResponse }
            .flatMap({
                return $0.statusCode == 200
                    ? Result<Void, Error>.Publisher(())
                    : Result<Void, Error>.Publisher(Errors.badStatusCode($0.statusCode))
            })
            .map { object }
            .mapError { ObjectWithImage.UploadError(object: object, error: $0) }
            .eraseToAnyPublisher()
    }
}

我们现在拥有实现完整管道所需的所有部分。但!如果任何一个对象无法上传,我们不想向订阅者发送失败消息!发出失败会结束订阅,但也许错误不是致命的,我们可以尝试上传更多对象。所以我们需要将故障转化为正常输出。我们将使用标准的 SwiftResult类型作为输出,如下所示:

let objectsToUpload = PassthroughSubject<ObjectWithImage, Never>()
let legacyUploader = LegacyImageUpload()
let databaseLoader = DatabaseLoader()

let uploadTicket = objectsToUpload
    .flatMap({
        legacyUploader.upload($0)
            .flatMap { databaseLoader.upload($0) }
            .map { Result<ObjectWithImage, ObjectWithImage.UploadError>.success($0) }
            .catch { Just(Result.failure($0)) }
    })
    .sink(receiveValue: {
        switch $0 {
        case .success(let object):
            // Update UI to tell user that object was uploaded successfully.
            break
        case .failure(let error):
            // Show a cryptic error message telling the user
            // that error.object wasn't uploaded because of error.error.
            break
        }
    })

推荐阅读