Skip to content

Commit ff35886

Browse files
authored
Update CollectionView (#61)
1 parent 655270e commit ff35886

File tree

2 files changed

+148
-1
lines changed

2 files changed

+148
-1
lines changed

Sources/CollectionView/CollectionView.swift

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,75 @@ import ScrollTracking
3636

3737
extension CollectionView {
3838

39+
/// Attaches an infinite‑scroll style loader to a CollectionView that calls your async closure
40+
/// as the user approaches the end of the scrollable content.
41+
///
42+
/// Use this modifier to automatically request and append more data to a `CollectionView` when the
43+
/// user scrolls near the end. The modifier observes the underlying scroll view produced by the
44+
/// selected layout and triggers `onLoad` once the remaining distance to the end is within
45+
/// `leadingScreens` times the visible length. If the content is initially smaller than the viewport,
46+
/// the loader triggers immediately so you can populate the view.
47+
///
48+
/// The `isLoading` binding is managed for you: it is set to `true` just before `onLoad` runs and
49+
/// reset to `false` when it finishes. While `isLoading` is `true`, additional triggers are suppressed.
50+
/// Only one load task runs at a time, and subsequent triggers are slightly debounced to avoid rapid
51+
/// re‑invocation when the user hovers near the threshold.
52+
///
53+
/// - Parameters:
54+
/// - isEnabled: Toggles the behavior on or off. When `false`, no loading is triggered. Default is `true`.
55+
/// - leadingScreens: The prefetch threshold expressed in multiples of the visible scrollable length
56+
/// (height for vertical layouts). For example, `2` triggers when the user is within two screenfuls
57+
/// of the end. Default is `2`.
58+
/// - isLoading: A binding that reflects the current loading state. This modifier sets it to `true`
59+
/// before calling `onLoad` and back to `false` when `onLoad` completes.
60+
/// - onLoad: An async closure executed on the main actor when the threshold is crossed. Perform your
61+
/// data fetch and append logic here.
62+
///
63+
/// - Returns: A view that monitors scrolling and triggers `onLoad` according to the provided parameters.
64+
///
65+
/// - Important: Avoid starting additional loads inside `onLoad` while `isLoading` is `true`. The
66+
/// modifier already prevents re‑entrancy by tracking the current load task and debouncing subsequent
67+
/// triggers.
68+
///
69+
/// - Note:
70+
/// - If the content length is smaller than the viewport, `onLoad` is triggered once on appear so
71+
/// you can fetch enough items to fill the screen.
72+
/// - Use non‑negative values for `leadingScreens`. Values near `0` trigger close to the end; larger
73+
/// values prefetch earlier.
74+
///
75+
/// - SeeAlso:
76+
/// - ``onAdditionalLoading(isEnabled:leadingScreens:isLoading:onLoad:)`` (non‑binding overload)
77+
/// - ``ScrollView/onAdditionalLoading(isEnabled:leadingScreens:isLoading:onLoad:)``
78+
/// - ``List/onAdditionalLoading(isEnabled:leadingScreens:isLoading:onLoad:)``
79+
///
80+
/// - Platform:
81+
/// - On iOS 18, macOS 15, tvOS 18, watchOS 11, and visionOS 2 or later, the modifier uses SwiftUI
82+
/// scroll geometry to observe position.
83+
/// - On earlier supported iOS versions, it relies on scroll view introspection to observe content offset.
84+
///
85+
/// - Example:
86+
/// ```swift
87+
/// struct FeedView: View {
88+
/// @State private var items: [Item] = []
89+
/// @State private var isLoading = false
90+
///
91+
/// var body: some View {
92+
/// CollectionView(layout: .list) {
93+
/// ForEach(items) { item in
94+
/// Row(item: item)
95+
/// }
96+
/// }
97+
/// .onAdditionalLoading(isEnabled: true,
98+
/// leadingScreens: 1.5,
99+
/// isLoading: $isLoading) {
100+
/// // Fetch more and append
101+
/// try? await Task.sleep(for: .seconds(1))
102+
/// let more = await fetchMoreItems()
103+
/// items.append(contentsOf: more)
104+
/// }
105+
/// }
106+
/// }
107+
/// ```
39108
@ViewBuilder
40109
public func onAdditionalLoading(
41110
isEnabled: Bool = true,
@@ -55,6 +124,85 @@ extension CollectionView {
55124

56125
}
57126

127+
/// Triggers a load-more action as the user approaches the end of the scrollable content,
128+
/// without managing any loading state internally.
129+
///
130+
/// This modifier observes the scroll position of the collection and invokes `onLoad` when
131+
/// the visible region nears the end of the content by the amount specified in `leadingScreens`.
132+
/// It is conditionally available when the ScrollTracking module can be imported.
133+
///
134+
/// Use this overload when you already manage loading state externally (e.g., in a view model)
135+
/// and simply want a callback to fire when additional content should be fetched. If you want
136+
/// the modifier to help manage loading state and support async work, consider the binding-based,
137+
/// async overload instead.
138+
///
139+
/// - Parameters:
140+
/// - isEnabled: A Boolean that enables or disables additional loading. When `false`, no callbacks
141+
/// are fired. Defaults to `true`.
142+
/// - leadingScreens: The prefetch distance, expressed as a multiple of the current viewport height.
143+
/// For example, `2` means `onLoad` is triggered once the user scrolls within two screen-heights
144+
/// of the end of the content. Defaults to `2`.
145+
/// - isLoading: A Boolean that indicates whether a load is currently in progress. When `true`,
146+
/// additional triggers are suppressed. This value is read-only from the modifier’s perspective;
147+
/// you are responsible for updating it in your own state to avoid duplicate loads.
148+
/// - onLoad: A closure executed on the main actor when the threshold is crossed and `isLoading` is `false`.
149+
/// Use this to kick off your loading logic (e.g., dispatch an async task or call into a view model).
150+
///
151+
/// - Returns: A view that monitors scroll position and invokes `onLoad` as the user approaches the end.
152+
///
153+
/// - Discussion:
154+
/// - The callback will not be invoked if the content is not scrollable, if `isEnabled` is `false`,
155+
/// or while `isLoading` is `true`.
156+
/// - Because this overload does not mutate `isLoading`, your code must set and clear loading state
157+
/// to prevent repeated triggers.
158+
/// - Choose `leadingScreens` based on your data-fetch latency and UI needs; values between `0.5` and `3`
159+
/// are common depending on how early you want to prefetch.
160+
/// - The `onLoad` closure runs on the main actor; if you need to perform asynchronous work,
161+
/// start a `Task { ... }` inside the closure or delegate to your view model.
162+
///
163+
/// - SeeAlso: The binding-based async overload:
164+
/// `onAdditionalLoading(isEnabled:leadingScreens:isLoading:onLoad:)` where `isLoading` is a `Binding<Bool>`
165+
/// and `onLoad` is `async`, which can simplify state management for loading.
166+
///
167+
/// - Example:
168+
/// ```swift
169+
/// struct FeedView: View {
170+
/// @StateObject private var viewModel = FeedViewModel()
171+
///
172+
/// var body: some View {
173+
/// CollectionView(layout: viewModel.layout) {
174+
/// ForEach(viewModel.items) { item in
175+
/// FeedRow(item: item)
176+
/// }
177+
/// }
178+
/// .onAdditionalLoading(
179+
/// isEnabled: true,
180+
/// leadingScreens: 1.5,
181+
/// isLoading: viewModel.isLoading
182+
/// ) {
183+
/// // Executed on the main actor
184+
/// viewModel.loadMore()
185+
/// }
186+
/// }
187+
/// }
188+
/// ```
189+
@ViewBuilder
190+
public func onAdditionalLoading(
191+
isEnabled: Bool = true,
192+
leadingScreens: Double = 2,
193+
isLoading: Bool,
194+
onLoad: @escaping @MainActor () -> Void
195+
) -> some View {
196+
self.onAdditionalLoading(
197+
additionalLoading: .init(
198+
isEnabled: isEnabled,
199+
leadingScreens: leadingScreens,
200+
isLoading: isLoading,
201+
onLoad: onLoad
202+
)
203+
)
204+
}
205+
58206
}
59207

60208
#endif

Sources/ScrollTracking/ScrollTracking.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,6 @@ extension List {
211211
)
212212
)
213213
)
214-
215214
}
216215
}
217216

0 commit comments

Comments
 (0)