ios - 上传图像数据然后引用对象 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
发布者?
我喜欢结合并看到力量,但在将所有概念串在一起以完成这条管道时遇到了麻烦。
解决方案
Combine 的优点之一是它迫使我们思考当错误发生时我们想要做什么。我们可以忽略它们,但我们必须明确地忽略它们。在您的问题中,您还没有解决您想要对Future
返回的失败执行的操作upload(imageData:)
。在这种情况下,您还需要知道ObjectWithImage
吗?
假设在成功的情况下,您想要输出ObjectWithImage
(这样您就可以在数据库加载器的下游使用它),而在失败的情况下,您想要同时输出 theObjectWithImage
和 an Error
(这样您就可以显示一个神秘的错误消息) . 对于失败的情况,让我们创建一个结合了ObjectWithImage
和 错误的类型:
extension ObjectWithImage {
struct UploadError: Error {
var object: ObjectWithImage
var error: Error
}
}
您upload(imageData:)
的方法Output
是Bool
,但它只输出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()
}
}
请注意,因为我们将上游输出类型从 更改Bool
为Void
,所以我们可以写.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
}
})
推荐阅读
- python - 遗传算法 - python中的有序交叉
- javascript - 反应 - 反应路由器 - 私有路由 - 无限循环
- ios - Swift 4 Alamofire 分段上传不起作用
- javascript - Amplify JS SDK 抛出 Uncaught TypeError: Cannot read property 'crypto' of undefined
- php - 如何选择第一个 n 个字符或通过拳头“<”?
- r - read.zoo(tmp, header = TRUE, sep = ",") 中的错误:索引在数据行中有错误条目:1 2
- json - copyToNotebook 返回未实现的 OData 功能
- javascript - Karate: passing dynamic parameter values via the command line not working
- java - 为什么我的 EntityManagerHelper 类返回以前的查询结果,我该如何解决?
- java - 如何使用 java、aws-lamda 在 s3 存储桶中以 .gz 格式压缩文件