不喜欢使用****UIViewRepresentable
****的方式去实现一个UIScrollview然后获取偏移量。这一点都不SwiftUI...
可是目前SwiftUI中的ScrollView还没有类似的功能。😭苹果大大给点力啊!
思路:
上层容器由ScrollView承载,底层容器由任意View承载,ScrollView滑动时上层容器的位置相对于底层容器的位置即是偏移量。
所以我写了以下代码
var body: some View {
ZStack(alignment: .topLeading) {
HStack {
Spacer()
Text("底层")
.font(.title)
Spacer()
}
.background(Color.blue)
.zIndex(1)
ScrollView(.vertical) {
HStack {
Spacer()
Text("上层")
.background(Color.orange)
Spacer()
}
}
.zIndex(1000)
}
}
image.png
但是如何实时获取上层和底层的实时位置?
掘金上对于GeometryReader的一篇介绍文章。
机翻叫做几何读取器
@frozen public struct GeometryReader<Content> : View where Content : View {
public var content: (GeometryProxy) -> Content
@inlinable public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content)
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
public typealias Body = Never
}
public struct GeometryProxy {
/// The size of the container view.
public var size: CGSize { get }
/// Resolves the value of `anchor` to the container view.
public subscript<T>(anchor: Anchor<T>) -> T { get }
/// The safe area inset of the container view.
public var safeAreaInsets: EdgeInsets { get }
// 我们将用它,来获取控件的实时位置。
public func frame(in coordinateSpace: CoordinateSpace) -> CGRect
}
简单来说,GeometryReader也是View, 只不过这个view可以让你做更多事。
这样一来,我们替换掉刚刚的底层和上层视图,统统换为GeometryReader:
var body: some View {
// 底层
GeometryReader { outProxy in
ScrollView(.vertical) {
// 上层
GeometryReader { inProxy in
// 底层minY - 上层minY
Text("\(outProxy.frame(in: .global).minY - inProxy.frame(in: .global).minY)")
}
}
}
}
如果运行程序,Text的文本将会正确的显示出当前ScrollView的偏移量。
封装:
现在要把这个功能点集成进一个ScrollView中,方便我们在任何地方获取offset。
实现的ScrollView应该提供纵向和横向的offset。
通过绑定使外部View能够获取offset。
使用preference将子View的变化传递给父View。
//
// InspectingScrollView.swift
// Memory
//
// Created by Xu on 2020/12/22.
//
import SwiftUI
// MARK: PreferenceKey
/*
使用PreferenceKey沿View树向上传递:
子View->父View
*/
struct ScrollOffsetPreferenceKey: PreferenceKey{
typealias Value = [CGFloat]
static var defaultValue: [CGFloat] = [0]
static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
value.append(contentsOf: nextValue())
}
}
struct InspectingScrollView<Content>: View where Content: View {
/// 方向
let axes: Axis.Set
/// 指示器
let showsIndicators: Bool
/// 偏移量
@Binding var contentOffset: CGFloat
let content: Content
init(_ axes: Axis.Set = .vertical, showsIndicators: Bool = true, contentOffset: Binding<CGFloat>, @ViewBuilder content: ()-> Content) {
self.axes = axes
self.showsIndicators = showsIndicators
self._contentOffset = contentOffset
self.content = content()
}
var body: some View {
GeometryReader { outsideProxy in
ScrollView(self.axes, showsIndicators: self.showsIndicators, content: {
ZStack(alignment: self.axes == .vertical ? .top : .leading) {
GeometryReader { insideProxy in
Color.clear
.preference(
key: ScrollOffsetPreferenceKey.self,
value: [
calculateContentoffset(from: insideProxy,
outsideProxy: outsideProxy)
])
}
VStack {
self.content
}
}
})
.onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: { value in
self.contentOffset = value.first ?? 0
})
}
}
func calculateContentoffset(from insideProxy: GeometryProxy, outsideProxy: GeometryProxy)-> CGFloat {
/*
Notice: frame(in: CoordinateSpace)
Returns the container view's bounds rectangle, converted to a defined
coordinate space.
返回容器视图的边界矩形,将其转换为定义的坐标空间。
CoordinateSpace: 坐标空间
{
.global
.local
.named("自定义坐标空间")
}
*/
if axes == .vertical {
return outsideProxy.frame(in: .global).minY - insideProxy.frame(in: .global).minY
}else {
return outsideProxy.frame(in: .global).minX - insideProxy.frame(in: .global).minX
}
}
}
如何使用:
struct DemoView: View {
@State private var contentOffset: CGFloat = 0
var body: some View {
InspectingScrollView(.vertical, showsIndicators: false, contentOffset: $contentOffset) {
// some views
}
}
}
至此大功告成。
参考文章: