繁体   English   中英

获取 SwiftUI ScrollView 的当前滚动 position

[英]Get the current scroll position of a SwiftUI ScrollView

使用新的ScrollViewReader ,似乎可以以编程方式设置滚动偏移量。

但我想知道是否也可以获得当前的滚动 position?

看起来ScrollViewProxy只带有scrollTo方法,允许我们设置偏移量。

谢谢!

以前可以阅读它。 这是一个基于视图偏好的解决方案。

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

备份

我找到了一个不使用PreferenceKey的版本。 这个想法很简单——通过从GeometryReader返回Color ,我们可以直接在背景修饰符内设置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")
    }
}

我有类似的需求,但使用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()
        }
    }
}

由 Swift package: https://github.com/Ceylo/ListItemTracking支持

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

我们可以将ScrollView的内容直接包装在GeometryReader中,这样该内容就可以直接依赖于它的 position(不使用State甚至是PreferenceKey )。

GeometryReader { geometry in
   content(geometry.frame(in: .named(spaceName)).origin)
}

content在哪里(CGPoint) -> some View

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

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

其中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)
    }
}

注意:我看到的唯一问题是 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)
    }
}

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM