4struct AssetIdentifiersCollection: RandomAccessCollection, Equatable {
5 typealias Element = (Int, String)
8 let assetIdentifiers: [String]
10 var startIndex: Int { 0 }
11 var endIndex: Int { assetIdentifiers.count }
13 subscript(position: Int) -> Element {
14 (position, assetIdentifiers[position])
18/// Creates an HStack of PHAssets that fills in right-to-left.
20/// This provides lazy loading to the left-hand side, and assumes you're
21/// going to start scrolled to the far right, e.g. if the last three items
24/// [9] [8] [7] [6] [5] [4] [3] [2] [1] [0]
27/// Then the lower-numbered items won't be rendered by SwiftUI until the
28/// users scrolls to bring them into view.
30/// This is similar to the behaviour of a LazyHStack, but if you scroll a
31/// LazyHStack to the far right, it loads every element immediately.
33/// This takes a subview which is used to render the individual entries;
34/// these subviews receive the position and identifier of the original asset.
36/// Note: this operates on a list of asset identifiers, but not the assets
37/// themselves -- this is a performance optimisation. If the user scrolls
38/// deep into the list, SwiftUI will try to render lots of entries, and if
39/// those are PHAsset elements, it'll go back to the Photos database, even
40/// though we don't really need any Photos data in our views.
42struct PHAssetHStack<Content: View>: View {
43 var subview: (String, Int) -> Content
44 var assetIdentifiers: [String]
47 assetIdentifiers: [String],
48 @ViewBuilder subview: @escaping (String, Int) -> Content
50 print("--> creating PHAssetHStack")
51 self.subview = subview
52 self.assetIdentifiers = assetIdentifiers
56 ScrollView(.horizontal) {
57 LazyHStack(spacing: 7) {
58 // Implementation note: we use the localIdentifier rather than the
59 // array index as the id here, because the app gets way slower if
60 // you use the PHFetchResult index -- it tries to regenerate a bunch of
61 // the thumbnails every time you change position.
63 // Note: an older implementation of this code had
67 // Array(zip(self.collection.indices, self.collection)),
68 // id: \.1.localIdentifier
72 // For some reason this caused the app to slow to a crawl -- I think it was
73 // creating the entire Array, which is quite expensive. I switched the
74 // PHFetchResultCollection to vend a struct with both the asset and the
75 // position, but now it does it by random access -- this seems faster.
77 // Note: enumerated is okay
78 ForEach(AssetIdentifiersCollection(assetIdentifiers: self.assetIdentifiers), id: \.1) { index, localIdentifier in
79 subview(localIdentifier, index)
82 // Note: these two uses of RTL direction are a way to get the LazyHStack
83 // to start on the right-hand side (i.e. the newest image) without loading
84 // everything else in the view.
86 // I suspect this may get easier with the new scrollPosition API, coming
87 // in the 2023 OS releases. TODO: Investigate this new API when available.
89 // See https://developer.apple.com/documentation/swiftui/view/scrollposition(initialanchor:)
91 // The current implementation comes from a suggestion in a Stack Overflow
92 // answer by Maciek Czarnik: https://stackoverflow.com/a/64195239/1558022
93 .flipsForRightToLeftLayoutDirection(true)
94 .environment(\.layoutDirection, .rightToLeft)
97 .flipsForRightToLeftLayoutDirection(true)
98 .environment(\.layoutDirection, .rightToLeft)