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 inside onAppear { }


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.

Tags:

Swift

Swiftui