I'd like to implement a timeout for loading images from the PhotosPicker in SwiftUI in case there's no internet connection and the images are not available in full resolution on the device yet, for example.
I used a task group with a parallel timer task for this, but unfortunately it seems, that loadTransferable(type:)
does not handle cancellation. The image loading task does not finish once the timer task has completed and group.cancelAll()
was called and thus the switch
block is not entered at all.
The other way round does work: If I pick an image that is available on the device already the timer task is cancelled and the task group finishes.
Am I doing something wrong? Is there a workaround or other approach for this?
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.")
}
}
Since I suspect a data race issue accessing variable
result
, this implementation will simplify your code and does not have data race issues, see below.However, this does not fix your issue, if
image.loadTransferable(type:)
does not handle cancellation. A task group needs to wait until all child tasks have been completed.