Skip to main content

Blink/Views/Helpers/PHAssetHStack.swift

1import SwiftUI
2import Photos
4struct AssetIdentifiersCollection: RandomAccessCollection, Equatable {
5 typealias Element = (Int, String)
6 typealias Index = Int
7
8 let assetIdentifiers: [String]
10 var startIndex: Int { 0 }
11 var endIndex: Int { assetIdentifiers.count }
13 subscript(position: Int) -> Element {
14 (position, assetIdentifiers[position])
15 }
18/// Creates an HStack of PHAssets that fills in right-to-left.
19///
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
22/// are visible:
23///
24/// [9] [8] [7] [6] [5] [4] [3] [2] [1] [0]
25/// ^^^^^^^^^^^
26///
27/// Then the lower-numbered items won't be rendered by SwiftUI until the
28/// users scrolls to bring them into view.
29///
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.
32///
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.
35///
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.
41///
42struct PHAssetHStack<Content: View>: View {
43 var subview: (String, Int) -> Content
44 var assetIdentifiers: [String]
46 init(
47 assetIdentifiers: [String],
48 @ViewBuilder subview: @escaping (String, Int) -> Content
49 ) {
50 print("--> creating PHAssetHStack")
51 self.subview = subview
52 self.assetIdentifiers = assetIdentifiers
53 }
55 var body: some View {
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.
62 //
63 // Note: an older implementation of this code had
64 //
65 // ```swift
66 // ForEach(
67 // Array(zip(self.collection.indices, self.collection)),
68 // id: \.1.localIdentifier
69 // )
70 // ```
71 //
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.
76 //
77 // Note: enumerated is okay
78 ForEach(AssetIdentifiersCollection(assetIdentifiers: self.assetIdentifiers), id: \.1) { index, localIdentifier in
79 subview(localIdentifier, index)
80 }
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.
85 //
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.
88 //
89 // See https://developer.apple.com/documentation/swiftui/view/scrollposition(initialanchor:)
90 //
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)
95 }.padding()
96 }
97 .flipsForRightToLeftLayoutDirection(true)
98 .environment(\.layoutDirection, .rightToLeft)
99 }