Skip to main content

Create a generic version of the PHAssetHStack

ID
da3a549
date
2023-06-09 20:03:16+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
d1df9b9
message
Create a generic version of the PHAssetHStack
changed files
5 files, 120 additions, 13 deletions

Changed files

BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj (28240) → BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj (29124)

diff --git a/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj b/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
index c7f3712..339bed3 100644
--- a/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
+++ b/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
@@ -9,6 +9,8 @@
 /* Begin PBXBuildFile section */
 		940331732A336B5100200C5D /* DeferredRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940331722A336B5100200C5D /* DeferredRendering.swift */; };
 		94C5FFF22A33ADD4004ADDF5 /* PHFetchResultCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C5FFF12A33ADD4004ADDF5 /* PHFetchResultCollection.swift */; };
+		94C5FFF42A33B09B004ADDF5 /* SwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C5FFF32A33B09B004ADDF5 /* SwiftUIView.swift */; };
+		94C5FFF62A33B698004ADDF5 /* PHAssetHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C5FFF52A33B698004ADDF5 /* PHAssetHStack.swift */; };
 		94D2C8B92A320E6F00BEE15B /* ReviewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D2C8B82A320E6F00BEE15B /* ReviewState.swift */; };
 		94D2C8BD2A32796500BEE15B /* AlbumHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D2C8BC2A32796500BEE15B /* AlbumHelpers.swift */; };
 		94D2C8BF2A3299BD00BEE15B /* PhotosLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D2C8BE2A3299BD00BEE15B /* PhotosLibrary.swift */; };
@@ -48,6 +50,8 @@
 /* Begin PBXFileReference section */
 		940331722A336B5100200C5D /* DeferredRendering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredRendering.swift; sourceTree = "<group>"; };
 		94C5FFF12A33ADD4004ADDF5 /* PHFetchResultCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHFetchResultCollection.swift; sourceTree = "<group>"; };
+		94C5FFF32A33B09B004ADDF5 /* SwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIView.swift; sourceTree = "<group>"; };
+		94C5FFF52A33B698004ADDF5 /* PHAssetHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHAssetHStack.swift; sourceTree = "<group>"; };
 		94D2C8B82A320E6F00BEE15B /* ReviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewState.swift; sourceTree = "<group>"; };
 		94D2C8BC2A32796500BEE15B /* AlbumHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumHelpers.swift; sourceTree = "<group>"; };
 		94D2C8BE2A3299BD00BEE15B /* PhotosLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosLibrary.swift; sourceTree = "<group>"; };
@@ -101,6 +105,7 @@
 			children = (
 				940331722A336B5100200C5D /* DeferredRendering.swift */,
 				94C5FFF12A33ADD4004ADDF5 /* PHFetchResultCollection.swift */,
+				94C5FFF52A33B698004ADDF5 /* PHAssetHStack.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -182,6 +187,7 @@
 				94D751212A31BD8E005859E7 /* PhotoReviewer.swift */,
 				94D7512F2A31DC4A005859E7 /* ThumbnailList.swift */,
 				94D2C8C02A32FCE300BEE15B /* PHAssetImage.swift */,
+				94C5FFF32A33B09B004ADDF5 /* SwiftUIView.swift */,
 				94F7E39D2A331A9E00763DB9 /* Statistics.swift */,
 			);
 			path = Views;
@@ -331,6 +337,7 @@
 				94D7512B2A31D6AC005859E7 /* AssetHelpers.swift in Sources */,
 				94D2C8BD2A32796500BEE15B /* AlbumHelpers.swift in Sources */,
 				94D2C8BF2A3299BD00BEE15B /* PhotosLibrary.swift in Sources */,
+				94C5FFF62A33B698004ADDF5 /* PHAssetHStack.swift in Sources */,
 				94D7511E2A31B243005859E7 /* FullSizeImage.swift in Sources */,
 				94C5FFF22A33ADD4004ADDF5 /* PHFetchResultCollection.swift in Sources */,
 				94D750F02A31A796005859E7 /* BlinkReviewerApp.swift in Sources */,
