When loading images from the photo library via PhotoKit, it can happen that the image is not downloaded from iCloud. In this case, a low-resolution "placeholder" version of that image is available locally if you browse it in the photos App:
However, when I try to get the same photo to display in an app, the photo that I get from PHAssetManager
will be at maximum 120
units wide (or high). This looks very blurry and is completely unusable except for a thumbnail.
I have already tried
PHImageManagerMaximumSize
as the target sizeresizeMode = .none
and .exact
deliveryMode
'srequestImageDataAndOrientation
privateFileURL
property on the assetI wanted to ask if anyone has come across this issue and maybe worked around it.
The behavior can be easily reproduced with some PhotoKit example app, for example from these course materials. This is the code I used to produce the below image:
extension UIImageView {
func fetchImageAsset(_ asset: PHAsset?, targetSize size: CGSize, contentMode: PHImageContentMode = .aspectFill, completionHandler: ((Bool) -> Void)?) {
let options = PHImageRequestOptions()
options.deliveryMode = .opportunistic
options.resizeMode = .none
options.isNetworkAccessAllowed = false
// 1
guard let asset = asset else {
completionHandler?(false)
return
}
// 2
let resultHandler: (UIImage?, [AnyHashable: Any]?) -> Void = { image, info in
if let image = image {
self.image = image
}
completionHandler?(true)
}
// 3
PHImageManager.default().requestImage(
for: asset,
targetSize: size,
contentMode: contentMode,
options: options,
resultHandler: resultHandler)
}
}
Counter-intuitively, PHImageManager will return higher-resolution images when the requested target size is below a certain threshold.
This gives us an ugly albeit functioning workaround that can be implemented as follows.
First, load the image as usual.
If the loading is completed (PHImageResultIsDegradedKey
is 0
) but PHImageResultIsInCloudKey
is 1
, load the image again with the targetSize
360x360
, which I have found a suitable value by experimentation. Here it is important to use the opportunistic
delivery mode and .none
for the resizeMode
. Other values for these settings do not work.
Here is an excerpt of my code. The continuation.yield
calls carry information about the process to the outside, async
world.
func imageRequestOptions(deliveryMode: PHImageRequestOptionsDeliveryMode,
progressHandler: PHAssetImageProgressHandler? = nil) -> PHImageRequestOptions {
let opts = PHImageRequestOptions()
opts.resizeMode = .none
opts.isSynchronous = false
opts.isNetworkAccessAllowed = networkAccessAllowed()
opts.deliveryMode = deliveryMode
opts.progressHandler = progressHandler
return opts
}
...
let targetSizeForOfflinePhotos = CGSize(width: 360, height: 360)
if metadata?[PHImageResultIsDegradedKey] as? Int == 0
&& metadata?[PHImageResultIsInCloudKey] as? Int == 1 {
continuation.yield(.internetAccessRequired)
let requestId = assetManager
.requestImage(
for: asset,
targetSize: targetSizeForOfflinePhotos,
contentMode: .aspectFill,
options:
imageRequestOptions(
deliveryMode: .opportunistic
)
) { image, metadata in
if let image = image {
continuation.yield(.imageAvailable(.uiImage(image)))
} else {
continuation.finish(throwing: PhotoLibraryError.assetNotFound)
}
}
continuation.onTermination = { _ in
Task {
assetManager.cancelImageRequest(requestId)
}
}
There might still be a correct way to implement image loading for offline scenarios. Unfortunately as of now, this hack is the only thing that I can find and it improves user experience by a vast margin in my case.
It seems that I am also not the only one having trouble with this: I downloaded multiple popular apps that deal with library photos, and they all suffer from the same issue I described in the question.