SwiftUI中@FocusState引发键盘首次弹出时视图重渲染问题解决
问题描述
我有一个名为NewEditListView的视图,用于创建或编辑ListItem(SwiftData对象)。当前遇到的问题是:首次调出键盘编辑ListItem名称时,会触发视图重渲染,导致ViewModel中的ListItemManager.configureList(for: id, context)方法被第二次调用,进而创建新的ListItem或将已编辑的ListItem传入新的Model Context。该视图通过sheet方式呈现,请问如何避免此问题?
视图模型代码
extension NewEditListView { @Observable final class ViewModel { // MARK: - Properties var list: ListItem // MARK: - Init init(id: PersistentIdentifier?, _ container: ModelContainer) { let context = ModelContext(container) self.list = ListItemManager.configureList(for: id, context) } } }
视图代码
struct NewEditListView: View { // MARK: - Properties let id: PersistentIdentifier? @State private var viewModel: ViewModel @FocusState private var isFocused: Bool @Environment(\.dismiss) private var dismiss init(id: PersistentIdentifier?, container: ModelContainer) { self.id = id let viewModel = ViewModel(id: id, container) _viewModel = State(initialValue: viewModel) } // MARK: - Body var body: some View { NavigationStack { ScrollView { VStack(spacing: 24) { textField } .padding(.top, 24) } .navigationTitle(viewModel.list.isNew ? "New List" : "Edit List") .navigationBarTitleDisplayMode(.inline) .background(Color(.systemGroupedBackground)) .interactiveDismissDisabled(true) .onAppear { DispatchQueue.main.asyncAfter(deadline: .now()+1) { isFocused = true } } } } // MARK: - Views private var textField: some View { TextField("List name", text: $viewModel.list.name) .font(.title3) .fontWeight(.semibold) .submitLabel(.done) .padding(.leading, 16) .frame(maxWidth: .infinity) .frame(height: 48) .padding(.horizontal, 16) .focused($isFocused) } }
视图呈现代码
.sheet(isPresented: $presentingSheet, content: { NewEditListView(id: nil, container: context.container) })
解决方案
1. 用@StateObject替代@State管理ViewModel
@StateObject会确保ViewModel在视图生命周期内仅初始化一次,不会因视图重渲染(比如键盘弹出触发布局变化)而重新创建,从根源避免configureList被重复调用。
修改视图中的ViewModel属性定义及初始化逻辑:
struct NewEditListView: View { let id: PersistentIdentifier? // 替换原@State为@StateObject @StateObject private var viewModel: ViewModel @FocusState private var isFocused: Bool @Environment(\.dismiss) private var dismiss init(id: PersistentIdentifier?, container: ModelContainer) { self.id = id // 使用StateObject的wrappedValue初始化,确保仅执行一次 _viewModel = StateObject(wrappedValue: ViewModel(id: id, container)) } // ... 剩余视图代码不变 }
2. 复用环境中的ModelContext(推荐SwiftData最佳实践)
不要在ViewModel中手动创建新的ModelContext,而是复用父视图环境中已有的上下文,避免因多上下文导致的对象重复创建问题。
步骤1:在App层级注入ModelContainer
@main struct YourApp: App { let container: ModelContainer init() { do { container = try ModelContainer(for: ListItem.self) } catch { fatalError("Failed to initialize ModelContainer: \(error)") } } var body: some Scene { WindowGroup { MainView() .modelContainer(container) } } }
步骤2:修改ViewModel和视图,使用环境上下文
// 视图模型修改 extension NewEditListView { @Observable final class ViewModel { var list: ListItem // 直接传入环境中的ModelContext init(id: PersistentIdentifier?, context: ModelContext) { self.list = ListItemManager.configureList(for: id, context) } } } // 视图修改 struct NewEditListView: View { let id: PersistentIdentifier? @StateObject private var viewModel: ViewModel @FocusState private var isFocused: Bool @Environment(\.dismiss) private var dismiss init(id: PersistentIdentifier?, context: ModelContext) { self.id = id _viewModel = StateObject(wrappedValue: ViewModel(id: id, context: context)) } // ... 剩余视图代码不变 }
步骤3:修改sheet呈现代码
.sheet(isPresented: $presentingSheet) { // 传入当前环境中的ModelContext NewEditListView(id: nil, context: context) }
关键原因说明
- 原代码中
@State存储ViewModel时,虽理论上不会重复初始化,但@StateObject是专门为Observable引用类型设计的管理方式,更可靠稳定。 - 手动创建
ModelContext会导致对象处于独立上下文,修改时容易触发不必要的对象复制或重建。 - sheet的content闭包每次触发时会重新创建视图实例,结合
@StateObject的单例特性,能彻底避免ViewModel重复初始化。
内容的提问来源于stack exchange,提问作者Ammar Ahmad
相关产品推荐
相关产品推荐

