Skip to content

Commit b5a3d0f

Browse files
committed
Add overscroll callback
1 parent 6f23d66 commit b5a3d0f

File tree

6 files changed

+91
-15
lines changed

6 files changed

+91
-15
lines changed

Examples/LazyPagerExampleApp/FullTestView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ struct FullTestView: View {
6464
.shouldLoadMore(on: .lastElement(minus: 2)) {
6565
data.append(Foo(img: "nora4", idx: data.count))
6666
}
67+
.overscroll { position in
68+
if position == .beginning {
69+
print("Swiped past beginning")
70+
} else {
71+
print("Swiped past end")
72+
}
73+
}
6774
.background(.black.opacity(opacity))
6875
.background(ClearFullScreenBackground())
6976
.ignoresSafeArea()

Examples/LazyPagerExampleApp/SimpleExample.swift

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,31 @@ struct SimpleExample: View {
1313
"nora6",
1414
]
1515

16+
@State var show = false
17+
1618
var body: some View {
17-
LazyPager(data: data) { element in
18-
Image(element)
19-
.resizable()
20-
.aspectRatio(contentMode: .fit)
19+
VStack {
20+
Button("Show") {
21+
show.toggle()
22+
}
23+
}
24+
.fullScreenCover(isPresented: $show) {
25+
LazyPager(data: data) { element in
26+
VStack {
27+
Image(element)
28+
.resizable()
29+
.aspectRatio(contentMode: .fit)
30+
.onTapGesture {
31+
print("tap image")
32+
}
33+
}
34+
.frame(maxWidth: .infinity, maxHeight: .infinity)
35+
.contentShape(Rectangle())
36+
.onTapGesture {
37+
show.toggle()
38+
print("tap background")
39+
}
40+
}
2141
}
2242
}
2343
}

Examples/LazyPagerExampleApp/VerticalMediaPager.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ struct VerticalMediaPager: View {
2525
.resizable()
2626
.aspectRatio(contentMode: .fill)
2727
}
28+
.overscroll {
29+
if $0 == .beginning {
30+
print("Swiped past beginning")
31+
} else if $0 == .end {
32+
print("Swiped past end")
33+
}
34+
}
2835
.ignoresSafeArea()
2936

3037
VStack(alignment: .leading) {

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ var body: some View {
8989
.shouldLoadMore {
9090
data.append("foobar")
9191
}
92+
93+
// Get notified when swiping past the beginning or end of the list
94+
.overscroll { position in
95+
if position == .beginning {
96+
print("Swiped past beginning")
97+
} else {
98+
print("Swiped past end")
99+
}
100+
}
92101

93102
// Set the background color with the drag opacity control
94103
.background(.black.opacity(opacity))
@@ -130,7 +139,7 @@ For a full working example, [open the sample project](https://github.com/gh123ma
130139
- Works with `.ignoresSafeArea()` (or not) to get a true full screen view.
131140
- Drag to dismiss is supported with `.onDismiss` - Supply a binding opacity value to control the background opacity during the transition.
132141
- Tap events are handled internally, so use `.onTap` to handle single taps (useful for hiding and showing UI)
133-
- Use `.settings` to [modify advanced settings](https://github.com/gh123man/SwiftUI-LazyPager/blob/master/Sources/LazyPager/LazyPager.swift#L54)
142+
- Use `.settings` to [modify advanced settings](https://github.com/gh123man/SwiftUI-LazyPager/blob/master/Sources/LazyPager/LazyPager.swift#L62)
134143

135144

136145
# Detailed usage

Sources/LazyPager/LazyPager.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public enum Direction {
2323
case vertical
2424
}
2525

26+
public enum ListPosition {
27+
case beginning
28+
case end
29+
}
30+
2631
public struct Config {
2732
/// binding variable to control a custom background opacity. LazyPager is transparent by default
2833
public var backgroundOpacity: Binding<CGFloat>?
@@ -51,7 +56,10 @@ public struct Config {
5156
/// Direction of the pager
5257
public var direction : Direction = .horizontal
5358

54-
/// Advanced settings (only accessible via .settings)
59+
/// Called whent the end of data is reached and the user tries to swipe again
60+
public var overscrollCallback: ((ListPosition) -> ())?
61+
62+
/// Advanced settings (only accessibleevia .settings)
5563

5664
/// How may out of view pages to load in advance (forward and backwards)
5765
public var preloadAmount: Int = 3
@@ -74,6 +82,9 @@ public struct Config {
7482

7583
/// The minimum scroll distance the in which the pinch gesture is enabled
7684
public var pinchGestureEnableOffset: Double = 10
85+
86+
/// % ammount (from 0-1) of overscroll needed to call overscrollCallback
87+
public var overscrollThreshold: Double = 0.15
7788
}
7889

7990
public struct LazyPager<Element, DataCollecton: RandomAccessCollection, Content: View> where DataCollecton.Index == Int, DataCollecton.Element == Element {
@@ -133,6 +144,12 @@ public extension LazyPager {
133144
adjust(&this.config)
134145
return this
135146
}
147+
148+
func overscroll(_ callback: @escaping (ListPosition) -> ()) -> LazyPager {
149+
var this = self
150+
this.config.overscrollCallback = callback
151+
return this
152+
}
136153
}
137154

138155
extension LazyPager: UIViewControllerRepresentable {

Sources/LazyPager/PagerView.swift

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -295,22 +295,37 @@ class PagerView<Element, Loader: ViewLoader, Content: View>: UIScrollView, UIScr
295295
// MARK: UISCrollVieDelegate methods
296296

297297
var lastPos: CGFloat = 0
298+
var hasNotfiedOverscroll = false
298299

299300
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
301+
var relativeIndex: Int
302+
var absoluteOffset: CGFloat
303+
if config.direction == .horizontal {
304+
absoluteOffset = scrollView.contentOffset.x / scrollView.frame.width
305+
relativeIndex = Int(round(absoluteOffset))
306+
} else {
307+
absoluteOffset = scrollView.contentOffset.y / scrollView.frame.height
308+
relativeIndex = Int(round(absoluteOffset))
309+
}
310+
relativeIndex = relativeIndex < 0 ? 0 : relativeIndex
311+
relativeIndex = relativeIndex >= loadedViews.count ? loadedViews.count-1 : relativeIndex
312+
300313
if !scrollView.isTracking, !isRotating {
301-
var relativeIndex: Int
302-
if config.direction == .horizontal {
303-
relativeIndex = Int(round(scrollView.contentOffset.x / scrollView.frame.width))
304-
} else {
305-
relativeIndex = Int(round(scrollView.contentOffset.y / scrollView.frame.height))
306-
}
307-
relativeIndex = relativeIndex < 0 ? 0 : relativeIndex
308-
relativeIndex = relativeIndex >= loadedViews.count ? loadedViews.count-1 : relativeIndex
309-
310314
currentIndex = loadedViews[relativeIndex].index
311315
page.wrappedValue = currentIndex
312316
}
313317

318+
if !hasNotfiedOverscroll {
319+
if relativeIndex >= loadedViews.count-1, absoluteOffset - CGFloat(relativeIndex) > config.overscrollThreshold {
320+
config.overscrollCallback?(.end)
321+
hasNotfiedOverscroll = true
322+
}
323+
324+
if relativeIndex <= 0, absoluteOffset - CGFloat(relativeIndex) < -config.overscrollThreshold {
325+
config.overscrollCallback?(.beginning)
326+
hasNotfiedOverscroll = true
327+
}
328+
}
314329

315330
// Horribly janky way to detect when scrolling (both touching and animation) is finnished.
316331
let caputred: CGFloat
@@ -324,6 +339,7 @@ class PagerView<Element, Loader: ViewLoader, Content: View>: UIScrollView, UIScr
324339
lastPos = caputred
325340
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
326341
if self.lastPos == caputred, !scrollView.isTracking {
342+
self.hasNotfiedOverscroll = false
327343
self.resizeOutOfBoundsViews()
328344
}
329345
}

0 commit comments

Comments
 (0)