You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI:如何在所有子视图完成测量时触发事件(GeometryReader、ViewPreferences)

解决SwiftUI父视图收集所有子视图尺寸并触发布局的问题

你的核心问题在于当前的SizePreferenceKey只能传递单个视图尺寸,无法收集所有子视图的尺寸数据,且使用Continuations不符合SwiftUI响应式范式,容易引发续体泄漏。下面是适配SwiftUI生态的正确实现方案:

1. 重构PreferenceKey,支持多视图尺寸收集

修改PreferenceKey为存储带唯一标识的尺寸字典,确保每个子视图的尺寸能被准确区分并合并:

struct ShapeSizePreferenceKey: PreferenceKey {
    static var defaultValue: [UUID: CGSize] = [:]
    
    static func reduce(value: inout [UUID: CGSize], nextValue: () -> [UUID: CGSize]) {
        // 合并新尺寸,重复ID用新值覆盖(适配视图重绘时的尺寸更新)
        value.merge(nextValue()) { _, new in new }
    }
}

2. 更新SizeModifier,关联子视图唯一ID

给Modifier添加shapeId参数,让每个子视图的尺寸与自身唯一标识绑定:

struct ShapeSizeModifier: ViewModifier {
    let shapeId: UUID
    
    private var sizeView: some View {
        GeometryReader { geometry in
            Color.clear
                .preference(
                    key: ShapeSizePreferenceKey.self,
                    value: [shapeId: geometry.size]
                )
        }
    }
    
    func body(content: Content) -> some View {
        content.background(sizeView)
    }
}

// 扩展View,简化调用
extension View {
    func trackShapeSize(shapeId: UUID) -> some View {
        modifier(ShapeSizeModifier(shapeId: shapeId))
    }
}

3. 父视图Board中收集尺寸并触发布局算法

在父视图中维护尺寸状态,当所有子视图尺寸收集完成后,自动执行布局逻辑:

struct BoardView: View {
    // 假设你的Shape数据模型实现Identifiable协议
    let shapes: [ShapeModel]
    @State private var shapeSizes: [UUID: CGSize] = [:]
    @State private var layoutFrames: [UUID: CGRect] = [:] // 存储计算后的布局位置
    
    var body: some View {
        ZStack(alignment: .topLeading) {
            ForEach(shapes) { shape in
                ShapeView(model: shape)
                    .trackShapeSize(shapeId: shape.id)
                    .frame(
                        width: layoutFrames[shape.id]?.width,
                        height: layoutFrames[shape.id]?.height
                    )
                    .position(
                        x: layoutFrames[shape.id]?.midX ?? 0,
                        y: layoutFrames[shape.id]?.midY ?? 0
                    )
            }
        }
        .onPreferenceChange(ShapeSizePreferenceKey.self) { newSizes in
            // 更新已收集的尺寸集合
            shapeSizes.merge(newSizes) { _, new in new }
            
            // 校验是否所有子视图尺寸都已收集完成
            guard shapeSizes.count == shapes.count else { return }
            
            // 执行你的最优填充布局算法
            let calculatedFrames = runLayoutAlgorithm(
                shapes: shapes,
                sizes: shapeSizes,
                availableSize: UIScreen.main.bounds.size
            )
            
            // 更新布局位置,触发视图重排
            layoutFrames = calculatedFrames
        }
    }
    
    // 示例布局算法,替换为你选定的最优填充逻辑
    private func runLayoutAlgorithm(shapes: [ShapeModel], sizes: [UUID: CGSize], availableSize: CGSize) -> [UUID: CGRect] {
        var frames: [UUID: CGRect] = [:]
        var currentX: CGFloat = 0
        var currentY: CGFloat = 0
        
        for shape in shapes {
            guard let size = sizes[shape.id] else { continue }
            
            // 简单流式布局:超出父视图宽度则换行
            if currentX + size.width > availableSize.width {
                currentX = 0
                currentY += size.height
            }
            
            frames[shape.id] = CGRect(x: currentX, y: currentY, width: size.width, height: size.height)
            currentX += size.width
        }
        
        return frames
    }
}

// 示例Shape数据模型与视图
struct ShapeModel: Identifiable {
    let id: UUID
    let content: String // 决定视图尺寸的内部内容
}

struct ShapeView: View {
    let model: ShapeModel
    
    var body: some View {
        Text(model.content)
            .padding()
            .background(Color.blue.opacity(0.2))
            .cornerRadius(8)
    }
}

为何放弃Continuations?

Continuations属于命令式异步编程模型,与SwiftUI的响应式生命周期不兼容。视图重绘、销毁时若未正确调用resume,会直接导致续体泄漏。而上述方案完全贴合SwiftUI响应式范式,自动处理视图更新与生命周期,无内存泄漏风险。

注意事项

  • 若子视图尺寸可能动态变化(如内容更新),merge方法会自动更新对应ID的尺寸,触发布局算法重新计算。
  • 确保每个子视图拥有唯一ID(通过Identifiable协议实现),避免尺寸数据混淆。
  • 布局算法中的availableSize可替换为父视图实际可用空间,比如用GeometryReader获取父视图尺寸而非直接使用屏幕尺寸。

内容的提问来源于stack exchange,提问作者Peter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 23:07:44