带有内容框架更新功能的SwiftUI ScrollView [英] SwiftUI ScrollView with content frame update closure

查看:102
本文介绍了带有内容框架更新功能的SwiftUI ScrollView的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我希望有一个ScrollView,您可以在其中知道用户滚动时内容框架的变化(类似于UIKit UIScrollView中的didScroll委托).

I want to have a ScrollView where you can be aware of the content frame changes as the user scrolls (similar to didScroll delegate in UIKit UIScrollView).

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

With this, you can then perform layout changes based on the scroll behavior.

推荐答案

我设法通过使用 View Preferences 作为在Windows中通知上游布局信息的方法,为这个问题提供了一个不错的解决方案.视图层次结构.

I managed to come with a nice solution for this problem by making use of View Preferences as a method to notify layout information upstream in the View Hierarchy.

要详细了解查看首选项的工作方式,我建议阅读此 kontiki

For a very detail explanation of how View Preferences work, I will suggest reading this 3 articles series on the topic by kontiki

对于我的解决方案,我实现了两个ViewModifiers:一个使用 anchor偏好设置对视图的布局进行更改,而第二个允许View处理对框架的更新子树上的视图.

For my solution, I implemented two ViewModifiers: one to make a view report changes on its layout using anchor preferences, and the second to allow a View to handle updates to frames on views on its subtree.

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

To do this, we first define a Struct to carry the identifiable frame information upstream:

/// 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来保存视图树的首选项更改:

and we define a Struct conforming to PreferenceKey protocol to hold the view tree preference changes:

/// 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:

Now we can define the ViewModifiers I mentioned:

制作视图报告,以更改其布局:

这只是使用处理程序向视图添加transformAnchorPreference修饰符,该处理程序仅构造具有当前帧Anchor值的ViewFrame实例并将其附加到FramePreferenceKey的当前值:

This just adds a transformAnchorPreference modifier to the View with a handler that simply constructs a ViewFrame instance with current frame Anchor value and appends it to the current value of the 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键控的框架更新字典:

This adds a onPreferenceChange modifier to the View, where the list of frame Anchors changes are transformed into frames(CGRect) on the view's coordinate space and reported as a dictionary of frame updates keyed by the view ids:

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

让我们使用它:

我将通过一个示例来说明用法:

I will illustrate the usage with an example:

在这里,我将收到 Header View 框架更改在ScrollView内的通知.由于此 Header View 位于ScrollView内容的顶部,因此,报告的帧原点上的帧更改等同于ScrollView

Here I will get notifications of a Header View frame changes inside a ScrollView. Since this Header View is on the top of the ScrollView content, the reported frame changes on the frame origin are equivalent to the contentOffset changes of the ScrollView

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

这篇关于带有内容框架更新功能的SwiftUI ScrollView的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