In SwiftUI, where are the control events, i.e. scrollViewDidScroll to detect the bottom of list data
Plenty of features are missing from SwiftUI - it doesn't seem to be possible at the moment.
But here's a workaround.
TL;DR skip directly at the bottom of the answer
An interesting finding whilst doing some comparisons between ScrollView
and List
:
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(1...100) { item in
Text("\(item)")
}
Rectangle()
.onAppear { print("Reached end of scroll view") }
}
}
}
I appended a Rectangle
at the end of 100 Text
items inside a ScrollView
, with a print
in onDidAppear
.
It fired when the ScrollView
appeared, even if it showed the first 20 items.
All views inside a Scrollview are rendered immediately, even if they are offscreen.
I tried the same with List
, and the behaviour is different.
struct ContentView: View {
var body: some View {
List {
ForEach(1...100) { item in
Text("\(item)")
}
Rectangle()
.onAppear { print("Reached end of scroll view") }
}
}
}
The print
gets executed only when the bottom of the List
is reached!
So this is a temporary solution, until SwiftUI API gets better.
Use a
List
and place a "fake" view at the end of it, and put fetching logic insideonAppear { }
You can to check that the latest element is appeared inside onAppear.
struct ContentView: View {
@State var items = Array(1...30)
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text("\(item)")
.onAppear {
if let last == self.items.last {
print("last item")
self.items += last+1...last+30
}
}
}
}
}
}
In case you need more precise info on how for the scrollView or list has been scrolled, you could use the following extension as a workaround:
extension View {
func onFrameChange(_ frameHandler: @escaping (CGRect)->(),
enabled isEnabled: Bool = true) -> some View {
guard isEnabled else { return AnyView(self) }
return AnyView(self.background(GeometryReader { (geometry: GeometryProxy) in
Color.clear.beforeReturn {
frameHandler(geometry.frame(in: .global))
}
}))
}
private func beforeReturn(_ onBeforeReturn: ()->()) -> Self {
onBeforeReturn()
return self
}
}
The way you can leverage the changed frame like this:
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(0..<100) { number in
Text("\(number)").onFrameChange({ (frame) in
print("Origin is now \(frame.origin)")
}, enabled: number == 0)
}
}
}
}
The onFrameChange
closure will be called while scrolling. Using a different color than clear might result in better performance.
edit: I've improved the code a little bit by getting the frame outside of the beforeReturn
closure. This helps in the cases where the geometryProxy is not available within that closure.