如何检测用户何时到达 ScrollView 的底部?

shi*_*shi 21 ios swift swiftui

我使用以下代码作为参考:

我认为它非常接近。似乎可以通过使用origin.maxY而不是 来解决origin.y,但origin.maxY似乎没有提供GeometryReader(严格来说CGRect:)。

如何检测用户何时到达 ScrollView 的底部?

import SwiftUI

struct ContentView: View {
  let spaceName = "scroll"

  @State var scrollViewSize: CGSize = .zero

  var body: some View {
    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 {
              print("User has reached the bottom of the ScrollView.")
            } else {
              print("not reached.")
            }
          }
        )
      }
    }
    .coordinateSpace(name: spaceName)
    .onChange(
      of: scrollViewSize,
      perform: { value in
        print(value)
      }
    )
  }
}

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

struct ChildSizeReader<Content: View>: View {
  @Binding var size: CGSize

  let content: () -> Content
  var body: some View {
    ZStack {
      content().background(
        GeometryReader { proxy in
          Color.clear.preference(
            key: SizePreferenceKey.self,
            value: proxy.size
          )
        }
      )
    }
    .onPreferenceChange(SizePreferenceKey.self) { preferences in
      self.size = preferences
    }
  }
}

struct SizePreferenceKey: PreferenceKey {
  typealias Value = CGSize
  static var defaultValue: Value = .zero

  static func reduce(value _: inout Value, nextValue: () -> Value) {
    _ = nextValue()
  }
}
Run Code Online (Sandbox Code Playgroud)

Geo*_*e_E 20

将你的整体包裹ScrollView在你的 中ChildSizeReader,这样你就可以获得它本身的高度ScrollView

因为偏移量从顶部的零开始,所以当在滚动视图的底部时,结束位置不是在屏幕的顶部,而是在底部。这个差异就是滚动视图的高度。这意味着ScrollView从 offset 开始0并转到total content height - scroll view height

代码:

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)
            }
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,您已经存在的scrollViewSize变量是内容的大小,而不是滚动视图的大小。

另请注意,我将 更改==>=- 这样您就不必完全处于高度,可以在橡皮筋向后滚动的地方过度滚动。


Cou*_*per 16

实现这一目标的一种非常简单的方法如下:

struct ContentView: View {
    let array: [String] = (0...100).map { "\($0)" }
    let onEndOfList: () -> Void
    var body: some View {
        List {
            ForEach(array, id: \.self) { element in
                Text(element)
            }
            Color.clear
                .frame(width: 0, height: 0, alignment: .bottom)
                .onAppear {
                    onEndOfList()
                }
        }
    }
}

Run Code Online (Sandbox Code Playgroud)

不用补充,这个基本思想可以增强并应用于 ScrollView 或任何其他可滚动视图。


小智 6

I recently found a somewhat simple method to do this:

ScrollView {
    LazyVStack {
            ForEach(Array(items.enumerated), id: \.element) { index, i in
                    Rectangle()
                        .frame(width: 200, height: 100)
                        .foregroundColor(.green)
                        .overlay(Text("\(i)"))
                        .onAppear {
                           let isLast = index == items.count
                           // Do whatever you need checking isLast
                        }

                }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)