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