获取 SwiftUI ScrollView 的当前滚动位置

Mof*_*waw 17 ios swift swiftui xcode12

使用 new ScrolLViewReader,似乎可以以编程方式设置滚动偏移。

但我想知道是否也可以获取当前的滚动位置?

似乎ScrollViewProxy唯一附带的scrollTo方法,允许我们设置偏移量。

谢谢!

Asp*_*eri 27

可以阅读它和之前。这是基于视图首选项的解决方案。

struct DemoScrollViewOffsetView: View {
    @State private var offset = CGFloat.zero
    var body: some View {
        ScrollView {
            VStack {
                ForEach(0..<100) { i in
                    Text("Item \(i)").padding()
                }
            }.background(GeometryReader {
                Color.clear.preference(key: ViewOffsetKey.self,
                    value: -$0.frame(in: .named("scroll")).origin.y)
            })
            .onPreferenceChange(ViewOffsetKey.self) { print("offset >> \($0)") }
        }.coordinateSpace(name: "scroll")
    }
}

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

  • @Asperi - 如果我将“DemoScrollViewOffsetView”包含在“NavigationView{}”中,则在运行模拟器时,我会在调试窗口中收到以下警告消息:“绑定首选项 ViewOffsetKey 尝试每帧更新多次。” 是什么导致了警告/错误以及如何解决? (4认同)
  • 一句警告。如果在同事寻求帮助后,您在代码库中发现了这一点,为什么他们的 ScrollView 严重滞后:请谨慎使用。看看阿德里安的答案给你的想法:改变状态通常会导致严重的身体重新评估。对于 ScrollView 内的复杂视图,Xcodes SwiftUI Profiler 显示数千级的主体评估 - 对于 Scrollview 内包含 3 次的视图。我花了两天时间调试。 (4认同)
  • 可以设置吗? (2认同)

Mof*_*waw 23

我找到了一个没有使用的版本PreferenceKey。这个想法很简单 - 通过Color从返回GeometryReader,我们可以scrollOffset直接在背景修饰符内设置。

struct DemoScrollViewOffsetView: View {
    @State private var offset = CGFloat.zero
    var body: some View {
        ScrollView {
            VStack {
                ForEach(0..<100) { i in
                    Text("Item \(i)").padding()
                }
            }.background(GeometryReader { proxy -> Color in
                DispatchQueue.main.async {
                    offset = -proxy.frame(in: .named("scroll")).origin.y
                }
                return Color.clear
            })
        }.coordinateSpace(name: "scroll")
    }
}
Run Code Online (Sandbox Code Playgroud)


小智 5

我有类似的需求,但用List而不是ScrollView,并且想知道列表中的项目是否可见(List预加载视图尚不可见,因此onAppear()/onDisappear()不合适)。

经过一番“美化”后,我最终得到了这样的用法:

struct ContentView: View {
    var body: some View {
        GeometryReader { geometry in
            List(0..<100) { i in
                Text("Item \(i)")
                    .onItemFrameChanged(listGeometry: geometry) { (frame: CGRect?) in
                        print("rect of item \(i): \(String(describing: frame)))")
                    }
            }
            .trackListFrame()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

由这个 Swift 包支持: https: //github.com/Ceylo/ListItemTracking


Adr*_*ien 5

.onPreferenceChange(ViewOffsetKey.self) { print("offset >> \($0)") } 最流行的答案(@Asperi's)有一个限制:滚动偏移量可以在方便根据该偏移量触发事件的函数中使用 。但是,如果 的内容ScrollView取决于此偏移量(例如,如果必须显示它)怎么办?所以我们需要这个函数来更新@State. 那么问题是每次偏移量发生变化时,都会@State更新并重新评估主体。这会导致显示缓慢。

我们可以将 的内容ScrollView直接包装在 中GeometryReader,以便该内容可以直接依赖于其位置(不使用 aState甚至 a PreferenceKey)。

GeometryReader { geometry in
   content(geometry.frame(in: .named(spaceName)).origin)
}
Run Code Online (Sandbox Code Playgroud)

哪里content(CGPoint) -> some View

我们可以利用这一点来观察偏移量何时停止更新,并重现didEndDraggingUIScrollView 的行为

GeometryReader { geometry in
   content(geometry.frame(in: .named(spaceName)).origin)
      .onChange(of: geometry.frame(in: .named(spaceName)).origin, 
                perform: offsetObserver.send)
      .onReceive(offsetObserver.debounce(for: 0.2, 
                 scheduler: DispatchQueue.main), 
                 perform: didEndScrolling)
}
Run Code Online (Sandbox Code Playgroud)

在哪里offsetObserver = PassthroughSubject<CGPoint, Never>()

最后,这给出:

struct _ScrollViewWithOffset<Content: View>: View {
    
    private let axis: Axis.Set
    private let content: (CGPoint) -> Content
    private let didEndScrolling: (CGPoint) -> Void
    private let offsetObserver = PassthroughSubject<CGPoint, Never>()
    private let spaceName = "scrollView"
    
    init(axis: Axis.Set = .vertical,
         content: @escaping (CGPoint) -> Content,
         didEndScrolling: @escaping (CGPoint) -> Void = { _ in }) {
        self.axis = axis
        self.content = content
        self.didEndScrolling = didEndScrolling
    }
    
    var body: some View {
        ScrollView(axis) {
            GeometryReader { geometry in
                content(geometry.frame(in: .named(spaceName)).origin)
                    .onChange(of: geometry.frame(in: .named(spaceName)).origin, perform: offsetObserver.send)
                    .onReceive(offsetObserver.debounce(for: 0.2, scheduler: DispatchQueue.main), perform: didEndScrolling)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
            }
        }
        .coordinateSpace(name: spaceName)
    }
}
Run Code Online (Sandbox Code Playgroud)

注意:我看到的唯一问题是 GeometryReader 获取所有可用的宽度和高度。这并不总是可取的(特别是对于水平的ScrollView)。然后,必须确定内容的大小以将其反映在 上ScrollView

struct ScrollViewWithOffset<Content: View>: View {
    @State private var height: CGFloat?
    @State private var width: CGFloat?
    let axis: Axis.Set
    let content: (CGPoint) -> Content
    let didEndScrolling: (CGPoint) -> Void
    
    var body: some View {
        _ScrollViewWithOffset(axis: axis) { offset in
            content(offset)
                .fixedSize()
                .overlay(GeometryReader { geo in
                    Color.clear
                        .onAppear {
                            height = geo.size.height
                            width = geo.size.width
                        }
                })
        } didEndScrolling: {
            didEndScrolling($0)
        }
        .frame(width: axis == .vertical ? width : nil,
               height: axis == .horizontal ? height : nil)
    }
}
Run Code Online (Sandbox Code Playgroud)

这在大多数情况下都有效(除非内容大小发生变化,我认为这是不可取的)。最后你可以这样使用它:

struct ScrollViewWithOffsetForPreviews: View {
    @State private var cpt = 0
    let axis: Axis.Set
    var body: some View {
        NavigationView {
            ScrollViewWithOffset(axis: axis) { offset in
                VStack {
                    Color.pink
                        .frame(width: 100, height: 100)
                    Text(offset.x.description)
                    Text(offset.y.description)
                    Text(cpt.description)
                }
            } didEndScrolling: { _ in
                cpt += 1
            }
            .background(Color.mint)
            .navigationTitle(axis == .vertical ? "Vertical" : "Horizontal")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)