Skip to main content

Get thumbnails working in a non-awful way

ID
322fe4b
date
2023-06-10 07:48:29+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
7f5afe6
message
Get thumbnails working in a non-awful way
changed files
6 files, 65 additions, 45 deletions

Changed files

BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift (5418) → BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift (6756)

diff --git a/BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift b/BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift
index 7d94dc2..59fffe6 100644
--- a/BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift
+++ b/BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift
@@ -123,7 +123,7 @@ class PhotosLibrary: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
         }
     }
     
-    func state(for asset: PHAsset) -> ReviewState? {
+    func state(of asset: PHAsset) -> ReviewState? {
         if self.rejectedAssets.contains(asset) {
             return .Rejected
         }
@@ -138,4 +138,37 @@ class PhotosLibrary: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
         
         return nil
     }
+    
+    // Implements a basic cache for thumbnail images.
+    //
+    // Thumbnail images are small and easily reused; I've put them here because
+    // we already pass this class around as a shared @EnvironmentObject.
+    //
+    // For some reason SwiftUI insists on trying to recreate all the thumbnail
+    // views when you step between images -- I think there's probably a way to
+    // have it cache the views rather than me doing it manually, but I'm not
+    // smart enough to debug that.  If I don't cache it, there's a "flash" as
+    // it reloads the thumbnails every time.
+    //
+    // TODO: Investigate using SwiftUI to do this.
+    // TODO: If that doesn't work, replace this Dictionary with NSCache or an
+    // LRU cache.  For some reason NSCache didn't store entries when I tried it,
+    // but I didn't try for very long.
+    private var thumbnailCache = Dictionary<PHAsset, PHAssetImage>()
+    
+    func getThumbnail(for asset: PHAsset) -> PHAssetImage {
+        if let cachedThumbnail = thumbnailCache[asset] {
+            return cachedThumbnail
+        }
+        
+        let newThumbnail = PHAssetImage(
+            asset,
+            size: CGSize(width: 70, height: 70),
+            deliveryMode: .fastFormat
+        )
+        
+        thumbnailCache[asset] = newThumbnail
+        
+        return newThumbnail
+    }
 }

BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift (10284) → BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift (10283)

