[英]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()
}
}
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.