I am trying to maintain an MVVM structure, and I've run into this issue where the view will show the value of the @Published variable one behind. The updated value will only be shown in View after I close the app and reopen it. The @Published variable remindersCount is not triggering the reload of my view.
I've spent days attempting different things and reading stack overflow and other forums and it seems this should work but obviously, I am doing something wrong. If someone could point me in the right direction, I would really appreciate it. Thank you.
**The ViewModel has 2 published variables: **
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()
}
The reminders and remindersCount variables are updated like so:
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)
}
After running user interactions through some logic, the Reminders database gets updated with this function, which lives in another ViewModel:
for time in randomTimes {
let idNotification = UUID().uuidString
let newReminder: ReminderModel = ReminderModel(idNotification: idNotification, random: true, date: time)
reminderVM.saveReminder(reminder: newReminder)
notificationMgr.scheduleNotification(identifier: idNotification, date: time)
}
**Implementation of a simple functionality to showcase the issue **
First Scenario: I expected the reminderVM.remindersCount to update and trigger the reload, but it is always one behind. I have to close the app and reopen it to have the the view with the updated count data
struct PreferencesView: View {
@EnvironmentObject private var reminderVM: ReminderViewModel
var body: some View {
NavigationView {
Text("NUMBER OF REMINDERS: \(reminderVM.remindersCount)")
}
}
}
Second Scenario: Since that didn't work, I attempted to "force" an update by adding a button for testing purposes. Still encountered the same issue.
struct PreferencesView: View {
@EnvironmentObject private var reminderVM: ReminderViewModel
var body: some View {
NavigationView {
VStack {
Button("reload") {
reminderVM.getRemindersCount()
}
Text("NUMBER OF REMINDERS: \(reminderVM.remindersCount)")
}
}
}
}
################################################################# MINIMAL REPRODUCIBLE EXAMPLE #################################################################
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()
}
}
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)
}
}
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()
}
// MARK: Private functions
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()
}
}
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)")
}
}
}
}
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
}
}
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
Entity: Reminder
| Attribute. | Type |
|---|---|
| date | Date |
| id | UUID |
| idNotification | String |
| random | Boolean |
You should not use
self.remindersbecause at the moment when a subscription gets new value, the property still have old value.So when set new value to a
@Publishedproperty the logic is next:Or a short version with the same result: