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 {