Skip to content

Commit 3e091c5

Browse files
authored
Merge pull request #161 from fermoya/feat/bounce-workaround
Feat/bounce workaround
2 parents 6b5afb7 + 81c2719 commit 3e091c5

File tree

13 files changed

+177
-256
lines changed

13 files changed

+177
-256
lines changed

Documentation/Usage.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,17 @@ Pager(...)
127127

128128
Be aware that this modifier will change the loading policy. See [Content Loading Policy](#content-loading-policy) for more information.
129129

130+
### More modifiers
131+
| **Modifier** | **Description** |
132+
|---|---|
133+
| `allowsDragging` | whether or not dragging is allowed |
134+
| `disableDragging` | disables dragging |
135+
| `bounces` | whether or not `Pager` should bounce |
136+
| `delaysTouches` | whether or not touches shoulf be delayed. Useful if nested in `ScrollView` |
137+
| `pageOffset` | allows _manual_ scroll |
138+
| `expandPageToEdges` | modifies `itemAspectRatio` so that the use up all the space available |
139+
140+
130141
## Paging Priority
131142

132143
For complex pages where a `Gesture` might be used, or any other `View` that internally holds a `Gesture` like `Button` or `NavigationLink`, you might come across issues when trying to swipe on top of certain areas within the page. For these scenarios, use `pagingPriority` to select the option that best suits your purpose. For instance, a page containing a `NavigationLink` won't be scrollable over the link area unless `pagingPrioriry(.simultaneous)` is added:
@@ -197,7 +208,7 @@ Transform your `Pager` into an endless sroll by using `loopPages`:
197208

198209
**Note**: You'll need a minimum number of elements to use this modifier based on the page size. If you need more items, use `loopPages(repeating:)` to let `Pager` know elements should be repeated in batches.
199210

200-
## Page Tranistions
211+
## Page Transitions
201212

202213
Use `pagingAnimation` to customize the _transition_ to the next page once the drag has ended. This is achieve by a block with a `DragResult`which contains:
203214
* Current page
@@ -225,6 +236,8 @@ Pager(...)
225236
})
226237
```
227238

239+
You can also use `onDraggingBegan`, `onDraggingChanged` and `onDragginEnded` to keep track of the dragging.
240+
228241
## Add pages on demand
229242

230243
You can use `onPageChanged` to add new items on demand whenever the user is getting to the last page:
@@ -247,6 +260,30 @@ var body: some View {
247260
}
248261
```
249262

263+
At the same time, items can be added at the start. Notice you'll need to update the page yourself (as you're inserting new elements) to keep `Pager` focused on the right element:
264+
265+
```swift
266+
267+
@State var count: Int = -1
268+
@State var page: Int = 3
269+
@State var data = Array(0..<5)
270+
271+
Pager(page: self.$page,
272+
data: self.data,
273+
id: \.self) {
274+
self.pageView($0)
275+
}
276+
.onPageChanged({ page in
277+
guard page == 1 else { return }
278+
let newData = (1...5).map { $0 * self.count }
279+
withAnimation {
280+
self.data1.insert(contentsOf: newData, at: 0)
281+
self.page1 += 5
282+
self.count -= 1
283+
}
284+
})
285+
```
286+
250287
## Content Loading Policy
251288

252289
`Pager` recycles views by default and won't have loaded all pages in memory at once. In some scenarios, this might be counterproductive, for example, if you're trying to manually scroll from the first page to the last. For these scenarios, use `.contentLoadingPolicy` and choose among the options available.

