import Photos import Dispatch import SDWebImage import ExpoModulesCore /** A custom loader for assets from the Photo Library. It handles all urls with the `ph` scheme. */ final class PhotoLibraryAssetLoader: NSObject, SDImageLoader { // MARK: - SDImageLoader func canRequestImage(for url: URL?) -> Bool { return isPhotoLibraryAssetUrl(url) } func requestImage( with url: URL?, options: SDWebImageOptions = [], context: SDWebImageContext?, progress progressBlock: SDImageLoaderProgressBlock?, completed completedBlock: SDImageLoaderCompletedBlock? = nil ) -> SDWebImageOperation? { guard isPhotoLibraryStatusAuthorized() else { let error = makeNSError(description: "Unauthorized access to the Photo Library") completedBlock?(nil, nil, error, false) return nil } let operation = PhotoLibraryAssetLoaderOperation() DispatchQueue.global(qos: .userInitiated).async { guard let url = url, let assetLocalIdentifier = assetLocalIdentifier(fromUrl: url) else { let error = makeNSError(description: "Unable to obtain the asset identifier from the url: '\(String(describing: url?.absoluteString))'") completedBlock?(nil, nil, error, false) return } guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetLocalIdentifier], options: .none).firstObject else { let error = makeNSError(description: "Asset with identifier '\(assetLocalIdentifier)' not found in the Photo Library") completedBlock?(nil, nil, error, false) return } operation.requestId = requestAsset( asset, url: url, context: context, progressBlock: progressBlock, completedBlock: completedBlock ) } return operation } func shouldBlockFailedURL(with url: URL, error: Error) -> Bool { // The lack of permission is one of the reasons of failed request, // but in that single case we don't want to blacklist the url as // the permission might be granted later and then the retry should be possible. return isPhotoLibraryStatusAuthorized() } } /** Returns a bool value whether the given url references the Photo Library asset. */ internal func isPhotoLibraryAssetUrl(_ url: URL?) -> Bool { return url?.scheme == "ph" } /** Returns the local identifier of the asset from the given `ph://` url. These urls have the form of "ph://26687849-33F9-4402-8EC0-A622CD011D70", where the asset local identifier is used as the host part. */ private func assetLocalIdentifier(fromUrl url: URL) -> String? { return url.host } /** Checks whether the app is authorized to read the Photo Library. */ private func isPhotoLibraryStatusAuthorized() -> Bool { let status = PHPhotoLibrary.authorizationStatus(for: .readWrite) return status == .authorized || status == .limited } /** Requests the image of the given asset object and returns the request identifier. */ private func requestAsset( _ asset: PHAsset, url: URL, context: SDWebImageContext?, progressBlock: SDImageLoaderProgressBlock?, completedBlock: SDImageLoaderCompletedBlock? ) -> PHImageRequestID { let options = PHImageRequestOptions() options.isSynchronous = false options.version = .current options.deliveryMode = .highQualityFormat options.resizeMode = .fast options.normalizedCropRect = .zero options.isNetworkAccessAllowed = true if let progressBlock = progressBlock { options.progressHandler = { progress, _, _, _ in // The `progress` is a double from 0.0 to 1.0, but the loader needs integers so we map it to 0...100 range let progressPercentage = Int((progress * 100.0).rounded()) progressBlock(progressPercentage, 100, url) } } var targetSize = PHImageManagerMaximumSize // We compute the minimal size required to display the image to avoid having to downsample it later if let scale = context?[ImageView.screenScaleKey] as? Double, let containerSize = context?[ImageView.frameSizeKey] as? CGSize, let contentFit = context?[ImageView.contentFitKey] as? ContentFit { targetSize = idealSize( contentPixelSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight), containerSize: containerSize, scale: scale, contentFit: contentFit ).rounded(.up) * scale } return PHImageManager.default().requestImage( for: asset, targetSize: targetSize, contentMode: .aspectFit, options: options, resultHandler: { image, info in // This value can be `true` only when network access is allowed and the photo is stored in the iCloud. let isDegraded: Bool = info?[PHImageResultIsDegradedKey] as? Bool ?? false completedBlock?(image, nil, nil, !isDegraded) } ) } /** Loader operation specialized for the Photo Library by keeping the request identifier. */ private class PhotoLibraryAssetLoaderOperation: NSObject, SDWebImageOperation { var canceled: Bool = false var requestId: PHImageRequestID? // MARK: - SDWebImageOperation func cancel() { if let requestId = requestId { PHImageManager.default().cancelImageRequest(requestId) } canceled = true } }