【问题标题】:SwiftUI ScrollView with content frame update closure带有内容框架更新关闭的 SwiftUI ScrollView
【发布时间】:2019-10-16 14:55:03
【问题描述】:

我想要一个ScrollView,您可以在其中了解用户滚动时内容框架的变化(类似于 UIKit 中的didScroll 委托UIScrollView)。

这样,您就可以根据滚动行为执行布局更改。

【问题讨论】:

    标签: ios macos scrollview swiftui contentoffset


    【解决方案1】:

    通过使用View Preferences作为在View Hierarchy中通知上游布局信息的方法,我设法为这个问题提供了一个很好的解决方案。

    关于View Preferences如何工作的详细解释,我建议阅读3 articles serieskontiki的主题

    对于我的解决方案,我实现了两个 ViewModifiers:一个使用 锚首选项 对其布局进行视图报告更改,第二个允许 View 处理对框架的更新对其子树的视图。

    为此,我们首先定义一个Struct,将可识别的帧信息携带到上游:

    /// Represents the `frame` of an identifiable view as an `Anchor`
    struct ViewFrame: Equatable {
    
        /// A given identifier for the View to faciliate processing
        /// of frame updates
        let viewId : String
    
    
        /// An `Anchor` representation of the View
        let frameAnchor: Anchor<CGRect>
    
        // Conformace to Equatable is required for supporting
        // view udpates via `PreferenceKey`
        static func == (lhs: ViewFrame, rhs: ViewFrame) -> Bool {
            // Since we can currently not compare `Anchor<CGRect>` values
            // without a Geometry reader, we return here `false` so that on
            // every change on bounds an update is issued.
            return false
        }
    }
    

    我们定义了一个符合PreferenceKey 协议的Struct 来保存视图树首选项的变化:

    /// A `PreferenceKey` to provide View frame updates in a View tree
    struct FramePreferenceKey: PreferenceKey {
        typealias Value = [ViewFrame] // The list of view frame changes in a View tree.
    
        static var defaultValue: [ViewFrame] = []
    
        /// When traversing the view tree, Swift UI will use this function to collect all view frame changes.
        static func reduce(value: inout [ViewFrame], nextValue: () -> [ViewFrame]) {
            value.append(contentsOf: nextValue())
        }
    }
    

    现在我们可以定义我提到的ViewModifiers

    对其布局进行视图报告更改

    这只是向 View 添加了一个 transformAnchorPreference 修饰符,它使用一个处理程序简单地构造一个具有当前帧 Anchor 值的 ViewFrame 实例并将其附加到 FramePreferenceKey 的当前值:

    /// Adds an Anchor preference to notify of frame changes
    struct ProvideFrameChanges: ViewModifier {
        var viewId : String
    
        func body(content: Content) -> some View {
            content
                .transformAnchorPreference(key: FramePreferenceKey.self, value: .bounds) {
                    $0.append(ViewFrame(viewId: self.viewId, frameAnchor: $1))
                }
        }
    }
    
    extension View {
    
        /// Adds an Anchor preference to notify of frame changes
        /// - Parameter viewId: A `String` identifying the View
        func provideFrameChanges(viewId : String) -> some View {
            ModifiedContent(content: self, modifier: ProvideFrameChanges(viewId: viewId))
        }
    }
    

    为视图提供更新处理程序,以便在其子树上更改帧:

    这会在视图中添加onPreferenceChange 修饰符,其中帧锚点更改列表将转换为视图坐标空间上的帧(CGRect),并报告为由视图 ID 键控的帧更新字典:

    typealias ViewTreeFrameChanges = [String : CGRect]
    
    /// Provides a block to handle internal View tree frame changes
    /// for views using the `ProvideFrameChanges` in own coordinate space.
    struct HandleViewTreeFrameChanges: ViewModifier {
        /// The handler to process Frame changes on this views subtree.
        /// `ViewTreeFrameChanges` is a dictionary where keys are string view ids
        /// and values are the updated view frame (`CGRect`)
        var handler : (ViewTreeFrameChanges)->Void
    
        func body(content: Content) -> some View {
            GeometryReader { contentGeometry in
                content
                    .onPreferenceChange(FramePreferenceKey.self) {
                        self._updateViewTreeLayoutChanges($0, in: contentGeometry)
                    }
            }
        }
    
        private func _updateViewTreeLayoutChanges(_ changes : [ViewFrame], in geometry : GeometryProxy) {
            let pairs = changes.map({ ($0.viewId, geometry[$0.frameAnchor]) })
            handler(Dictionary(uniqueKeysWithValues: pairs))
        }
    }
    
    extension View {
        /// Adds an Anchor preference to notify of frame changes
        /// - Parameter viewId: A `String` identifying the View
        func handleViewTreeFrameChanges(_ handler : @escaping (ViewTreeFrameChanges)->Void) -> some View {
            ModifiedContent(content: self, modifier: HandleViewTreeFrameChanges(handler: handler))
        }
    }
    

    让我们使用它:

    我会举例说明用法:

    在这里,我将收到ScrollView 内的标题视图 框架更改的通知。由于这个Header ViewScrollView内容的顶部,所以在frame origin上报告的frame变化等同于ScrollViewcontentOffset变化

    enum TestEnum : String, CaseIterable, Identifiable {
        case one, two, three, four, five, six, seven, eight, nine, ten
    
        var id: String {
            rawValue
        }
    }
    
    struct TestView: View {
        private let _listHeaderViewId = "testView_ListHeader"
    
        var body: some View {
            ScrollView {
                // Header View
                Text("This is some Header")
                    .provideFrameChanges(viewId: self._listHeaderViewId)
    
                // List of test values
                ForEach(TestEnum.allCases) {
                    Text($0.rawValue)
                        .padding(60)
                }
            }
                .handleViewTreeFrameChanges {
                    self._updateViewTreeLayoutChanges($0)
                }
        }
    
        private func _updateViewTreeLayoutChanges(_ changes : ViewTreeFrameChanges) {
            print(changes)
        }
    }
    

    【讨论】:

    • 太棒了。我有一个问题:如果多个视图有provideFrameChangeshandleViewTreeFrameChanges 回调如何识别变化源?
    • @LiangWang,抱歉回复晚了。基本上,在回调中传递的 ViewFrame 结构有一个 viewId 这是一个 String 你可以设置为任何你想要的。
    【解决方案2】:

    这个问题有一个优雅的解决方案,Soroush Khanlou 已经发布了它的要点,所以我不会复制粘贴它。你可以找到它here,是的......可惜它还不是框架的一部分!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-11-03
      • 1970-01-01
      • 2020-09-23
      • 2023-03-15
      • 2011-01-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多