带有内容框架更新关闭的 SwiftUI ScrollView
Posted
技术标签:
【中文标题】带有内容框架更新关闭的 SwiftUI ScrollView【英文标题】:SwiftUI ScrollView with content frame update closure 【发布时间】:2019-10-16 14:55:03 【问题描述】:我想要一个ScrollView
,您可以在其中了解用户滚动时内容框架的变化(类似于 UIKit 中的didScroll
委托UIScrollView
)。
这样,您就可以根据滚动行为执行布局更改。
【问题讨论】:
【参考方案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 View在ScrollView
内容的顶部,所以在frame origin上报告的frame变化等同于ScrollView
的contentOffset
变化
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)
【讨论】:
太棒了。我有一个问题:如果多个视图有provideFrameChanges
,handleViewTreeFrameChanges
回调如何识别变化源?
@LiangWang,抱歉回复晚了。基本上,在回调中传递的 ViewFrame
结构有一个 viewId
这是一个 String
你可以设置为任何你想要的。【参考方案2】:
这个问题有一个优雅的解决方案,Soroush Khanlou 已经发布了它的要点,所以我不会复制粘贴它。你可以找到它here,是的......可惜它还不是框架的一部分!
【讨论】:
以上是关于带有内容框架更新关闭的 SwiftUI ScrollView的主要内容,如果未能解决你的问题,请参考以下文章
带有 Xcode 11 beta 7 的 SwiftUI 未更新 List / ForEach 的内容
已发布/观察到的 var 在视图 swiftui 中未更新,带有调用函数