Skip to content

Commit c87cb00

Browse files
committed
Add completion handler + timer to reset height changes
1 parent 5eeea06 commit c87cb00

File tree

3 files changed

+88
-4
lines changed

3 files changed

+88
-4
lines changed

Examples/ContentView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ struct DetailsSearch: View {
4141
.scaleEffect(2)
4242
}
4343
}
44+
// .shouldLoadMore { done in
45+
// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
46+
// data.append(data.last! + 1)
47+
// print("foo")
48+
// done()
49+
// }
50+
// }
4451
.shouldLoadMore {
4552
await Task.sleep(seconds: 0.1)
4653
// data.append(contentsOf: (data.last! + 1)...data.last! + 100)

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,28 @@ Set the absolute offset to a fixed value:
6363
}
6464
```
6565

66+
### `waitForHeightChange`
67+
68+
It may be desirable for `shouldLoadMore` to be called whenever the user scrolls - even if the scroll view content didn't change. You can change this behavior with `waitForHeightChange`:
69+
```swift
70+
.shouldLoadMore(waitForHeightChange: .never) {
71+
// Will be called regardless of if the height changed from a previous update
72+
}
73+
```
74+
75+
```swift
76+
.shouldLoadMore(waitForHeightChange: .always) {
77+
// Will only be called if the content height changed since last time
78+
}
79+
```
80+
81+
```swift
82+
.shouldLoadMore(waitForHeightChange: .after(2)) {
83+
// Will only be called if the content height changed since last time or after 2 seconds of no change
84+
}
85+
```
86+
and now `shouldLoadMore` will be called whenever it's in the offset threshold. By default `waitForHeightChange` is `true` so the function doesn't get called in quick succession when no content updates are made.
87+
6688
## More details
6789

6890
- The callback will only be called once when the bottom approaches.
@@ -72,6 +94,17 @@ Set the absolute offset to a fixed value:
7294

7395
# More Examples
7496

97+
## Using a completion handler instead of `async`
98+
99+
```swift
100+
.shouldLoadMore { done in
101+
loadYourContent {
102+
data.append(data.last! + 1)
103+
done() // Call done so shouldLoadMore can be called again later
104+
}
105+
}
106+
```
107+
75108
Larger batching
76109

77110
![Navigation](/images/1.gif)

Sources/ScrollViewLoader/ScrollViewLoader.swift

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,41 @@ import SwiftUI
33
import UIKit
44
import Introspect
55

6+
public enum HeightChangeConfig {
7+
case always
8+
case after(TimeInterval)
9+
case never
10+
}
11+
612
public enum OffsetTrigger {
713
case relative(CGFloat)
814
case absolute(CGFloat)
915
}
1016

1117
extension View {
1218
public func shouldLoadMore(bottomDistance offsetTrigger: OffsetTrigger = .relative(0.5),
13-
shouldLoadMore: @escaping () async -> ()) -> some View {
19+
waitForHeightChange: HeightChangeConfig = .after(2),
20+
shouldLoadMore: @escaping () async -> ()) -> some View {
1421
return DelegateHolder(offsetNotifier: ScrollOffsetNotifier(offsetTrigger: offsetTrigger,
22+
waitForHeightChange: waitForHeightChange,
1523
onNotify: shouldLoadMore),
1624
content: self)
1725
}
26+
27+
public func shouldLoadMore(bottomDistance offsetTrigger: OffsetTrigger = .relative(0.5),
28+
waitForHeightChange: HeightChangeConfig = .after(2),
29+
shouldLoadMore: @escaping (_ done: @escaping () -> ()) -> ()) -> some View {
30+
return DelegateHolder(offsetNotifier: ScrollOffsetNotifier(offsetTrigger: offsetTrigger,
31+
waitForHeightChange: waitForHeightChange,
32+
onNotify: {
33+
await withCheckedContinuation { continuation in
34+
shouldLoadMore {
35+
continuation.resume()
36+
}
37+
}
38+
}),
39+
content: self)
40+
}
1841
}
1942

2043
struct DelegateHolder<Content: View>: View {
@@ -38,15 +61,18 @@ class ScrollOffsetNotifier: NSObject, UIScrollViewDelegate, ObservableObject {
3861
private var canNotify = true
3962
private var trigger: OffsetTrigger
4063
private var oldContentHeight: Double = 0
64+
private var waitForHeightChange: HeightChangeConfig
65+
private var isTimerRunning = false
4166
weak var scrollView: UIScrollView? {
4267
didSet {
4368
scrollView?.addObserver(self, forKeyPath: "contentSize", context: nil)
4469
}
4570
}
4671

47-
init(offsetTrigger: OffsetTrigger, onNotify: @escaping () async -> ()) {
72+
init(offsetTrigger: OffsetTrigger, waitForHeightChange: HeightChangeConfig, onNotify: @escaping () async -> ()) {
4873
self.trigger = offsetTrigger
4974
self.onNotify = onNotify
75+
self.waitForHeightChange = waitForHeightChange
5076
}
5177

5278
deinit {
@@ -73,14 +99,32 @@ class ScrollOffsetNotifier: NSObject, UIScrollViewDelegate, ObservableObject {
7399
}
74100

75101
let bottomOffset = (scrollView.contentSize.height + scrollView.contentInset.bottom) - (scrollView.contentOffset.y + scrollView.visibleSize.height)
102+
var heightChanged = false
103+
104+
switch waitForHeightChange {
105+
case .always:
106+
heightChanged = oldContentHeight != scrollView.contentSize.height
107+
case .after(let timeInterval):
108+
heightChanged = oldContentHeight != scrollView.contentSize.height
109+
if !isTimerRunning {
110+
isTimerRunning = true
111+
DispatchQueue.main.asyncAfter(deadline: .now() + timeInterval) {
112+
self.oldContentHeight = 0
113+
self.isTimerRunning = false
114+
}
115+
}
116+
case .never:
117+
heightChanged = true
118+
}
76119

77120
Task { @MainActor in
78-
if bottomOffset < triggerHeight, canNotify {
121+
guard canNotify else { return }
122+
if bottomOffset < triggerHeight, heightChanged {
79123
oldContentHeight = scrollView.contentSize.height
80124
canNotify = false
81125
await onNotify()
126+
canNotify = true
82127
}
83-
canNotify = bottomOffset >= triggerHeight || oldContentHeight != scrollView.contentSize.height
84128
}
85129
}
86130
}

0 commit comments

Comments
 (0)