Swiftui - Detect When Scrollview Has Finished Scrolling

SwiftUI - Detect when ScrollView has finished scrolling?

Here is a demo of possible approach - use publisher with changed scrolled content coordinates with debounce, so event reported only after coordinates stopped changing.

Tested with Xcode 12.1 / iOS 14.1

UPDATE: verified as worked with Xcode 13.3 / iOS 15.4

Note: you can play with debounce period to tune it for your needs.

demo

import Combine

struct ContentView: View {
let detector: CurrentValueSubject<CGFloat, Never>
let publisher: AnyPublisher<CGFloat, Never>

init() {
let detector = CurrentValueSubject<CGFloat, Never>(0)
self.publisher = detector
.debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
.dropFirst()
.eraseToAnyPublisher()
self.detector = detector
}

var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(0...100, id: \.self) { i in
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.green)
.overlay(Text("\(i)"))
}
}
.frame(maxWidth: .infinity)
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: .named("scroll")).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self) { detector.send($0) }
}.coordinateSpace(name: "scroll")
.onReceive(publisher) {
print("Stopped on: \($0)")
}
}
}

struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}

How do I detect when User has reached the bottom of the ScrollView?

Wrap your whole ScrollView in your ChildSizeReader, so you can get the height of the ScrollView itself.

Because the offset starts at zero at the top, when at the bottom of the scroll view the end isn't at the top of the screen, but rather the bottom. This difference is the height of the scroll view. This means the ScrollView starts at offset 0 and goes to total content height - scroll view height.

Code:

struct ContentView: View {
let spaceName = "scroll"

@State var wholeSize: CGSize = .zero
@State var scrollViewSize: CGSize = .zero

var body: some View {
ChildSizeReader(size: $wholeSize) {
ScrollView {
ChildSizeReader(size: $scrollViewSize) {
VStack {
ForEach(0..<100) { i in
Text("\(i)")
}
}
.background(
GeometryReader { proxy in
Color.clear.preference(
key: ViewOffsetKey.self,
value: -1 * proxy.frame(in: .named(spaceName)).origin.y
)
}
)
.onPreferenceChange(
ViewOffsetKey.self,
perform: { value in
print("offset: \(value)") // offset: 1270.3333333333333 when User has reached the bottom
print("height: \(scrollViewSize.height)") // height: 2033.3333333333333

if value >= scrollViewSize.height - wholeSize.height {
print("User has reached the bottom of the ScrollView.")
} else {
print("not reached.")
}
}
)
}
}
.coordinateSpace(name: spaceName)
}
.onChange(
of: scrollViewSize,
perform: { value in
print(value)
}
)
}
}

Note your already existing scrollViewSize variable is the content's size, not the scroll view's size.

Also notice that I changed the == to >= - this is so you don't have to be exactly at the height, can be over-scrolled where it rubber-bands back.

How to detect when a UIScrollView has finished scrolling

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self stoppedScrolling];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
[self stoppedScrolling];
}
}

- (void)stoppedScrolling {
// ...
}

In SwiftUI, where are the control events, i.e. scrollViewDidScroll to detect the bottom of list data

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
}
}
}
}
}
}

SwiftUI UIScrollView Detect when user scrolls near the bottom

I did this via finding out whether the object displayed is the last element in list, then by calling fetchMore function

    //EmployeeViewModel
class EmployeeViewModel: ObservableObject {
@Published var employees : [Employee] = []

func initialize() {
self.employees = [Employee(name: "A", id: "100"),
Employee(name: "B", id: "101"),
Employee(name: "C", id: "102"),
Employee(name: "D", id: "103"),
Employee(name: "E", id: "104"),
Employee(name: "F", id: "105"),
Employee(name: "G", id: "106"),
Employee(name: "H", id: "107"),
Employee(name: "I", id: "108"),
Employee(name: "J", id: "109"),
Employee(name: "K", id: "110"),
Employee(name: "L", id: "112"),
Employee(name: "M", id: "113"),
Employee(name: "N", id: "114"),
Employee(name: "O", id: "115"),
Employee(name: "P", id: "116"),
Employee(name: "Q", id: "117"),
Employee(name: "R", id: "118"),
Employee(name: "S", id: "119"),
Employee(name: "T", id: "120"),
Employee(name: "U", id: "121"),
Employee(name: "V", id: "122"),
Employee(name: "W", id: "123")]
}


//fetch more employees inside viewmodel
func fetchMoreEmployees(employee: Employee) {
if let index = self.employees.firstIndex(where: {$0.id == employee.id}) {
if index == self.employees.count - 1 {
print("Item: \(employee.name) - Reached bottom of list ")
} else if index == 0 {
print("Item: \(employee.name) - Reached top of list")
}
}
}
}

struct EmployeeView: View {
@ObservedObject var vm = EmployeeViewModel()

var body: some View {
NavigationView {
List {
ForEach(self.vm.employees) { employee in
EmployeeCellView(vm: self.vm, employee: employee)
}.listRowBackground(Color.white)
}.onAppear(perform: initialize)
.navigationBarTitle("Users", displayMode: .inline)
}
}

func initialize() {
self.vm.initialize()
}
}

struct EmployeeCellView: View {
@ObservedObject var vm: EmployeeViewModel
let employee: Employee

var body: some View {
Text("\(employee.name)").onAppear(perform: fetchMore)
}

func fetchMore() {
self.vm.fetchMoreEmployees(employee: self.employee)
}
}

Add this fetchMoreEmployees(employee: Employee) function inside your ViewModel and call this function in .onAppear() of your cell shown above.

How to detect scroll direction programmatically in SwiftUI ScrollView

You would use GeometryReader to get the global position of one in the views in the ScrollView to detect the scroll direction. The code below will print out the current midY position. Dependent on the +/- value you could display or hide other views.

struct ContentView: View {

var body: some View {
ScrollView{
GeometryReader { geometry in

Text("Top View \(geometry.frame(in: .global).midY)")
.frame(width: geometry.size.width, height: 50)
.background(Color.orange)
}

}.frame(minWidth: 0, idealWidth: 0, maxWidth: .infinity, minHeight: 0, idealHeight: 0, maxHeight: .infinity, alignment: .center)
}

}

SwiftUI: how to detect when List is scrolled?

I recommend to use simultaneousGesture modifier as in below:

List {
// ... your list items
}
.simultaneousGesture(DragGesture().onChanged({ _ in
// if keyboard is opened then hide it
}))

Update: verified with Xcode 13.3 / iOS 15.4 - for the use-case formulated by PO still works fine.



Related Topics



Leave a reply



Submit