SwiftUI中@Query导致.sheet(item:content:)被重复调用两次的问题
SwiftUI中@Query导致.sheet(item:)的content闭包重复调用的解决办法
在使用SwiftUI的@Query获取Core Data数据时,点击按钮设置editingToDo后,.sheet(item:content:)的content闭包会被调用两次,日志显示第一次editingToDo为nil,第二次才是预期的对象。已确认用FetchDescriptor替代@Query可解决问题,但不想引入副作用,以下是其他可行方案:
方案1:改用isPresented绑定替代item绑定
将基于Identifiable对象的item绑定,改为布尔值的isPresented绑定,结合单独存储选中对象的变量,避免因@Query刷新导致的闭包重复执行:
struct TodayPage: View { @Environment(\.modelContext) private var modelContext @FocusState private var focusedToDo: ToDoModel? @State private var isEditing = false @State private var currentEditingToDo: ToDoModel? @Query( filter: #Predicate<ToDoModel> { $0.isRepeat }, sort: \.order ) private var toDos: [ToDoModel] private var visibleToDos: [ToDoModel] { let sortedToDos = toDos.sorted(option: .sortByGoal) return sortedToDos.filter { $0.repeatDays.contains(.now) } } var body: some View { NavigationStack { GeometryReader { proxy in if visibleToDos.isEmpty { ContentUnavailableView( LocalizedString.Today.emptyTitle, systemImage: emptyImageName ) .background(Color.background) } else { List { ForEach(visibleToDos) { toDo in HStack(alignment: .top, spacing: 10) { // 其他视图... VStack(alignment: .leading, spacing: 8) { HStack { // 其他视图... if focusedToDo == toDo { Button { currentEditingToDo = toDo isEditing = true } label: { Image(systemName: "info.circle") .resizable() .frame(width: 20, height: 20) } } } // 其他视图... } } } } } } } .sheet(isPresented: $isEditing) { if let toDo = currentEditingToDo { ToDoFormSheet(toDo: toDo) } } .onDismiss { currentEditingToDo = nil } } }
方案2:将Sheet封装到独立的宿主视图
把Sheet的逻辑抽离到一个不会因@Query数据更新而重建的独立视图中,隔离@Query刷新对Sheet的影响:
struct TodayPage: View { @Environment(\.modelContext) private var modelContext @FocusState private var focusedToDo: ToDoModel? @State private var editingToDo: ToDoModel? @Query( filter: #Predicate<ToDoModel> { $0.isRepeat }, sort: \.order ) private var toDos: [ToDoModel] private var visibleToDos: [ToDoModel] { let sortedToDos = toDos.sorted(option: .sortByGoal) return sortedToDos.filter { $0.repeatDays.contains(.now) } } var body: some View { SheetHost(editingToDo: $editingToDo) { NavigationStack { GeometryReader { proxy in if visibleToDos.isEmpty { ContentUnavailableView( LocalizedString.Today.emptyTitle, systemImage: emptyImageName ) .background(Color.background) } else { List { ForEach(visibleToDos) { toDo in HStack(alignment: .top, spacing: 10) { // 其他视图... VStack(alignment: .leading, spacing: 8) { HStack { // 其他视图... if focusedToDo == toDo { Button { editingToDo = toDo } label: { Image(systemName: "info.circle") .resizable() .frame(width: 20, height: 20) } } } // 其他视图... } } } } } } } } } } struct SheetHost<Content: View>: View { @Binding var editingToDo: ToDoModel? let content: Content init(editingToDo: Binding<ToDoModel?>, @ViewBuilder content: () -> Content) { self._editingToDo = editingToDo self.content = content() } var body: some View { content .sheet(item: $editingToDo) { toDo in ToDoFormSheet(toDo: toDo) } } }
方案3:避免@Query触发不必要的视图刷新
将visibleToDos转换为@State变量,通过onChange监听toDos的变化来更新,减少视图重建次数:
struct TodayPage: View { @Environment(\.modelContext) private var modelContext @FocusState private var focusedToDo: ToDoModel? @State private var editingToDo: ToDoModel? @State private var visibleToDos: [ToDoModel] = [] @Query( filter: #Predicate<ToDoModel> { $0.isRepeat }, sort: \.order ) private var toDos: [ToDoModel] var body: some View { NavigationStack { GeometryReader { proxy in if visibleToDos.isEmpty { ContentUnavailableView( LocalizedString.Today.emptyTitle, systemImage: emptyImageName ) .background(Color.background) } else { List { ForEach(visibleToDos) { toDo in HStack(alignment: .top, spacing: 10) { // 其他视图... VStack(alignment: .leading, spacing: 8) { HStack { // 其他视图... if focusedToDo == toDo { Button { editingToDo = toDo } label: { Image(systemName: "info.circle") .resizable() .frame(width: 20, height: 20) } } } // 其他视图... } } } } } } } .sheet(item: $editingToDo) { toDo in ToDoFormSheet(toDo: toDo) } .onChange(of: toDos) { newValue in let sortedToDos = newValue.sorted(option: .sortByGoal) visibleToDos = sortedToDos.filter { $0.repeatDays.contains(.now) } } } }
问题核心原因
@Query是Observable类型,当Core Data数据变化时会触发视图body重新计算,而.sheet(item:content:)的content闭包会在视图重建时被再次执行,加上editingToDo的状态变化,最终导致两次调用。上述方案通过隔离状态变化、减少视图刷新频率等方式解决该问题。
内容的提问来源于stack exchange,提问作者joeydev
相关产品推荐
相关产品推荐

