SwiftUI如何獲取ScrollView的contentOffset

不喜歡使用****UIViewRepresentable****的方式去實(shí)現(xiàn)一個(gè)UIScrollview然后獲取偏移量。這一點(diǎn)都不SwiftUI...

可是目前SwiftUI中的ScrollView還沒有類似的功能。??蘋果大大給點(diǎn)力?。?/strong>

思路:

上層容器由ScrollView承載,底層容器由任意View承載,ScrollView滑動(dòng)時(shí)上層容器的位置相對(duì)于底層容器的位置即是偏移量。

所以我寫了以下代碼

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

但是如何實(shí)時(shí)獲取上層和底層的實(shí)時(shí)位置?

SwiftUI:GeometryReader

掘金上對(duì)于GeometryReader的一篇介紹文章。

機(jī)翻叫做幾何讀取器

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

    // 我們將用它,來獲取控件的實(shí)時(shí)位置。
    public func frame(in coordinateSpace: CoordinateSpace) -> CGRect
} 

簡(jiǎn)單來說,GeometryReader也是View, 只不過這個(gè)view可以讓你做更多事。

這樣一來,我們替換掉剛剛的底層和上層視圖,統(tǒng)統(tǒng)換為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)")
        }
    }
  }
}

如果運(yùn)行程序,Text的文本將會(huì)正確的顯示出當(dāng)前ScrollView的偏移量。

封裝:

現(xiàn)在要把這個(gè)功能點(diǎn)集成進(jìn)一個(gè)ScrollView中,方便我們?cè)谌魏蔚胤将@取offset。

  1. 實(shí)現(xiàn)的ScrollView應(yīng)該提供縱向和橫向的offset。

  2. 通過綁定使外部View能夠獲取offset。

  3. 使用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.
         返回容器視圖的邊界矩形,將其轉(zhuǎn)換為定義的坐標(biāo)空間。
         
         CoordinateSpace: 坐標(biāo)空間
         {
            .global
            .local
            .named("自定義坐標(biāo)空間")
         }
         */
        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
    }
  }
}

至此大功告成。

參考文章

  1. https://medium.com/@maxnatchanon/swiftui-how-to-get-content-offset-from-scrollview-5ce1f84603ec

  2. SwiftUI編程思想

  3. https://juejin.cn/post/6844904004133060615

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容