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

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实体定义

AttributeType
dateDate
idUUID
idNotificationString
randomBoolean

问题根源分析

  1. 订阅逻辑错误:getRemindersCount()的map闭包中,错误使用self.reminders.count而非闭包传入的最新数组参数。由于self.reminders的更新滞后于$reminders的发布值,导致拿到的是旧计数。
  2. 冗余订阅风险:每次调用getRemindersCount()都会新增一个订阅,内存中会存在多个重复订阅,导致逻辑混乱。
  3. 不必要的状态维护: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"按钮添加新提醒时:

  1. ReminderDataService保存数据并刷新reminderEntities
  2. dataService.get()发送新的ReminderModel数组
  3. ViewModel的reminders被更新,触发@Published发布
  4. 计算属性remindersCount自动更新,视图因reminders的变化而刷新,显示最新计数

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 07:36:02