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的实际尺寸,以此作为动画触发源:
- 定义PreferenceKey捕获尺寸
struct LayoutSizePreferenceKey: PreferenceKey { static var defaultValue: CGSize = .zero static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() } }
- 修改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 } } }
- 在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
相关产品推荐
相关产品推荐

