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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 06:34:57