如何构建类似ScrollViewReader/GeometryReader的代理并实现moveToTop功能?
实现CustomReader + CustomStack的视图置顶功能
问题背景
GeometryReader和ScrollViewReader的代理机制逻辑不透明,现有基于CustomReader和CustomStack Layout的垂直排列视图,需要实现调用moveToTop方法时,将指定ID的子视图移至顶部。
解决方案
核心思路是通过代理传递置顶目标ID,在Layout的布局逻辑中重新排序子视图,将目标视图放在最前列。以下是完整修改后的代码:
import SwiftUI struct ContentView: View { var body: some View { CustomReader { proxy in CustomStack { ForEach(0..<10) { index in Text("Sample \(index)") .customId(index) } } .background(.red) .onAppear { proxy.moveToTop(id: 5) } } .padding() } } // 重构CustomProxy为ObservableObject,方便跨视图同步状态 class CustomProxy: ObservableObject, Equatable { static func == (lhs: CustomProxy, rhs: CustomProxy) -> Bool { lhs.size == rhs.size && lhs.targetId == rhs.targetId } @Published var size: CGSize @Published var targetId: (any Hashable)? // 存储需要置顶的视图ID init(size: CGSize = .zero) { self.size = size self.targetId = nil } // 对外暴露的置顶方法 func moveToTop<ID: Hashable>(id: ID) { targetId = id } } // 修改CustomReader使用@StateObject管理代理,通过EnvironmentObject传递 struct CustomReader<Content: View>: View { var content: (CustomProxy) -> Content @StateObject private var proxy = CustomProxy() init(content: @escaping (CustomProxy) -> Content) { self.content = content } var body: some View { content(proxy) .environmentObject(proxy) } } // CustomStack通过EnvironmentObject获取代理,传递给Layout struct CustomStack<Content: View>: View { var content: () -> Content @EnvironmentObject private var proxy: CustomProxy init(content: @escaping () -> Content) { self.content = content } var body: some View { CustomStackLayout(proxy: proxy) { content() } .frame( maxWidth: proxy.size.width == 0 ? .infinity : proxy.size.width, maxHeight: proxy.size.height == 0 ? .infinity : proxy.size.height ) } } // 核心:在Layout中根据targetId重新排序子视图 struct CustomStackLayout: Layout { @ObservedObject var proxy: CustomProxy func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { proposal.replacingUnspecifiedDimensions() } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { var containerRect: CGRect = CGRect(origin: bounds.origin, size: .zero) var yPos: CGFloat = 0 // 根据targetId排序子视图 let targetId = proxy.targetId var sortedSubviews: Subviews if let targetId = targetId { // 找出目标视图 let targetSubview = subviews.first { subview in subview[CustomIDLayoutValueKey.self] == targetId } // 构造排序后的数组:目标视图在前,其余保持原顺序 sortedSubviews = [] if let target = targetSubview { sortedSubviews.append(target) sortedSubviews.append(contentsOf: subviews.filter { $0.id != target.id }) } else { sortedSubviews = subviews } // 重置targetId,避免重复触发置顶 proxy.targetId = nil } else { sortedSubviews = subviews } // 按新顺序放置子视图 for subview in sortedSubviews { let subviewSize = subview.sizeThatFits(proposal) let subviewPoint = CGPoint(x: bounds.minX, y: bounds.minY + yPos) subview.place(at: subviewPoint, proposal: proposal) yPos += subviewSize.height containerRect.size.width = max(containerRect.size.width, subviewSize.width) containerRect.size.height += subviewSize.height } proxy.size = containerRect.size } } // 保持原有ID标记逻辑 struct CustomIDLayoutValueKey: LayoutValueKey { static let defaultValue: (any Hashable)? = nil } extension View { func customId(_ value: (any Hashable)?) -> some View { layoutValue(key: CustomIDLayoutValueKey.self, value: value) } } #Preview { ContentView() }
关键改动说明
- CustomProxy重构:从struct改为
ObservableObject,通过@Published属性自动同步状态,避免值类型的副本修改问题。 - 代理传递优化:使用
EnvironmentObject替代自定义环境键,简化跨视图的状态传递逻辑。 - Layout排序逻辑:在
placeSubviews中根据targetId筛选目标视图,将其放在数组最前列,其余视图保持原顺序,完成置顶效果。 - 状态重置:置顶完成后清空
targetId,防止重复触发布局排序。
内容的提问来源于stack exchange,提问作者Mark
相关产品推荐
相关产品推荐

