我想在 SwiftUI 中实现从 PhotosPicker 加载图像的超时,以防没有互联网连接并且图像在设备上尚未提供全分辨率。
为此,我使用了带有并行计时器任务的任务组,但不幸的是,
loadTransferable(type:)
似乎不处理取消。一旦计时器任务完成并且调用 group.cancelAll()
,图像加载任务就不会完成,因此根本不会进入 switch
块。
相反的方式确实有效:如果我选择设备上可用的图像,则计时器任务将被取消并且任务组完成。
我做错了什么吗?有解决方法或其他方法吗?
func loadImage(_ image: PhotosPickerItem, timeout milliseconds: Int) async throws -> UIImage {
var result: Result<UIImage, Error>!
await withTaskGroup(of: Result<UIImage, Error>.self) { group in
// Load the image.
group.addTask {
do {
if let imageData = try await image.loadTransferable(type: Data.self) {
if let image = UIImage(data: imageData) {
return .success(image)
}
}
return .failure(MyError.imageCouldNotBeLoaded(details: "Image transfer or decoding issues."))
} catch {
return .failure(MyError.imageCouldNotBeLoaded(details: "Unsupported image format."))
}
}
// Activate the timeout timer.
group.addTask {
do {
try await Task.sleep(nanoseconds: UInt64(milliseconds * 1_000_000))
return .failure(MyError.imageCouldNotBeLoaded(details: "Timeout occured."))
} catch is CancellationError {
return .failure(MyError.loadingCancelled)
} catch {
return .failure(error)
}
}
// Wait for the first result and stop all (other) tasks.
if let taskResult = await group.next() {
result = taskResult
group.cancelAll()
}
}
switch result {
case .success(let image):
return image
case .failure(let error):
throw error
case .none:
throw MyError.imageCouldNotBeLoaded(details: "Task not executed or cancelled.")
}
}
由于我怀疑访问变量存在数据争用问题
result
,因此此实现将简化您的代码并且不存在数据争用问题,请参见下文。
但是,如果
image.loadTransferable(type:)
不处理取消,这并不能解决您的问题。任务组需要等待所有子任务完成。
private func loadImage(
_ image: PhotosPickerItem,
timeout milliseconds: UInt64
) async throws -> UIImage {
try await withThrowingTaskGroup(
of: UIImage.self,
returning: UIImage.self
) { group in
// Load the image.
group.addTask {
let data = try await self.loadTransferable(photosPickerItem: image)
guard Task.isCancelled == false,
let imageData = data,
let image: UIImage = UIImage(data: imageData)
else {
throw MyError.imageCouldNotBeLoaded(
details: Task.isCancelled ? "cancelled"
: data == nil ? "content type not supported" : "image data encoding error"
)
}
return image
}
// Activate the timeout timer.
group.addTask {
try await Task.sleep(for: .milliseconds(milliseconds))
throw MyError.loadingCancelled
}
// If a child task throws an error and if it will be propagated from
// this method out of the body of this function, then all remaining
// child tasks in that group are implicitly canceled.
guard let image = try await group.next() else {
fatalError("can't happen") // `group.next()` either throws or it returns an image.
}
return image
}
}