如何正确检查SwiftUI View Tree 实现自定义List内容展平为Section数组
实现方案
核心思路
SwiftUI 视图为不透明类型,无法直接遍历解析视图树结构,因此我们采用官方提供的 PreferenceKey 机制,从子视图向上传递 Section 信息,自动合并所有层级的 Section 数据,最终得到平铺的 Section 数组。
具体实现步骤
1. 定义Section存储模型与偏好键
首先我们定义抹掉类型的 Section 存储结构,以及用来传递 Section 数组的偏好键:
import SwiftUI // 抹掉类型的Section存储结构 struct SectionItem: Identifiable { let id = UUID() let header: AnyView? let content: AnyView let footer: AnyView? } // 收集Section的偏好键 struct SectionPreferenceKey: PreferenceKey { static var defaultValue: [SectionItem] = [] static func reduce(value: inout [SectionItem], nextValue: () -> [SectionItem]) { // 自动合并所有子视图上报的Section value.append(contentsOf: nextValue()) } }
2. 扩展Section和普通View,添加上报逻辑
给原生 Section 添加扩展,让它自动上报自身的 header、content、footer 信息;同时给所有 View 添加扩展,非 Section 的普通视图会自动被包装成无 header/footer 的 Section 上报:
extension Section { func collectSection() -> some View { let item = SectionItem( header: AnyView(header), content: AnyView(content), footer: AnyView(footer) ) return self.preference(key: SectionPreferenceKey.self, value: [item]) } } extension View { func autoCollectSection() -> some View { // 优先匹配Section的collectSection方法,普通View则包装为Section上报 if let sec = self as? Section<EmptyView, some View, some View> { return AnyView(sec.collectSection()) } else { let item = SectionItem( header: nil, content: AnyView(self), footer: nil ) return AnyView(self.preference(key: SectionPreferenceKey.self, value: [item])) } } }
3. 实现MyList的Section收集逻辑
在 MyList 中通过偏好值监听获取全量平铺的 Section 数组,再基于该数组实现自定义列表布局:
struct MyList<Content: View>: View { private let content: Content @State private var sections: [SectionItem] = [] init(@ViewBuilder content: @escaping () -> Content) { // 递归给所有子视图添加自动收集修饰符,支持TupleView、ForEach、Group等容器 self.content = content().transformIfNeeded { $0.autoCollectSection() } } var body: some View { // 隐藏原视图仅用来收集偏好值,也可以直接渲染后叠加布局 content .hidden() .onPreferenceChange(SectionPreferenceKey.self) { newSections in sections = newSections } .overlay( // 以下为自定义列表布局示例,可根据需求修改 ScrollView { VStack(spacing: 8) { ForEach(sections) { section in if let header = section.header { header .font(.headline) .padding(.horizontal) } section.content .padding(.horizontal) if let footer = section.footer { footer .font(.caption) .foregroundColor(.secondary) .padding(.horizontal) } Divider() } } .padding(.vertical) } ) } } // 辅助扩展:递归处理所有容器类型的子视图 extension View { func transformIfNeeded(_ transform: (AnyView) -> some View) -> some View { let mirror = Mirror(reflecting: self) // 处理TupleView、ForEach等多子元素容器 if let children = mirror.descendant("children") as? [Any] { return AnyView(VStack { ForEach(children.indices, id: \.self) { idx in AnyView(children[idx]).transformIfNeeded(transform) } }) } else { return transform(AnyView(self)) } } }
注意事项
- 该方案基于SwiftUI官方
PreferenceKey实现,兼容性强,支持任意层级嵌套的ForEach、TupleView、Group等容器类型 - 普通非Section视图会自动被包装为无头部尾部的Section,无需额外处理
- 如果需要适配更多自定义容器,只需在
transformIfNeeded方法中添加对应容器的子视图遍历逻辑即可
内容的提问来源于stack exchange,提问作者Lorenzo Fiamingo
相关产品推荐
相关产品推荐

