简体   繁体   English

获取 SwiftUI ScrollView 的当前滚动 position

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

With the new ScrollViewReader , it seems possible to set the scroll offset programmatically.使用新的ScrollViewReader ,似乎可以以编程方式设置滚动偏移量。

But I was wondering if it is also possible to get the current scroll position?但我想知道是否也可以获得当前的滚动 position?

It seems like the ScrollViewProxy only comes with the scrollTo method, allowing us to set the offset.看起来ScrollViewProxy只带有scrollTo方法,允许我们设置偏移量。

Thanks!谢谢!

It was possible to read it and before.以前可以阅读它。 Here is a solution based on view preferences.这是一个基于视图偏好的解决方案。

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

backup 备份

I found a version without using PreferenceKey .我找到了一个不使用PreferenceKey的版本。 The idea is simple - by returning Color from GeometryReader , we can set scrollOffset directly inside background modifier.这个想法很简单——通过从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")
    }
}

I had a similar need but with List instead of ScrollView , and wanted to know wether items in the lists are visible or not ( List preloads views not yet visible, so onAppear() / onDisappear() are not suitable).我有类似的需求,但使用List而不是ScrollView ,并且想知道列表中的项目是否可见( List预加载视图尚不可见,因此onAppear() / onDisappear()不适合)。

After a bit of "beautification" I ended up with this usage:经过一番“美化”后,我最终得到了这种用法:

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

which is backed by this Swift package: https://github.com/Ceylo/ListItemTracking由 Swift package: https://github.com/Ceylo/ListItemTracking支持

The most popular answer (@Asperi's) has a limitation: The scroll offset can be used in a function .onPreferenceChange(ViewOffsetKey.self) { print("offset >> \($0)") } which is convenient for triggering an event based on that offset.最流行的答案(@Asperi's)有一个限制:滚动偏移量可用于 function .onPreferenceChange(ViewOffsetKey.self) { print("offset >> \($0)") }这便于触发基于事件在那个偏移量上。 But what if the content of the ScrollView depends on this offset (for example if it has to display it).但是如果ScrollView的内容取决于这个偏移量(例如,如果它必须显示它)怎么办。 So we need this function to update a @State .所以我们需要这个 function 来更新@State The problem then is that each time this offset changes, the @State is updated and the body is re-evaluated.那么问题是,每次这个偏移量发生变化时, @State都会更新并重新评估主体。 This causes a slow display.这会导致显示缓慢。

We could instead wrap the content of the ScrollView directly in the GeometryReader so that this content can depend on its position directly (without using a State or even a PreferenceKey ).我们可以将ScrollView的内容直接包装在GeometryReader中,这样该内容就可以直接依赖于它的 position(不使用State甚至是PreferenceKey )。

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

where content is (CGPoint) -> some View content在哪里(CGPoint) -> some View

We could take advantage of this to observe when the offset stops being updated, and reproduce the didEndDragging behavior of UIScrollView我们可以利用这一点来观察偏移何时停止更新,并重现 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)
}

where offsetObserver = PassthroughSubject<CGPoint, Never>()其中offsetObserver = PassthroughSubject<CGPoint, Never>()

In the end, this gives:最后,这给出了:

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

Note: the only problem I see is that the GeometryReader takes all the available width and height.注意:我看到的唯一问题是 GeometryReader 采用了所有可用的宽度和高度。 This is not always desirable (especially for a horizontal ScrollView ).这并不总是可取的(尤其是对于水平ScrollView )。 One must then determine the size of the content to reflect it on the 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)
    }
}

This will work in most cases (unless the content size changes, which I don't think is desirable).这在大多数情况下都有效(除非内容大小发生变化,我认为这是不可取的)。 And finally you can use it like that:最后你可以这样使用它:

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