SwiftUI iOS15中@Published属性更新时视图不刷新问题
解决MVVM架构中@Published变量更新滞后、视图不刷新的问题
问题描述
维护MVVM架构的SwiftUI应用时,遇到@Published变量remindersCount无法触发视图刷新的问题:视图显示的值总是滞后一步,必须重启应用才能看到最新计数。
核心代码问题
ViewModel中的问题代码
class ReminderViewModel: ObservableObject { @Published private(set) var reminders: [ReminderModel] = [] @Published private(set) var remindersCount: Int = 0 init(dataService: any ReminderDataServiceProtocol) { self.dataService = dataService getReminders() getRemindersCount() } func getReminders() { dataService.get() .sink { error in fatalError("\(error)") } receiveValue: { reminders in self.reminders = reminders } .store(in: &cancellables) } func getRemindersCount() { $reminders .map { (_: [ReminderModel]) -> Int in // 错误:使用self.reminders.count而非闭包传入的最新数组 return self.reminders.count } .sink(receiveValue: { [weak self] count in self?.remindersCount = count }) .store(in: &cancellables) } }
问题场景复现
场景一:直接绑定变量无刷新
struct PreferencesView: View { @EnvironmentObject private var reminderVM: ReminderViewModel var body: some View { NavigationView { Text("NUMBER OF REMINDERS: \(reminderVM.remindersCount)") } } }
场景二:强制调用更新方法仍无效
struct PreferencesView: View { @EnvironmentObject private var reminderVM: ReminderViewModel var body: some View { NavigationView { VStack { Button("reload") { reminderVM.getRemindersCount() } Text("NUMBER OF REMINDERS: \(reminderVM.remindersCount)") } } } }
最小可复现示例(MRE)
ContentView
import SwiftUI import Foundation import Combine import CoreData struct ContentView: View { @EnvironmentObject private var reminderVM: ReminderViewModel var body: some View { VStack { VStack (){ Button("Add a reminder") { let newReminder: ReminderModel = ReminderModel(idNotification: UUID().uuidString, random: true, date: Date.now) reminderVM.saveReminder(reminder: newReminder) } .buttonStyle(.bordered) Text("NUMBER OF REMINDERS: \(reminderVM.remindersCount)") Button("Update") { print("button pressed") reminderVM.getRemindersCount() } .buttonStyle(.bordered) } } .padding() } }
ReminderViewModel
class ReminderViewModel: ObservableObject { private let dataService: ReminderDataService private var cancellables = Set<AnyCancellable>() @Published private(set) var reminders: [ReminderModel] = [] @Published private(set) var remindersCount: Int = 0 init(dataService: ReminderDataService) { self.dataService = dataService getReminders() getRemindersCount() } func saveReminder(reminder: ReminderModel) { dataService.add(reminder) } func getReminders() { dataService.get() .sink { error in fatalError("\(error)") } receiveValue: { reminders in self.reminders = reminders } .store(in: &cancellables) } func getRemindersCount() { $reminders .map { (_: [ReminderModel]) -> Int in return self.reminders.count } .sink(receiveValue: { [weak self] count in self?.remindersCount = count }) .store(in: &cancellables) } }
ReminderDataService
class ReminderDataService: ObservableObject { @Published private var reminderEntities: [Reminder] = [] private let persistenceController: PersistenceController private let fetchRequest = NSFetchRequest<Reminder>(entityName: "Reminder") init(persistenceController: PersistenceController) { self.persistenceController = persistenceController fetch() } func get() -> AnyPublisher<[ReminderModel], Error> { $reminderEntities.tryMap { reminders in reminders.map {reminder in ReminderModel(reminder: reminder) } } .eraseToAnyPublisher() } func add(_ reminder: ReminderModel) { let reminderEntity = Reminder(context: persistenceController.container.viewContext) reminderEntity.id = reminder.id reminderEntity.idNotification = reminder.idNotification reminderEntity.random = reminder.random reminderEntity.date = reminder.date saveContext() } private func fetch() { do { fetchRequest.predicate = nil reminderEntities = try persistenceController.container.viewContext.fetch(fetchRequest) } catch let error { print("Error: Unable to fetch core data using data services \(error.localizedDescription)") } } private func saveContext() { do { try persistenceController.container.viewContext.save() } catch let error{ print("Error saving reminders in Core Data: \(error)") } fetch() } }
PersistenceController
class PersistenceController: ObservableObject { static let shared = PersistenceController() let container: NSPersistentContainer init() { container = NSPersistentContainer(name: "Model") container.loadPersistentStores { description, error in if let error = error { print("Core Data failed to load: \(error.localizedDescription)") } } } }
ReminderModel
struct ReminderModel: Hashable, Identifiable, Codable { var id = UUID() var idNotification: String var random: Bool var date: Date } extension ReminderModel { init(reminder: Reminder) { id = reminder.id ?? UUID() idNotification = reminder.idNotification ?? "" random = reminder.random date = reminder.date ?? Date.now } }
App入口
import SwiftUI @main struct MRE_App: App { @StateObject private var reminderVM = ReminderViewModel(dataService: ReminderDataService(persistenceController: (PersistenceController.shared))) var body: some Scene { WindowGroup { ContentView() .environmentObject(reminderVM) } } }
Core Data实体定义
| Attribute | Type |
|---|---|
| date | Date |
| id | UUID |
| idNotification | String |
| random | Boolean |
问题根源分析
- 订阅逻辑错误:
getRemindersCount()的map闭包中,错误使用self.reminders.count而非闭包传入的最新数组参数。由于self.reminders的更新滞后于$reminders的发布值,导致拿到的是旧计数。 - 冗余订阅风险:每次调用
getRemindersCount()都会新增一个订阅,内存中会存在多个重复订阅,导致逻辑混乱。 - 不必要的状态维护:
remindersCount作为独立的@Published变量,需要手动同步reminders的变化,增加了出错概率。
修复方案
最优方案:将remindersCount改为计算属性
直接依赖reminders数组,自动跟随其变化,省去手动订阅逻辑,彻底避免同步错误:
class ReminderViewModel: ObservableObject { private let dataService: ReminderDataService private var cancellables = Set<AnyCancellable>() @Published private(set) var reminders: [ReminderModel] = [] // 改为计算属性,自动返回reminders的计数 var remindersCount: Int { reminders.count } init(dataService: ReminderDataService) { self.dataService = dataService getReminders() // 移除getRemindersCount()的调用和定义 } func saveReminder(reminder: ReminderModel) { dataService.add(reminder) } func getReminders() { dataService.get() .sink { error in fatalError("\(error)") } receiveValue: { reminders in self.reminders = reminders } .store(in: &cancellables) } }
备选方案:修复订阅逻辑(不推荐)
如果必须保留独立的remindersCount变量,需修正map逻辑并确保只订阅一次:
func getRemindersCount() { $reminders // 使用闭包传入的最新数组计算计数 .map { $0.count } .sink(receiveValue: { [weak self] count in self?.remindersCount = count }) .store(in: &cancellables) }
注意:此方法需确保getRemindersCount()只在初始化时调用一次,避免重复订阅。
验证修复
修改后,点击"Add a reminder"按钮添加新提醒时:
ReminderDataService保存数据并刷新reminderEntitiesdataService.get()发送新的ReminderModel数组- ViewModel的
reminders被更新,触发@Published发布 - 计算属性
remindersCount自动更新,视图因reminders的变化而刷新,显示最新计数
内容的提问来源于stack exchange,提问作者trilogy
相关产品推荐
相关产品推荐

