SwiftUI实现不同栈内视图平滑动画及TextField聚焦动画优化
SwiftUI 视图平滑动画问题解决
问题场景1:切换容器时的动画不符合预期
我设置了一个视图,当状态变化时,蓝色Text会在HStack或独立HStack(位于下方)中切换。原本希望它在position1(右侧)与position2(下方右侧)间平滑上下移动,但直接添加动画后效果未达预期。
示例代码:
struct ContentView: View { @State var show = false var body: some View { VStack { VStack { HStack { Text("test spacer") .contentShape(Rectangle()) .frame(minWidth: 0, maxWidth: .infinity) .background(Color.red) if !show { Text("xxxxxx") .background(Color.blue) // position1 } } if show { HStack { Spacer() Text("xxxxxx") .background(Color.blue) // position2 } } } .background(Color.yellow) .animation(.easeInOut(duration: 1), value: show) Button("button") { show.toggle() } } .padding() } }
问题场景2:TextField聚焦+键盘弹出时动画卡顿
使用matchedGeometryEffect解决了上述Text的动画问题,但将左侧替换为TextField,并将动画触发时机改为聚焦状态后,键盘弹出时动画执行不流畅。
示例代码:
struct ContentView: View { @State private var text = "" @Namespace var ns @FocusState private var isFocused: Bool var body: some View { ZStack { Color.clear .contentShape(Rectangle()) .onTapGesture { isFocused = false } VStack { HStack { TextField("placeholder", text: $text, axis: .vertical) .focused($isFocused) .frame(minHeight: 28) .background(Color.red) if !isFocused { Text("xxxxxx") .background(Color.blue) .matchedGeometryEffect(id: "xxxxxx", in: ns) } } if isFocused { HStack { Spacer() Text("xxxxxx") .background(Color.blue) .matchedGeometryEffect(id: "xxxxxx", in: ns) } } } .background(Color.yellow) .animation(.easeInOut(duration: 1), value: isFocused) } .padding() } }
解决方案
通过以下优化点实现平滑滑动:
- 避免视图销毁重建:用
opacity控制显示/隐藏,替代条件渲染,保证两个状态下的视图始终存在,消除重建带来的动画卡顿。 - 精准控制动画触发:用
withAnimation在状态变化时主动触发动画,避免全局动画被键盘事件干扰。 - 处理键盘布局偏移:监听键盘高度,动态调整视图底部 padding,避免键盘弹出导致的布局突变影响动画轨迹。
修改后的完整代码:
struct ContentView: View { @State private var text = "" @Namespace var ns @FocusState private var isFocused: Bool @Environment(\.keyboardFrame) private var keyboardFrame var body: some View { ZStack { Color.clear .contentShape(Rectangle()) .onTapGesture { withAnimation(.easeInOut(duration: 0.5)) { isFocused = false } } VStack(spacing: 0) { HStack { TextField("placeholder", text: $text, axis: .vertical) .focused($isFocused) .frame(minHeight: 28) .background(Color.red) // 保留视图,用opacity控制显示,配合matchedGeometryEffect Text("xxxxxx") .background(Color.blue) .matchedGeometryEffect(id: "xxxxxx", in: ns, isSource: !isFocused) .opacity(isFocused ? 0 : 1) } HStack { Spacer() // 同样保留视图,用opacity控制 Text("xxxxxx") .background(Color.blue) .matchedGeometryEffect(id: "xxxxxx", in: ns, isSource: isFocused) .opacity(isFocused ? 1 : 0) } } .background(Color.yellow) // 聚焦时触发动画 .onChange(of: isFocused) { newValue in if newValue { withAnimation(.easeInOut(duration: 0.5)) {} } } // 适配键盘高度,添加平滑过渡 .padding(.bottom, keyboardFrame.height > 0 ? keyboardFrame.height : 0) .animation(.easeInOut(duration: 0.3), value: keyboardFrame) } .padding() } } // 扩展环境变量,监听键盘高度 private extension EnvironmentValues { var keyboardFrame: CGRect { self[KeyboardFrameKey.self] ?? .zero } } private struct KeyboardFrameKey: EnvironmentKey { static let defaultValue: CGRect? = nil } private extension View { func keyboardFrame(_ frame: CGRect?) -> some View { environment(\.keyboardFrame, frame) } // 添加键盘监听Modifier func observeKeyboard() -> some View { self .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero self.keyboardFrame(frame) } .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in self.keyboardFrame(.zero) } } } // 在App入口添加键盘监听 @main struct SmoothAnimationApp: App { var body: some Scene { WindowGroup { ContentView() .observeKeyboard() } } }
关键优化说明
matchedGeometryEffect的isSource参数:明确指定哪个状态下的视图是动画源,让SwiftUI更精准地计算动画轨迹。- 独立的键盘动画:给键盘高度变化设置单独的短时长动画,避免与视图切换动画冲突。
- 避免条件渲染:视图始终存在仅通过透明度控制显示,消除视图销毁重建带来的性能损耗和动画断层。
内容的提问来源于stack exchange,提问作者mikko
相关产品推荐
相关产品推荐

