4/// This view gets an NSImage for a PHAsset.
6/// When you get a photo from the Photos library, it may not be available
7/// immediately -- for example, if the image has to be downloaded from
8/// iCloud first. Downstream views can create an instance of this object,
9/// and then watch the `image` property -- this will be populated with the
10/// appropriate image as it loads.
12/// You can use this class in two ways:
14/// 1. Create a new instance for every PHAsset you want to render
15/// 2. Create a single instance and update the `asset` property; the `image`
16/// property will be updated shortly after
18/// Note: PhotoKit may return multiple versions of an image, e.g. a low-res
19/// version immediately and a high-res version later. You can inspect the
20/// `isDegraded` property -- this will tell you if Photos has returned a
21/// low quality image now and expects to return a higher quality image later.
22class PHAssetImage: NSObject, ObservableObject {
24 @Published var image = NSImage()
25 @Published var isDegraded = false
27 init(_ asset: PHAsset?, size: CGSize, deliveryMode: PHImageRequestOptionsDeliveryMode) {
29 self.deliveryMode = deliveryMode
30 self.imageCache = Dictionary()
37 private var _asset: PHAsset?
38 private var size: CGSize
39 private var deliveryMode: PHImageRequestOptionsDeliveryMode
41 // Often we'll be retrieving the same image repeatedly, as the user shuttles
42 // back and forth between a few images they're comparing. In this case, we
43 // don't want to go back to Photos every time -- so we keep a cache of images
44 // we've retrieved previously.
46 // In theory the `PHCachingImageManager` does this for us; in practice the app
47 // feels snappier to me with this additional cache.
49 // TODO: Replace this Dictionary with an LRU cache of some sort; this could
50 // allow the app's memory usage to balloon indefinitely.
51 private var imageCache: Dictionary<PHAsset, NSImage>
59 self._asset = newValue
60 self.isDegraded = true
65 private func regenerateImage() {
66 if let thisAsset = asset {
67 if let nsImage = imageCache[thisAsset] {
69 self.isDegraded = false
73 print("regenerating image for \(thisAsset.localIdentifier)")
75 // This implementation is based on code in a Stack Overflow answer
76 // by Francois Nadeau: https://stackoverflow.com/a/48755517/1558022
78 let options = PHImageRequestOptions()
80 options.isSynchronous = false
81 options.deliveryMode = deliveryMode
83 // If i don't set this value, then sometimes I get an error like
84 // this in the `info` variable:
86 // Error Domain=PHPhotosErrorDomain Code=3164 "(null)"
88 // This means that the asset is in the cloud, and by default Photos
89 // isn't allowed to download assets here. Apple's documentation
90 // suggests adding this option as the fix.
92 // See https://developer.apple.com/documentation/photokit/phphotoserror/phphotoserrornetworkaccessrequired
93 options.isNetworkAccessAllowed = true
95 PHCachingImageManager.default()
99 contentMode: .aspectFill,
101 resultHandler: { (result, info) -> Void in
102 if let isDegraded = info?[PHImageResultIsDegradedKey] as? Bool {
103 self.isDegraded = isDegraded
106 if let imageResult = result {
107// print("got image!")
108 self.image = imageResult
110 if !self.isDegraded {
111 self.imageCache[thisAsset] = imageResult
114 self.isDegraded = true
115 print("Error getting image: \(String(describing: info))")