@@ -341,6 +348,7 @@
 				94D2C8B92A320E6F00BEE15B /* ReviewState.swift in Sources */,
 				94F7E39E2A331A9E00763DB9 /* Statistics.swift in Sources */,
 				94D2C8C12A32FCE300BEE15B /* PHAssetImage.swift in Sources */,
+				94C5FFF42A33B09B004ADDF5 /* SwiftUIView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift (362) → BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift (3863)

diff --git a/BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift b/BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift
index 1457399..1896a68 100644
--- a/BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift
@@ -1,20 +1,92 @@
-//
-//  PHAssetHStack.swift
-//  BlinkReviewer
-//
-//  Created by Alex Chan on 09/06/2023.
-//
-
 import SwiftUI
+import Photos
 
-struct PHAssetHStack: View {
+/// Creates an HStack of PHAssets that scrolls right-to-left.
+///
+/// This provides lazy loading to the left-hand side, and assumes you're
+/// going to start scrolled to the far right, e.g. if the last three items
+/// are visible:
+///
+///         [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
+///                                     ^^^^^^^^^^^
+///
+/// Then the lower-numbered items won't be rendered by SwiftUI until the
+/// users scrolls to bring them into view.
+///
+/// This is similar to the behaviour of a LazyHStack, but if you scroll a
+/// LazyHStack to the far right, it loads every element immediately.
+///
+/// This takes a subview which is used to render the individual entries;
+/// these subviews receive the original PHAsset and the index (counting
+/// from the left, 0-indexed).
+///
+struct PHAssetHStack<Content: View>: View {
+    var subview: (PHAsset, Int) -> Content
+    var fetchResult: PHFetchResult<PHAsset>
+    
+    init(
+        _ fetchResult: PHFetchResult<PHAsset>,
+        @ViewBuilder subview: @escaping (PHAsset, Int) -> Content
+    ) {
+        self.subview = subview
+        self.fetchResult = fetchResult
+    }
+    
     var body: some View {
-        Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
+        ScrollView(.horizontal) {
+            LazyHStack(spacing: 5) {
+                // TODO: placeholder images for start/end
+                
+                // Implementation note: we use the localIdentifier rather than the
+                // array index as the id here, because the app gets way slower if
+                // you use the array index -- it tries to regenerate a bunch of
+                // the thumbnails every time you change position.
+                //
+                // However, we do want to expose the index to the callers -- I think?
+                //
+                // TODO: Investigate whether we can do this entirely using the
+                // localIdentiifer, and skip the index entirely.
+                ForEach(
+                    Array(
+                        zip(PHFetchResultCollection(fetchResult).indices, PHFetchResultCollection(fetchResult))
+                    ),
+                    id: \.1.localIdentifier
+                ) { index, asset in
+                    subview(asset, fetchResult.count - index - 1)
+                }
+                
+                // Note: these two uses of RTL direction are a way to get the LazyHStack
+                // to start on the right-hand side (i.e. the newest image) without loading
+                // everything else in the view.
+                //
+                // I suspect this may get easier with the new scrollPosition API, coming
+                // in the 2023 OS releases.  TODO: Investigate this new API when available.
+                //
+                // See https://developer.apple.com/documentation/swiftui/view/scrollposition(initialanchor:)
+                //
+                // The current implementation comes from a suggestion in a Stack Overflow
+                // answer by Maciek Czarnik: https://stackoverflow.com/a/64195239/1558022
+                    .flipsForRightToLeftLayoutDirection(true)
+                    .environment(\.layoutDirection, .rightToLeft)
+                }.padding()
+        }
+            .flipsForRightToLeftLayoutDirection(true)
+            .environment(\.layoutDirection, .rightToLeft)
     }
 }
 
 struct PHAssetHStack_Previews: PreviewProvider {
+    static var fetchResult: PHFetchResult<PHAsset> {
+        let options = PHFetchOptions()
+        options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
+        options.fetchLimit = 25
+        
+        return PHAsset.fetchAssets(with: options)
+    }
+    
     static var previews: some View {
-        PHAssetHStack()
+        PHAssetHStack(fetchResult) { asset, index in
+            Text("Asset \(index):\n\(asset.creationDate?.ISO8601Format() ?? "(unknown)")")
+        }
     }
 }

BlinkReviewer/BlinkReviewer/Views/Helpers/PHFetchResultCollection.swift (1485) → BlinkReviewer/BlinkReviewer/Views/Helpers/PHFetchResultCollection.swift (1769)

diff --git a/BlinkReviewer/BlinkReviewer/Views/Helpers/PHFetchResultCollection.swift b/BlinkReviewer/BlinkReviewer/Views/Helpers/PHFetchResultCollection.swift
index 7f0f924..b1be7a8 100644
--- a/BlinkReviewer/BlinkReviewer/Views/Helpers/PHFetchResultCollection.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/Helpers/PHFetchResultCollection.swift
@@ -25,6 +25,10 @@ struct PHFetchResultCollection: RandomAccessCollection, Equatable {
 
     let fetchResult: PHFetchResult<PHAsset>
 
+    init(_ fetchResult: PHFetchResult<PHAsset>) {
+        self.fetchResult = fetchResult
+    }
+    
     var startIndex: Int { 0 }
     var endIndex: Int { fetchResult.count }
 
@@ -36,17 +40,20 @@ struct PHFetchResultCollection: RandomAccessCollection, Equatable {
 struct PHFetchResultCollection_Previews: PreviewProvider {
     static var resultCollection: PHFetchResultCollection {
         let options = PHFetchOptions()
+        options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
         options.fetchLimit = 3
         
         let fetchResult: PHFetchResult<PHAsset> = PHAsset.fetchAssets(with: options)
         
-        return PHFetchResultCollection(fetchResult: fetchResult)
+        return PHFetchResultCollection(fetchResult)
     }
     
     static var previews: some View {
         VStack {
+            Text("These dates should be in descending order:")
+            
             ForEach(self.resultCollection, id: \.localIdentifier) { asset in
-                Text("\(asset.localIdentifier)")
+                Text("\(asset.creationDate?.ISO8601Format() ?? "(unknown)")")
             }
         }
     }

BlinkReviewer/BlinkReviewer/Views/SwiftUIView.swift (0) → BlinkReviewer/BlinkReviewer/Views/SwiftUIView.swift (354)

diff --git a/BlinkReviewer/BlinkReviewer/Views/SwiftUIView.swift b/BlinkReviewer/BlinkReviewer/Views/SwiftUIView.swift
new file mode 100644
index 0000000..b224003
--- /dev/null
+++ b/BlinkReviewer/BlinkReviewer/Views/SwiftUIView.swift
@@ -0,0 +1,20 @@
+//
+//  SwiftUIView.swift
+//  BlinkReviewer
+//
+//  Created by Alex Chan on 09/06/2023.
+//
+
+import SwiftUI
+
+struct SwiftUIView: View {
+    var body: some View {
+        Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
+    }
+}
+
+struct SwiftUIView_Previews: PreviewProvider {
+    static var previews: some View {
+        SwiftUIView()
+    }
+}

BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift (3284) → BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift (3271)

diff --git a/BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift b/BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift
index 01e9944..0f4d9bd 100644
--- a/BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/ThumbnailList.swift
@@ -15,7 +15,7 @@ struct ThumbnailList: View {
     @Binding var selectedAssetIndex: Int
     
     private var assets: PHFetchResultCollection {
-        return PHFetchResultCollection(fetchResult: photosLibrary.assets2)
+        return PHFetchResultCollection(photosLibrary.assets2)
     }
     
     var body: some View {