Skip to content

Commit 0cee012

Browse files
muukiiclaude
andauthored
Add horizontal scrolling support to onAdditionalLoading (#67)
## Summary This PR adds horizontal scrolling support to the `.onAdditionalLoading` modifier. Previously, the modifier only supported vertical scrolling, but now it works with both horizontal and vertical scroll axes. ## Changes - **Added `axis` parameter** to `AdditionalLoading` struct with default value `.vertical` - **Refactored `calculate` function** to use generic parameters instead of Y-specific ones - **Updated `_Modifier` implementation** to handle both horizontal and vertical axes using switch statements - **Added axis parameter to all public APIs**: - `ScrollView.onAdditionalLoading` (binding and non-binding overloads) - `List.onAdditionalLoading` - `CollectionView.onAdditionalLoading` (binding and non-binding overloads) - **Added horizontal scrolling preview** demonstrating the new functionality - **Updated documentation** to reflect horizontal scrolling support ## Usage Example ### Horizontal scrolling ```swift ScrollView(.horizontal) { LazyHStack { ForEach(items) { item in ItemView(item: item) } } } .onAdditionalLoading( leadingScreens: 1, axis: .horizontal, // New parameter! isLoading: $isLoading, onLoad: { // Load more items... } ) ``` ### Vertical scrolling (unchanged) ```swift ScrollView { LazyVStack { ForEach(items) { item in ItemView(item: item) } } } .onAdditionalLoading( isLoading: $isLoading, // axis defaults to .vertical onLoad: { // Load more items... } ) ``` ## Backward Compatibility All existing code continues to work without changes since the `axis` parameter defaults to `.vertical`. ## Test Plan - [x] All existing tests pass - [x] Package builds successfully - [x] Added horizontal scrolling preview for manual testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 561c6dc commit 0cee012

File tree

2 files changed

+139
-54
lines changed

2 files changed

+139
-54
lines changed

Sources/CollectionView/CollectionView.swift

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ extension CollectionView {
5252
/// - Parameters:
5353
/// - isEnabled: Toggles the behavior on or off. When `false`, no loading is triggered. Default is `true`.
5454
/// - leadingScreens: The prefetch threshold expressed in multiples of the visible scrollable length
55-
/// (height for vertical layouts). For example, `2` triggers when the user is within two screenfuls
56-
/// of the end. Default is `2`.
55+
/// (height for vertical, width for horizontal). For example, `2` triggers when the user is within
56+
/// two screenfuls of the end. Default is `2`.
57+
/// - axis: The scroll axis to monitor. Use `.vertical` for vertical scrolling or `.horizontal` for
58+
/// horizontal scrolling. Default is `.vertical`.
5759
/// - isLoading: A binding that reflects the current loading state. This modifier sets it to `true`
5860
/// before calling `onLoad` and back to `false` when `onLoad` completes.
5961
/// - onLoad: An async closure executed on the main actor when the threshold is crossed. Perform your
@@ -72,9 +74,9 @@ extension CollectionView {
7274
/// values prefetch earlier.
7375
///
7476
/// - SeeAlso:
75-
/// - ``onAdditionalLoading(isEnabled:leadingScreens:isLoading:onLoad:)`` (non‑binding overload)
76-
/// - ``ScrollView/onAdditionalLoading(isEnabled:leadingScreens:isLoading:onLoad:)``
77-
/// - ``List/onAdditionalLoading(isEnabled:leadingScreens:isLoading:onLoad:)``
77+
/// - ``onAdditionalLoading(isEnabled:leadingScreens:axis:isLoading:onLoad:)`` (non‑binding overload)
78+
/// - ``ScrollView/onAdditionalLoading(isEnabled:leadingScreens:axis:isLoading:onLoad:)``
79+
/// - ``List/onAdditionalLoading(isEnabled:leadingScreens:axis:isLoading:onLoad:)``
7880
///
7981
/// - Platform:
8082
/// - On iOS 18, macOS 15, tvOS 18, watchOS 11, and visionOS 2 or later, the modifier uses SwiftUI
@@ -95,6 +97,7 @@ extension CollectionView {
9597
/// }
9698
/// .onAdditionalLoading(isEnabled: true,
9799
/// leadingScreens: 1.5,
100+
/// axis: .vertical,
98101
/// isLoading: $isLoading) {
99102
/// // Fetch more and append
100103
/// try? await Task.sleep(for: .seconds(1))
@@ -108,47 +111,51 @@ extension CollectionView {
108111
public func onAdditionalLoading(
109112
isEnabled: Bool = true,
110113
leadingScreens: Double = 2,
114+
axis: Axis = .vertical,
111115
isLoading: Binding<Bool>,
112116
onLoad: @MainActor @escaping () async -> Void
113117
) -> some View {
114-
115-
self.onAdditionalLoading(
118+
119+
self.onAdditionalLoading(
116120
additionalLoading: .init(
117121
isEnabled: isEnabled,
118122
leadingScreens: leadingScreens,
119123
isLoading: isLoading,
124+
axis: axis,
120125
onLoad: onLoad
121126
)
122127
)
123-
128+
124129
}
125130

126131
/// Triggers a load-more action as the user approaches the end of the scrollable content,
127132
/// without managing any loading state internally.
128-
///
133+
///
129134
/// This modifier observes the scroll position of the collection and invokes `onLoad` when
130135
/// the visible region nears the end of the content by the amount specified in `leadingScreens`.
131136
/// It is conditionally available when the ScrollTracking module can be imported.
132-
///
137+
///
133138
/// Use this overload when you already manage loading state externally (e.g., in a view model)
134139
/// and simply want a callback to fire when additional content should be fetched. If you want
135140
/// the modifier to help manage loading state and support async work, consider the binding-based,
136141
/// async overload instead.
137-
///
142+
///
138143
/// - Parameters:
139144
/// - isEnabled: A Boolean that enables or disables additional loading. When `false`, no callbacks
140145
/// are fired. Defaults to `true`.
141-
/// - leadingScreens: The prefetch distance, expressed as a multiple of the current viewport height.
142-
/// For example, `2` means `onLoad` is triggered once the user scrolls within two screen-heights
143-
/// of the end of the content. Defaults to `2`.
146+
/// - leadingScreens: The prefetch distance, expressed as a multiple of the current viewport length
147+
/// (height for vertical, width for horizontal). For example, `2` means `onLoad` is triggered once
148+
/// the user scrolls within two screen-lengths of the end of the content. Defaults to `2`.
149+
/// - axis: The scroll axis to monitor. Use `.vertical` for vertical scrolling or `.horizontal` for
150+
/// horizontal scrolling. Default is `.vertical`.
144151
/// - isLoading: A Boolean that indicates whether a load is currently in progress. When `true`,
145-
/// additional triggers are suppressed. This value is read-only from the modifiers perspective;
152+
/// additional triggers are suppressed. This value is read-only from the modifier's perspective;
146153
/// you are responsible for updating it in your own state to avoid duplicate loads.
147154
/// - onLoad: A closure executed on the main actor when the threshold is crossed and `isLoading` is `false`.
148155
/// Use this to kick off your loading logic (e.g., dispatch an async task or call into a view model).
149-
///
156+
///
150157
/// - Returns: A view that monitors scroll position and invokes `onLoad` as the user approaches the end.
151-
///
158+
///
152159
/// - Discussion:
153160
/// - The callback will not be invoked if the content is not scrollable, if `isEnabled` is `false`,
154161
/// or while `isLoading` is `true`.
@@ -158,16 +165,16 @@ extension CollectionView {
158165
/// are common depending on how early you want to prefetch.
159166
/// - The `onLoad` closure runs on the main actor; if you need to perform asynchronous work,
160167
/// start a `Task { ... }` inside the closure or delegate to your view model.
161-
///
168+
///
162169
/// - SeeAlso: The binding-based async overload:
163-
/// `onAdditionalLoading(isEnabled:leadingScreens:isLoading:onLoad:)` where `isLoading` is a `Binding<Bool>`
170+
/// `onAdditionalLoading(isEnabled:leadingScreens:axis:isLoading:onLoad:)` where `isLoading` is a `Binding<Bool>`
164171
/// and `onLoad` is `async`, which can simplify state management for loading.
165-
///
172+
///
166173
/// - Example:
167174
/// ```swift
168175
/// struct FeedView: View {
169176
/// @StateObject private var viewModel = FeedViewModel()
170-
///
177+
///
171178
/// var body: some View {
172179
/// CollectionView(layout: viewModel.layout) {
173180
/// ForEach(viewModel.items) { item in
@@ -177,6 +184,7 @@ extension CollectionView {
177184
/// .onAdditionalLoading(
178185
/// isEnabled: true,
179186
/// leadingScreens: 1.5,
187+
/// axis: .vertical,
180188
/// isLoading: viewModel.isLoading
181189
/// ) {
182190
/// // Executed on the main actor
@@ -189,14 +197,16 @@ extension CollectionView {
189197
public func onAdditionalLoading(
190198
isEnabled: Bool = true,
191199
leadingScreens: Double = 2,
200+
axis: Axis = .vertical,
192201
isLoading: Bool,
193202
onLoad: @escaping @MainActor () -> Void
194203
) -> some View {
195-
self.onAdditionalLoading(
204+
self.onAdditionalLoading(
196205
additionalLoading: .init(
197206
isEnabled: isEnabled,
198207
leadingScreens: leadingScreens,
199208
isLoading: isLoading,
209+
axis: axis,
200210
onLoad: onLoad
201211
)
202212
)

0 commit comments

Comments
 (0)