SwiftUI中TextField聚焦触发跨栈视图平滑动画的实现方案
问题描述
使用matchedGeometryEffect关联两个视图的尺寸与位置后,通过TextField聚焦事件触发动画时,键盘弹出会导致动画执行不流畅,如何让元素从position1平滑移动到position2?
相关代码:
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") // position1 .background(Color.blue) .matchedGeometryEffect(id: "xxxxxx", in: ns) } } if isFocused { HStack { Spacer() Text("xxxxxx") // position2 .background(Color.blue) .matchedGeometryEffect(id: "xxxxxx", in: ns) } } } .background(Color.yellow) .animation(.easeInOut(duration: 1), value: isFocused) } .padding() } }
动画表现:元素从TextField右侧(position1)移动到下方靠右位置(position2)时,因键盘弹出导致卡顿,动画不连贯。
解决方案
1. 替换全局动画修饰符,用withAnimation精准控制触发时机
全局.animation会响应所有与isFocused相关的布局变化(包括键盘弹出带来的视图偏移),引发动画冲突。改用withAnimation包裹状态变更操作,仅在聚焦/失焦时触发目标动画:
- 给TextField添加点击手势,触发聚焦时启动动画:
TextField("placeholder", text: $text, axis: .vertical) .focused($isFocused) .frame(minHeight: 28) .background(Color.red) .onTapGesture { withAnimation(.easeInOut(duration: 1)) { isFocused = true } } - 修改外部点击失焦的逻辑,同样用
withAnimation包裹:Color.clear .contentShape(Rectangle()) .onTapGesture { withAnimation(.easeInOut(duration: 1)) { isFocused = false } } - 移除原VStack上的全局
.animation(.easeInOut(duration: 1), value: isFocused)修饰符。
2. 优化matchedGeometryEffect参数
明确指定匹配的属性与锚点,减少不必要的动画计算,确保动画对齐精准:
// position1的Text Text("xxxxxx") .background(Color.blue) .matchedGeometryEffect(id: "xxxxxx", in: ns, properties: .frame, anchor: .center) // position2的Text Text("xxxxxx") .background(Color.blue) .matchedGeometryEffect(id: "xxxxxx", in: ns, properties: .frame, anchor: .center)
3. 隔离键盘布局变化的影响
键盘弹出会导致父视图偏移,给VStack添加.ignoresSafeArea(.keyboard),让容器不随键盘移动,保证动画路径不受干扰:
VStack { // 原有内容 } .background(Color.yellow) .ignoresSafeArea(.keyboard)
完整修改后的代码
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 { withAnimation(.easeInOut(duration: 1)) { isFocused = false } } VStack { HStack { TextField("placeholder", text: $text, axis: .vertical) .focused($isFocused) .frame(minHeight: 28) .background(Color.red) .onTapGesture { withAnimation(.easeInOut(duration: 1)) { isFocused = true } } if !isFocused { Text("xxxxxx") // position1 .background(Color.blue) .matchedGeometryEffect(id: "xxxxxx", in: ns, properties: .frame, anchor: .center) } } if isFocused { HStack { Spacer() Text("xxxxxx") // position2 .background(Color.blue) .matchedGeometryEffect(id: "xxxxxx", in: ns, properties: .frame, anchor: .center) } } } .background(Color.yellow) .ignoresSafeArea(.keyboard) } .padding() } }
核心逻辑说明
withAnimation确保动画仅在聚焦状态变更时触发,避免键盘布局变化的干扰。matchedGeometryEffect的properties: .frame限定只匹配尺寸与位置,减少动画计算量。.ignoresSafeArea(.keyboard)让容器保持固定布局,保证元素移动路径的连贯性。
内容的提问来源于stack exchange,提问作者mikko
相关产品推荐
相关产品推荐

