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

SwiftUI:使用Layout协议实现布局变更动画(iOS16/macOS13)

解决SwiftUI自定义Layout的动画警告问题

问题分析

你当前使用.animation(.default)触发布局动画,但该写法已被弃用,必须指定value参数让SwiftUI感知动画触发的变化源。直接在placeSubviews中使用withAnimation无效,因为布局方法是SwiftUI布局系统的内部执行环节,无法直接通过该方式触发动画。

解决方案1:监听父容器尺寸变化(最简便)

如果你的布局变化是由父容器尺寸改变(比如屏幕旋转、窗口调整)引起的,直接用GeometryReader获取父容器尺寸作为动画的value:

struct ContentView: View {
    var body: some View {
        GeometryReader { proxy in
            FlowLayout {
                ForEach(0..<5) { _ in
                    Group {
                        Text("Hello")
                            .font(.largeTitle)
                        Text("World")
                            .font(.title)
                        Text("!!!")
                            .font(.title3)
                    }
                    .border(Color.red)
                }
            }
            .animation(.default, value: proxy.size)
        }
    }
}

解决方案2:捕获Layout自身尺寸变化(通用场景)

如果布局变化源于子视图内容变化(比如文本长度改变),可以通过PreferenceKey捕获Layout的实际尺寸,以此作为动画触发源:

  1. 定义PreferenceKey捕获尺寸
struct LayoutSizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}
  1. 修改FlowLayout传递尺寸偏好
struct FlowLayout: Layout {
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let sizes = subviews.map { $0.sizeThatFits(.unspecified) }

        var totalHeight: CGFloat = 0
        var totalWidth: CGFloat = 0

        var lineWidth: CGFloat = 0
        var lineHeight: CGFloat = 0

        for size in sizes {
            if lineWidth + size.width > proposal.width ?? 0 {
                totalHeight += lineHeight
                lineWidth = size.width
                lineHeight = size.height
            } else {
                lineWidth += size.width
                lineHeight = max(lineHeight, size.height)
            }

            totalWidth = max(totalWidth, lineWidth)
        }

        totalHeight += lineHeight
        let finalSize = CGSize(width: totalWidth, height: totalHeight)
        
        // 传递尺寸偏好给父视图
        subviews.first?.layoutPreference(key: LayoutSizePreferenceKey.self, value: finalSize)
        return finalSize
    }

    // placeSubviews方法保持不变
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let sizes = subviews.map { $0.sizeThatFits(.unspecified) }

        var lineX = bounds.minX
        var lineY = bounds.minY
        var lineHeight: CGFloat = 0

        for index in subviews.indices {
            if lineX + sizes[index].width > (proposal.width ?? 0) {
                lineY += lineHeight
                lineHeight = 0
                lineX = bounds.minX
            }

            subviews[index].place(
                at: .init(
                    x: lineX + sizes[index].width / 2,
                    y: lineY + sizes[index].height / 2
                ),
                anchor: .center,
                proposal: ProposedViewSize(sizes[index])
            )

            lineHeight = max(lineHeight, sizes[index].height)
            lineX += sizes[index].width
        }
    }
}
  1. 在ContentView中监听并触发动画
struct ContentView: View {
    @State private var layoutSize = CGSize.zero
    
    var body: some View {
        FlowLayout {
            ForEach(0..<5) { _ in
                Group {
                    Text("Hello")
                        .font(.largeTitle)
                    Text("World")
                        .font(.title)
                    Text("!!!")
                        .font(.title3)
                }
                .border(Color.red)
            }
        }
        .onPreferenceChange(LayoutSizePreferenceKey.self) { newSize in
            layoutSize = newSize
        }
        .animation(.default, value: layoutSize)
    }
}

关键原理

SwiftUI的动画系统需要明确的**变化值(value)**来触发过渡效果,指定value后,系统会在该值改变时自动为布局变化添加动画。直接在布局方法中使用withAnimation无效,因为布局计算是系统同步执行的环节,无法被动画块包裹。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 11:45:32