diff --git a/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift b/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
index b5ea9b3..b2fe40e 100644
--- a/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
@@ -97,7 +97,7 @@ struct PhotoReviewer: View {
             
             case let e where e.characters == "1" || e.characters == "2" || e.characters == "3":
                 print("time to review!")
-                let state = photosLibrary.state(for: focusedAsset)
+                let state = photosLibrary.state(of: focusedAsset)
             
                 let approved = getAlbum(withName: "Approved")
                 let rejected = getAlbum(withName: "Rejected")

BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift (3016) → BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift (3015)

diff --git a/BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift b/BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift
index ca7830f..e56d6d0 100644
--- a/BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift
@@ -34,7 +34,7 @@ struct ThumbnailList: View {
                         //                Text("asset \(index)")
                         ThumbnailImage(
                             thumbnail: PHAssetImage(asset, size: CGSize(width: 70, height: 70), deliveryMode: .opportunistic),
-                            state: photosLibrary.state(for: asset),
+                            state: photosLibrary.state(of: asset),
                             isFavorite: asset.isFavorite,
                             isSelected: photosLibrary.assets2.count - 1 - index == selectedAssetIndex
                         ).onTapGesture {

BlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swift (841) → BlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swift (854)

diff --git a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swift b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swift
index 4bb1685..0adb2fe 100644
--- a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swift
@@ -5,14 +5,14 @@ import Photos
 ///
 /// This is meant to match the way favorite items are marked in Photos.
 struct FavoriteHeartIcon: ViewModifier {
-    let asset: PHAsset
+    let isFavorite: Bool
     
-    init(_ asset: PHAsset) {
-        self.asset = asset
+    init(_ isFavorite: Bool) {
+        self.isFavorite = isFavorite
     }
     
     func body(content: Content) -> some View {
-        if asset.isFavorite {
+        if isFavorite {
             content.overlay(alignment: Alignment(horizontal: .leading, vertical: .bottom)) {
                 Image(systemName: "heart.fill")
                     .foregroundColor(.white)
@@ -26,7 +26,7 @@ struct FavoriteHeartIcon: ViewModifier {
 }
 
 extension View {
-    func favoriteHeartIcon(for asset: PHAsset) -> some View {
-        modifier(FavoriteHeartIcon(asset))
+    func favoriteHeartIcon(_ isFavorite: Bool) -> some View {
+        modifier(FavoriteHeartIcon(isFavorite))
     }
 }

BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift (2012) → BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift (1827)

diff --git a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift
index d8d96f7..25a2503 100644
--- a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift
@@ -7,32 +7,30 @@ import Photos
 /// mean some information gets cropped out -- that's okay, these are only
 /// small previews, not complete images.
 struct NewThumbnailImage: View {
-    @EnvironmentObject var photosLibrary: PhotosLibrary
-    
-    var asset: PHAsset
+    @ObservedObject var assetImage: PHAssetImage
+    var state: ReviewState?
     var isFocused: Bool
+    var isFavorite: Bool
     
     private var size: CGFloat
     private var cornerRadius: CGFloat
-    
-    @ObservedObject var assetImage: PHAssetImage
-    
-    init(_ asset: PHAsset, isFocused: Bool) {
-        self.asset = asset
+
+    // Implementation note: the reason we pass in a bunch of individual
+    // properties rather than the whole asset is because we need an
+    // @EnvironmentObject (the PhotosLibrary) to create the PHAssetImage,
+    // so we can stick the latter in an @ObservedObject.
+    //
+    // But EnvironmentObject values aren't passed down until you call the
+    // `body` method, which is too late!  So instead we have the parent
+    // view call into PhotosLibrary and pass in the relevant values here.
+    init(_ assetImage: PHAssetImage, state: ReviewState?, isFavorite: Bool, isFocused: Bool) {
+        self.assetImage = assetImage
+        self.state = state
+        self.isFavorite = isFavorite
         self.isFocused = isFocused
         
         self.size = isFocused ? 70 : 50
         self.cornerRadius = isFocused ? 7 : 5
-        
-        self.assetImage = PHAssetImage(
-            asset,
-            size: CGSize(width: self.size, height: self.size),
-            deliveryMode: .fastFormat
-        )
-    }
-    
-    private var state: ReviewState? {
-        photosLibrary.state(for: asset)
     }
     
     var body: some View {
@@ -45,20 +43,6 @@ struct NewThumbnailImage: View {
             .reviewStateBorder(for: state, with: cornerRadius)
             .reviewStateIcon(for: state)
             .reviewStateColor(isRejected: state == .Rejected)
-            .favoriteHeartIcon(for: asset)
-    }
-}
-
-struct NewThumbnailImage_Previews: PreviewProvider {
-    static var asset: PHAsset = PHAsset.fetchAssets(with: nil).firstObject!
-    
-    static var previews: some View {
-        NewThumbnailImage(asset, isFocused: false)
-            .environmentObject(PhotosLibrary())
-            .previewDisplayName("thumbnail, not focused")
-        
-        NewThumbnailImage(asset, isFocused: true)
-            .environmentObject(PhotosLibrary())
-            .previewDisplayName("thumbnail, focused")
+            .favoriteHeartIcon(isFavorite)
     }
 }

BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailList.swift (539) → BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailList.swift (607)

diff --git a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailList.swift b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailList.swift
index c8990ad..bd777d0 100644
--- a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailList.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailList.swift
@@ -9,13 +9,16 @@ import SwiftUI
 
 struct NewThumbnailList: View {
     @EnvironmentObject var photosLibrary: PhotosLibrary
-    @EnvironmentObject var thumbnailManager: ThumbnailManager
     @Binding var focusedAssetIndex: Int
     
     var body: some View {
         PHAssetHStack(photosLibrary.assets2) { asset, index in
-            NewThumbnailImage(asset, isFocused: index == focusedAssetIndex)
-                .environmentObject(photosLibrary)
+            NewThumbnailImage(
+                photosLibrary.getThumbnail(for: asset),
+                state: photosLibrary.state(of: asset),
+                isFavorite: asset.isFavorite,
+                isFocused: index == focusedAssetIndex
+            )
         }
     }
 }