iOS 16下父视图动画时子视图@State变更导致动画失效的解决方法
iOS 16 SwiftUI 弹窗动画异常修复:子视图@State变更导致动画失效
问题描述
实现带内容的隐藏弹窗,触发后父视图上滑,所有子内容应跟随滑动。iOS 16前功能正常,但iOS 16中出现异常:父视图动画期间子视图的@State属性变更时,子视图会瞬间显示在目标位置,无法跟随父视图完成滑动动画;使用静态文本时动画则正常执行。
复现代码
异常场景(动态文本)
struct BrokenAnimationView: View { @State private var isPresented = false @State private var dynamicText = "初始文本" var body: some View { ZStack { Color.gray.opacity(0.3) .onTapGesture { withAnimation(.easeInOut(duration: 0.8)) { isPresented = false } } VStack { Spacer() if isPresented { VStack { Text(dynamicText) .font(.title) .padding() Button("关闭") { dynamicText = "更新后的文本" withAnimation(.easeInOut(duration: 0.8)) { isPresented = false } } .padding() } .background(Color.white) .cornerRadius(16) .padding() } } } .onAppear { withAnimation(.easeInOut(duration: 0.8)) { isPresented = true } } } }
正常场景(静态文本)
struct WorkingAnimationView: View { @State private var isPresented = false var body: some View { ZStack { Color.gray.opacity(0.3) .onTapGesture { withAnimation(.easeInOut(duration: 0.8)) { isPresented = false } } VStack { Spacer() if isPresented { VStack { Text("固定文本") .font(.title) .padding() Button("关闭") { withAnimation(.easeInOut(duration: 0.8)) { isPresented = false } } .padding() } .background(Color.white) .cornerRadius(16) .padding() } } } .onAppear { withAnimation(.easeInOut(duration: 0.8)) { isPresented = true } } } }
问题原因
iOS 16中,当父视图处于动画周期时,子视图的@State属性变更会触发该子视图立即重绘,系统会中断子视图继承自父视图的动画上下文,导致子视图直接跳转到目标状态,无法跟随父视图完成滑动动画。
解决方案
方案1:统一状态管理(推荐)
将子视图的状态提升到父视图的ObservableObject中,确保状态变更与动画上下文在同一作用域,避免局部重绘中断动画:
class PopupViewModel: ObservableObject { @Published var isPresented = false @Published var dynamicText = "初始文本" } struct FixedAnimationView: View { @StateObject private var viewModel = PopupViewModel() var body: some View { ZStack { Color.gray.opacity(0.3) .onTapGesture { withAnimation(.easeInOut(duration: 0.8)) { viewModel.isPresented = false } } VStack { Spacer() if viewModel.isPresented { VStack { Text(viewModel.dynamicText) .font(.title) .padding() Button("关闭") { withAnimation(.easeInOut(duration: 0.8)) { viewModel.dynamicText = "更新后的文本" viewModel.isPresented = false } } .padding() } .background(Color.white) .cornerRadius(16) .padding() } } } .onAppear { withAnimation(.easeInOut(duration: 0.8)) { viewModel.isPresented = true } } } }
方案2:显式指定过渡动画
给弹窗内容添加明确的transition,强制子视图动画与父视图滑动动画绑定:
struct FixedAnimationView2: View { @State private var isPresented = false @State private var dynamicText = "初始文本" var body: some View { ZStack { Color.gray.opacity(0.3) .onTapGesture { withAnimation(.easeInOut(duration: 0.8)) { isPresented = false } } VStack { Spacer() if isPresented { VStack { Text(dynamicText) .font(.title) .padding() Button("关闭") { dynamicText = "更新后的文本" withAnimation(.easeInOut(duration: 0.8)) { isPresented = false } } .padding() } .background(Color.white) .cornerRadius(16) .padding() .transition(.move(edge: .bottom).combined(with: .opacity)) } } } .onAppear { withAnimation(.easeInOut(duration: 0.8)) { isPresented = true } } } }
方案3:使用iOS 16+的continuous动画
利用iOS 16新增的.continuous动画修饰符,确保状态变更时动画连续执行:
struct FixedAnimationView3: View { @State private var isPresented = false @State private var dynamicText = "初始文本" var body: some View { ZStack { Color.gray.opacity(0.3) .onTapGesture { withAnimation(.easeInOut(duration: 0.8).continuous) { isPresented = false } } VStack { Spacer() if isPresented { VStack { Text(dynamicText) .font(.title) .padding() Button("关闭") { dynamicText = "更新后的文本" withAnimation(.easeInOut(duration: 0.8).continuous) { isPresented = false } } .padding() } .background(Color.white) .cornerRadius(16) .padding() } } } .onAppear { withAnimation(.easeInOut(duration: 0.8)) { isPresented = true } } } }
内容的提问来源于stack exchange,提问作者Danny
相关产品推荐
相关产品推荐

