带有内容框架更新功能的SwiftUI ScrollView [英] SwiftUI ScrollView with content frame update closure
问题描述
我希望有一个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:
在这里,我将收到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屋!