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

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()
    }
}

解决方案

通过以下优化点实现平滑滑动:

  1. 避免视图销毁重建:用opacity控制显示/隐藏,替代条件渲染,保证两个状态下的视图始终存在,消除重建带来的动画卡顿。
  2. 精准控制动画触发:用withAnimation在状态变化时主动触发动画,避免全局动画被键盘事件干扰。
  3. 处理键盘布局偏移:监听键盘高度,动态调整视图底部 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 05:23:16