通过新的ScrollViewReader
,似乎可以通过编程方式设置滚动偏移量。
但我想知道是否也可以 获取 当前滚动位置?
看起来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()
}
}
DemoScrollViewOffsetView
放入NavigationView{}
中,当在模拟器上运行时,在调试窗口中会收到以下警告信息:Bound preference ViewOffsetKey tried to update multiple times per frame.
这个警告/错误是什么原因造成的,如何解决? - Vee我发现了一种不使用 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")
}
}
.onPreferenceChange(ViewOffsetKey.self) { print("offset >> \($0)") }
这对于基于该偏移触发事件非常方便。
但是,如果ScrollView
的内容取决于此偏移量(例如,如果它必须显示它),那么我们需要此函数来更新@State
。
然后问题就是每次更改此偏移量时,都会更新@State
并重新评估主体。这会导致显示缓慢。ScrollView
的内容包装在GeometryReader
中,以使该内容可以直接依赖其位置(而不使用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")
}
}
}
我有类似的需求,但是需要使用 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 包支持的:https://github.com/Ceylo/ListItemTracking
我有点晚来参加派对,但今天我遇到了这个问题,所以我想回答一下。这个要点包含了能够满足你需求的代码。
https://gist.github.com/rsalesas/313e6aefc098f2b3357ae485da507fc4
ScrollView {
ScrollViewReader { proxy in
content()
}
.onScrolled { point in
print("Point: \(point)")
}
}
.trackScrolling()
它提供了扩展功能,以在滚动ScrollView时调用。首先在ScrollView上使用.trackScrolling,然后在其中放置一个ScrollViewReader。在ScrollViewReader上使用.onScrolled扩展来接收事件(一个参数,一个UnitPoint)。
您确实需要打开滚动跟踪,我找不到其他方法来做到这一点。为什么不支持这个...
通过查看以前的示例,您可以在不使用PreferenceKeys的情况下达到相同的结果。
import SwiftUI
struct LazyVScrollView<Content: View>: View {
@State private var rect: CGRect = .zero
private let id = UUID()
private let content: (CGRect) -> Content
init(content: @escaping (CGRect) -> Content) {
self.content = content
}
var body: some View {
ScrollView {
content(rect)
.background {
GeometryReader {
Color.clear
.onChange(of: $0.frame(in: .named(id))) { newValue in
rect = newValue
}
}
}
}
.coordinateSpace(name: id)
}
}