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

如何在SwiftUI的ViewModifier中传递FocusState?

问题:使用FocusState控制输入框移至顶部的动画失效

成功案例:按钮切换Binding实现动画

通过按钮切换布尔值Binding,可实现输入框获取焦点时平滑移动到屏幕顶部的效果,代码如下:

import SwiftUI

let movableFieldID = "MyTextField"

struct TopMovable: ViewModifier {
    @Binding var text: String
    @Binding var active: Bool
    var namespace: Namespace.ID

    func body(content: Content) -> some View {
        if active {
            VStack {
                HStack {
                    Spacer()
                    Button("Dismiss") {
                        withAnimation {
                            active = false
                        }
                    }
                }
                TextField("Enter text", text: $text)
                    .textFieldStyle(.roundedBorder)
                    .matchedGeometryEffect(id: movableFieldID, in: namespace)
                Spacer()
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        } else {
            content
        }
    }
}

extension View {
    func topMovable(text: Binding<String>,
                    active: Binding<Bool>,
                    namespace: Namespace.ID) -> some View {
        self.modifier(TopMovable(text: text,
                                 active: active,
                                 namespace: namespace))
    }
}

struct ContentView: View {
    @Namespace private var namespace
    @State var active: Bool = false
    @State var text: String = ""

    var body: some View {
        VStack {
            Text("Hello, world!")
            Text("Hi!")
            TextField("Enter text", text: $text)
                .matchedGeometryEffect(id: movableFieldID, in: namespace)
                .textFieldStyle(.roundedBorder)
            Text("Howdy!")
            Button("Move the field") {
                withAnimation {
                    active.toggle()
                }
            }
        }
        .topMovable(text: $text, active: $active, namespace: namespace)
        .padding()
    }
}

效果:输入框点击按钮后会平滑移动到顶部,显示Dismiss按钮,点击可返回原位置。


失败尝试:FocusState控制导致无响应/动画失效

改用FocusState直接控制active状态后,输入框完全无响应,即使恢复响应也无法实现动画效果,代码如下:

import SwiftUI

let movableFieldID = "MyTextField"

struct TopMovable: ViewModifier {
    @Binding var text: String
    @FocusState var active: Bool
    var namespace: Namespace.ID

    func body(content: Content) -> some View {
        if active {
            VStack {
                HStack {
                    Spacer()
                    Button("Dismiss") {
                        withAnimation {
                            active = false
                        }
                    }
                }
                TextField("Enter text", text: $text)
                    .textFieldStyle(.roundedBorder)
                    .matchedGeometryEffect(id: movableFieldID, in: namespace)
                Spacer()
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        } else {
            content
        }
    }
}

extension View {
    func topMovable(text: Binding<String>,
                    active: FocusState<Bool>,
                    namespace: Namespace.ID) -> some View {
        self.modifier(TopMovable(text: text,
                                 active: active,
                                 namespace: namespace))
    }
}

struct ContentView: View {
    @Namespace private var namespace
    @FocusState var active: Bool
    @State var text: String = ""

    var body: some View {
        VStack {
            Text("Hello, world!")
            Text("Hi!")
            TextField("Enter text", text: $text)
                .focused($active)
                .matchedGeometryEffect(id: movableFieldID, in: namespace)
                .textFieldStyle(.roundedBorder)
            Text("Howdy!")
        }
        .topMovable(text: $text, active: _active, namespace: namespace)
        .padding()
    }
}

问题表现:点击输入框无任何响应,无法触发移动动画。


解决方案

核心问题是ViewModifier中直接使用FocusState会破坏视图层级的焦点管理,正确的做法是分离焦点状态和动画控制状态,通过Binding同步两者:

修正后的完整代码

import SwiftUI

let movableFieldID = "MyTextField"

struct TopMovable: ViewModifier {
    @Binding var text: String
    @Binding var active: Bool
    var namespace: Namespace.ID

    func body(content: Content) -> some View {
        if active {
            VStack {
                HStack {
                    Spacer()
                    Button("Dismiss") {
                        withAnimation {
                            active = false
                        }
                    }
                }
                TextField("Enter text", text: $text)
                    .textFieldStyle(.roundedBorder)
                    .matchedGeometryEffect(id: movableFieldID, in: namespace)
                    .focused($active) // 绑定焦点,确保顶部输入框能正确失焦
                Spacer()
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        } else {
            content
        }
    }
}

extension View {
    func topMovable(text: Binding<String>,
                    active: Binding<Bool>,
                    namespace: Namespace.ID) -> some View {
        self.modifier(TopMovable(text: text,
                                 active: active,
                                 namespace: namespace))
    }
}

struct ContentView: View {
    @Namespace private var namespace
    @FocusState private var isFocused: Bool // 单独管理焦点状态
    @State private var active: Bool = false // 管理动画/视图切换
    @State private var text: String = ""

    var body: some View {
        VStack {
            Text("Hello, world!")
            Text("Hi!")
            TextField("Enter text", text: $text)
                .focused($isFocused)
                .matchedGeometryEffect(id: movableFieldID, in: namespace)
                .textFieldStyle(.roundedBorder)
            Text("Howdy!")
        }
        .topMovable(text: $text, active: $active, namespace: namespace)
        .padding()
        // 同步焦点状态到动画控制状态,触发动画
        .onChange(of: isFocused) { newValue in
            withAnimation {
                active = newValue
            }
        }
        // 动画状态关闭时,同步失去焦点
        .onChange(of: active) { newValue in
            if !newValue {
                isFocused = false
            }
        }
    }
}

关键修正说明

  1. 分离状态管理:用@FocusState var isFocused专门处理输入框焦点,@State var active控制视图切换和动画,避免直接在Modifier中使用FocusState
  2. 双向状态同步:通过onChange监听两个状态的变化,确保焦点变化触发动画,动画关闭时同步失焦
  3. 顶部输入框绑定焦点:在Modifier内部的TextField添加.focused($active),保证点击Dismiss按钮时输入框能正确失去焦点,避免状态不一致

内容的提问来源于stack exchange,提问作者Tres

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 08:30:03