Example/SwiftUIPagerExample.xcodeproj/project.pbxproj

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
6B4EC8A8240D1182001E7490 /* BizarreExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B4EC8A7240D1182001E7490 /* BizarreExampleView.swift */; };
2121
6B6FAA3D24D553C8000D1539 /* PagingAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B6FAA3C24D553C8000D1539 /* PagingAnimation.swift */; };
2222
6B9C4A8A24B45F66004C06C5 /* OnDeactivateModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B9C4A8924B45F66004C06C5 /* OnDeactivateModifier.swift */; };
23-
6BB1AAD324C9C9D20032B5A3 /* PagerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB1AAD224C9C9D20032B5A3 /* PagerProxy.swift */; };
2423
6BB1AAD524C9CA1C0032B5A3 /* PagerContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB1AAD424C9CA1C0032B5A3 /* PagerContent.swift */; };
2524
6BB1AAD724C9D0820032B5A3 /* Pager+Buildable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB1AAD624C9D0820032B5A3 /* Pager+Buildable.swift */; };
2625
6BC5EDFC24866D9500E1E78C /* PagerContent+Buildable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC5EDF424866D9500E1E78C /* PagerContent+Buildable.swift */; };
@@ -67,7 +66,6 @@
6766
6B4EC8A7240D1182001E7490 /* BizarreExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BizarreExampleView.swift; sourceTree = "<group>"; };
6867
6B6FAA3C24D553C8000D1539 /* PagingAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PagingAnimation.swift; path = ../../Sources/SwiftUIPager/PageConfiguration/PagingAnimation.swift; sourceTree = "<group>"; };
6968
6B9C4A8924B45F66004C06C5 /* OnDeactivateModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDeactivateModifier.swift; sourceTree = "<group>"; };
70-
6BB1AAD224C9C9D20032B5A3 /* PagerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagerProxy.swift; sourceTree = "<group>"; };
7169
6BB1AAD424C9CA1C0032B5A3 /* PagerContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PagerContent.swift; path = ../../Sources/SwiftUIPager/PagerContent.swift; sourceTree = "<group>"; };
7270
6BB1AAD624C9D0820032B5A3 /* Pager+Buildable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Pager+Buildable.swift"; path = "../../Sources/SwiftUIPager/Pager+Buildable.swift"; sourceTree = "<group>"; };
7371
6BC5EDF424866D9500E1E78C /* PagerContent+Buildable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "PagerContent+Buildable.swift"; path = "../../Sources/SwiftUIPager/PagerContent+Buildable.swift"; sourceTree = "<group>"; };
@@ -147,21 +145,11 @@
147145
path = Examples;
148146
sourceTree = "<group>";
149147
};
150-
6BB1AAD124C9C9370032B5A3 /* Proxy */ = {
151-
isa = PBXGroup;
152-
children = (
153-
6BB1AAD224C9C9D20032B5A3 /* PagerProxy.swift */,
154-
);
155-
name = Proxy;
156-
path = ../../Sources/SwiftUIPager/Proxy;
157-
sourceTree = "<group>";
158-
};
159148
6BC5EDEB24866D6000E1E78C /* Pagination */ = {
160149
isa = PBXGroup;
161150
children = (
162151
6BC5EDF524866D9500E1E78C /* Helpers */,
163152
6BEA730E24ACF8BB007EA8DC /* PageConfiguration */,
164-
6BB1AAD124C9C9370032B5A3 /* Proxy */,
165153
6BC5EDFA24866D9500E1E78C /* Pager.swift */,
166154
6BB1AAD624C9D0820032B5A3 /* Pager+Buildable.swift */,
167155
6BB1AAD424C9CA1C0032B5A3 /* PagerContent.swift */,
@@ -274,7 +262,6 @@
274262
buildActionMask = 2147483647;
275263
files = (
276264
6B4EC8A2240D072B001E7490 /* ColorsExampleView.swift in Sources */,
277-
6BB1AAD324C9C9D20032B5A3 /* PagerProxy.swift in Sources */,
278265
17D9E0F423D4CF6700C5AE93 /* AppDelegate.swift in Sources */,
279266
6BEF677024C98B62008533FE /* PageWrapper.swift in Sources */,
280267
6BD3828224C97DE3007B1CF6 /* CGPoint+Angle.swift in Sources */,

Example/SwiftUIPagerExample/Examples/NestedExampleView.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import SwiftUI
1111
struct NestedExampleView: View {
1212

1313
@State var page: Int = 0
14+
@State var pageOffset: Double = 0
1415
@State var nestedPages: [Int] = [0, 0, 0, 0]
1516

1617
var data = Array(0..<4)
@@ -22,6 +23,7 @@ struct NestedExampleView: View {
2223
id: \.self) { page in
2324
self.nestedPager(page)
2425
}
26+
.pageOffset(pageOffset)
2527
.swipeInteractionArea(.allAvailable)
2628
.background(Color.gray.opacity(0.2))
2729
}
@@ -47,9 +49,26 @@ struct NestedExampleView: View {
4749
id: \.self) { page in
4850
self.pageView(page)
4951
}
52+
.bounces(false)
5053
.onDraggingBegan({
5154
print("Dragging Began")
5255
})
56+
.onDraggingChanged { increment in
57+
withAnimation {
58+
if binding.wrappedValue == self.nestedData.count - 1, increment > 0 {
59+
pageOffset = increment
60+
} else if binding.wrappedValue == 0, increment < 0 {
61+
pageOffset = increment
62+
}
63+
}
64+
}
65+
.onDraggingEnded { increment in
66+
guard pageOffset != 0 else { return }
67+
withAnimation {
68+
pageOffset = 0
69+
page += Int(increment.rounded())
70+
}
71+
}
5372
.itemSpacing(10)
5473
.itemAspectRatio(0.8, alignment: .end)
5574
.padding(8)

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
_SwiftUIPager_ provides a `Pager` component built with SwiftUI native components. `Pager` is a view that renders a scrollable container to display a handful of pages. These pages are recycled on scroll, so you don't have to worry about memory issues. `Pager` will load just a handful of items, enough to beatifully scroll along.
1212

13-
Create vertical or horizontal pagers, align the cards, change the direction of the scroll, animate the pagintation... `Pager` lets you do anything you want.
13+
Create vertical or horizontal pagers, align the cards, change the direction of the scroll, animate the pagination... `Pager` lets you do anything you want.
1414

1515
- [Requirements](#requirements)
1616
- [Installation](#installation)
@@ -28,12 +28,13 @@ Create vertical or horizontal pagers, align the cards, change the direction of t
2828
- [Alignment](Documentation/Usage.md#alignment)
2929
- [Partial pagination](Documentation/Usage.md#partial-pagination)
3030
- [Multiple pagination](Documentation/Usage.md#multiple-pagination)
31+
- [More modifiers](Documentation/Usage.md#more-modifiers)
3132
- [Paging Priority](Documentation/Usage.md#paging-priority)
3233
- [Animations](Documentation/Usage.md#animations)
3334
- [Scale](Documentation/Usage.md#scale)
3435
- [Rotation](Documentation/Usage.md#rotation)
3536
- [Loop](Documentation/Usage.md#loop)
36-
- [Page Tranistions](Documentation/Usage.md#page-transitions)
37+
- [Page Transitions](Documentation/Usage.md#page-transitions)
3738
- [Add pages on demand](Documentation/Usage.md#add-pages-on-demand)
3839
- [Content Loading Policy](Documentation/Usage.md#content-loading-policy)
3940
- [Examples](Documentation/Usage.md#examples)

Sources/SwiftUIPager/Pager+Buildable.swift

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import SwiftUI
1010

1111
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
12-
extension Pager: Buildable, PagerProxy {
12+
extension Pager: Buildable {
1313

1414
/// Result of paginating
1515
public typealias DragResult = (page: Int, newPage: Int, translation: CGFloat, velocity: Double)
@@ -113,6 +113,32 @@ extension Pager: Buildable, PagerProxy {
113113
mutating(keyPath: \.swipeInteractionArea, value: value)
114114
}
115115

116+
/// Sets whether `Pager` should bounce or not
117+
public func bounces(_ value: Bool) -> Self {
118+
mutating(keyPath: \.bounces, value: value)
119+
}
120+
121+
/// Adds a callback to react when dragging begins
122+
///
123+
/// - Parameter callback: block to be called when dragging begins
124+
public func onDraggingBegan(_ callback: (() -> Void)?) -> Self {
125+
mutating(keyPath: \.onDraggingBegan, value: callback)
126+
}
127+
128+
/// Adds a callback to react when dragging changes
129+
///
130+
/// - Parameter callback: block to be called when dragging changes. `pageInrement` is passed as argument
131+
public func onDraggingChanged(_ callback: ((Double) -> Void)?) -> Self {
132+
mutating(keyPath: \.onDraggingChanged, value: callback)
133+
}
134+
135+
/// Adds a callback to react when dragging ends
136+
///
137+
/// - Parameter callback: block to be called when dragging ends. `pageInrement` is passed as argument
138+
public func onDraggingEnded(_ callback: ((Double) -> Void)?) -> Self {
139+
mutating(keyPath: \.onDraggingEnded, value: callback)
140+
}
141+
116142
#endif
117143

118144
/// Changes the a the alignment of the pages relative to their container
@@ -216,13 +242,6 @@ extension Pager: Buildable, PagerProxy {
216242
public func onPageChanged(_ callback: ((Int) -> Void)?) -> Self {
217243
mutating(keyPath: \.onPageChanged, value: callback)
218244
}
219-
220-
/// Adds a callback to react when dragging begins. Useful for dismissing a keyboard like a scrollview
221-
///
222-
/// - Parameter callback: block to be called when dragging begins
223-
public func onDraggingBegan(_ callback: (() -> Void)?) -> Self {
224-
mutating(keyPath: \.onDraggingBegan, value: callback)
225-
}
226245

227246
/// Sets some padding on the non-scroll axis
228247
///

Sources/SwiftUIPager/Pager.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ public struct Pager<Element, ID, PageView>: View where PageView: View, Element:
6464

6565
/*** ViewModified properties ***/
6666

67+
/// Whether `Pager` should bounce or not
68+
var bounces: Bool = true
69+
6770
/// Max relative item size that `Pager` will scroll before determining whether to move to the next page
6871
var pageRatio: CGFloat = 1
6972

@@ -139,6 +142,12 @@ public struct Pager<Element, ID, PageView>: View where PageView: View, Element:
139142
/// Callback for when dragging begins
140143
var onDraggingBegan: (() -> Void)?
141144

145+
/// Callback for when dragging changes
146+
var onDraggingChanged: ((Double) -> Void)?
147+
148+
/// Callback for when dragging ends
149+
var onDraggingEnded: ((Double) -> Void)?
150+
142151
/*** State and Binding properties ***/
143152

144153
/// Size of the view
@@ -200,7 +209,6 @@ public struct Pager<Element, ID, PageView>: View where PageView: View, Element:
200209
.itemSpacing(itemSpacing)
201210
.itemAspectRatio(itemAspectRatio, alignment: itemAlignment)
202211
.onPageChanged(onPageChanged)
203-
.onDraggingBegan(onDraggingBegan)
204212
.padding(sideInsets)
205213
.pagingAnimation(pagingAnimation)
206214
.partialPagination(pageRatio)
@@ -212,6 +220,10 @@ public struct Pager<Element, ID, PageView>: View where PageView: View, Element:
212220
.pagingPriority(gesturePriority)
213221
.delaysTouches(delaysTouches)
214222
.sensitivity(sensitivity)
223+
.onDraggingBegan(onDraggingBegan)
224+
.onDraggingChanged(onDraggingChanged)
225+
.onDraggingEnded(onDraggingEnded)
226+
.bounces(bounces)
215227
#endif
216228

217229
pagerContent = allowsMultiplePagination ? pagerContent.multiplePagination() : pagerContent

Sources/SwiftUIPager/PagerContent+Buildable.swift

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import SwiftUI
1010

1111
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
12-
extension Pager.PagerContent: Buildable, PagerProxy {
12+
extension Pager.PagerContent: Buildable {
1313

1414
/// Sets the animation to be applied when the user stops dragging
1515
///
@@ -109,6 +109,32 @@ extension Pager.PagerContent: Buildable, PagerProxy {
109109
mutating(keyPath: \.swipeInteractionArea, value: value)
110110
}
111111

112+
/// Sets whether `Pager` should bounce or not
113+
func bounces(_ value: Bool) -> Self {
114+
mutating(keyPath: \.bounces, value: value)
115+
}
116+
117+
/// Adds a callback to react when dragging begins. Useful for dismissing a keyboard like a scrollview
118+
///
119+
/// - Parameter callback: block to be called when dragging begins
120+
func onDraggingBegan(_ callback: (() -> Void)?) -> Self {
121+
mutating(keyPath: \.onDraggingBegan, value: callback)
122+
}
123+
124+
/// Adds a callback to react when dragging changes
125+
///
126+
/// - Parameter callback: block to be called when dragging changes. `pageInrement` is passed as argument
127+
func onDraggingChanged(_ callback: ((Double) -> Void)?) -> Self {
128+
mutating(keyPath: \.onDraggingChanged, value: callback)
129+
}
130+
131+
/// Adds a callback to react when dragging ends
132+
///
133+
/// - Parameter callback: block to be called when dragging ends. `pageInrement` is passed as argument
134+
func onDraggingEnded(_ callback: ((Double) -> Void)?) -> Self {
135+
mutating(keyPath: \.onDraggingEnded, value: callback)
136+
}
137+
112138
#endif
113139

114140
/// Changes the a the alignment of the pages relative to their container
@@ -205,13 +231,6 @@ extension Pager.PagerContent: Buildable, PagerProxy {
205231
func onPageChanged(_ callback: ((Int) -> Void)?) -> Self {
206232
mutating(keyPath: \.onPageChanged, value: callback)
207233
}
208-
209-
/// Adds a callback to react when dragging begins. Useful for dismissing a keyboard like a scrollview
210-
///
211-
/// - Parameter callback: block to be called when dragging begins
212-
func onDraggingBegan(_ callback: (() -> Void)?) -> Self {
213-
mutating(keyPath: \.onDraggingBegan, value: callback)
214-
}
215234

216235
/// Sets some padding on the non-scroll axis
217236
///

Sources/SwiftUIPager/PagerContent+Helper.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,16 @@ extension Pager.PagerContent {
7474
/// Minimum offset allowed. This allows a bounce offset
7575
var offsetLowerbound: CGFloat {
7676
guard currentPage == 0, !isInifinitePager else { return CGFloat(numberOfPages) * self.size.width }
77-
return CGFloat(numberOfPagesDisplayed) / 2 * pageDistance - pageDistance / 4 + alignmentOffset
77+
let bounceOffset = bounces ? -pageDistance / 4 : -pageDistance / 2
78+
return CGFloat(numberOfPagesDisplayed) / 2 * pageDistance + bounceOffset + alignmentOffset
7879
}
7980

8081
/// Maximum offset allowed. This allows a bounce offset
8182
var offsetUpperbound: CGFloat {
8283
guard currentPage == numberOfPages - 1, !isInifinitePager else { return -CGFloat(numberOfPages) * self.size.width }
8384
let a = -CGFloat(numberOfPagesDisplayed) / 2
84-
let b = pageDistance / 4
85-
return a * pageDistance + b + alignmentOffset
85+
let bounceOffset = bounces ? pageDistance / 4 : pageDistance / 2
86+
return a * pageDistance + bounceOffset + alignmentOffset
8687
}
8788

8889
/// Addition of `draggingOffset` and `contentOffset`

0 commit comments

Comments
 (0)