I'm using a PHCachingImageManager to request thumbnail for a series of images as the user scrolls up and down a gridview which displays those images. I'm using an async function as follows to support modern swift concurrency with async-await style.
class PhotoRequestManager {
// Properties from class
private var imageCachingManager = PHCachingImageManager()
func fetchImage(
asset: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode
) async throws -> UIImage? {
let options = PHImageRequestOptions()
options.deliveryMode = .opportunistic
options.resizeMode = .fast
options.isNetworkAccessAllowed = true
options.isSynchronous = true
return try await withCheckedThrowingContinuation { [weak self] continuation in
self?.imageCachingManager.requestImage(
for: asset, targetSize: targetSize, contentMode: contentMode, options: options
) { image, info in
/// When the hang occurs, this block never gets executed.
if let error = info?[PHImageErrorKey] as? Error {
continuation.resume(throwing: error)
return
}
continuation.resume(returning: image)
}
}
}
}
// From UI
struct MySwiftUIView: View {
@EnvironmentObject private var photosRequestManager: PhotoRequestManager
var body: some View {
ZStack {
// Content
}
.task { await requestImage() }
}
@State private var thumbnail: Image?
private func requestImage() async {
guard
let uiImage = try? await photosRequestManager.fetchImage(
asset: asset, targetSize: size)
else {
return
}
thumbnail = Image(uiImage: uiImage)
}
}
I have come across a weird issue where at times this function hangs without returning either a result or an error. There are no console errors or anything that is visible, just that the function never returns a value. When that happens, any subsequent requests to the same function hangs as well. If anyone has any idea on what could be happening here, greatly appreciate any help.
You must avoid performing slow, synchronous tasks from within the Swift concurrency cooperative thread pool. As WWDC 2021’s Swift concurrency: Behind the scenes says, “The [cooperative] thread pool will only spawn as many threads as there are CPU cores, thereby making sure not to overcommit the system.” So, if you ever block all the threads in the very constrained cooperative thread pool, you can find yourself quickly exhausting this pool, and new tasks will be unable to run, and it can even deadlock. You should set
isSynchronoustofalse(or don’t call this directly from the Swift concurrency cooperative thread pool).In WWDC 2022 video Visualize and optimize Swift concurrency, they outline this deadlock risk. As they say:
That having been said, while that video talks about what to do if you are stuck with a synchronous API, you have no such constraint here. You can set
isSynchronoustofalseand then we can stay within the realm of Swift concurrency without any problems.Note, as discussed below, with the asynchronous rendition, a
deliveryModeofopportunisticmeans that the closure might be called more than once. That means that you would either want to use adeliveryModeofhighQualityFormatorfastFormatto ensure the closure is called only once, or switch from a simple continuation to anAsyncStreamto handle the multiple images that might come in.FWIW, my original answer, below, was predicated on using the asynchronous rendition (setting
isSynchronoustofalse). But all of the caveats outlined below (about eliminating any paths of execution where you fail to satisfy the continuation) still apply.I am unfamiliar with any scenario where the
requestImagecompletion handler is not called. (If anything, you actually have the opposite problem, that using.opportunisticcan cause the completion handler to be called multiple times, which would be a problem for your simple continuation pattern. You really should use anAsyncStreamorAsyncThrowingStreamif dealing with.opportunisticdelivery mode. See the example in latter part of this answer.)You should confirm:
requestImageis getting called;On that last point, there are several paths of execution where the continuation is not resumed:
The
guard let-try?.Consider:
If there was an error thrown, the
try?will discard the error (with its meaningful diagnostic information), silently exit and never satisfy the continuation. I would suggest:try?withdo-try-catch;continuation.resume(throwing: …);nil, log that fact, and have the continuationresumewith your own custom error.Bottom line, never have an early exit within a
withCheckedThrowingContinuationthat fails toresumethe continuation.The optional chaining of
self.Consider:
In the unlikely case that
selfwasnil, therequestImagewill never be called, and the continuation will never be satisfied. Personally, I would recommend not using the[weak self]capture list in your continuation. If you want to capture anything, capture theimageCachingManager.Again, never have a path of execution within
withCheckedThrowingContinuationwhere we fail toresumea continuation.In short, I would advise looking at all paths of execution and ensure you have none that could prevent the continuation from being satisfied. But right now, there are several.