I found some unexpected behavior when using @Published
to listen for view model's updates. Here's what I found:
// My View Model Class
class NotificationsViewModel {
// MARK: - Properties
@Published private(set) var notifications = [NotificationData]()
// MARK: - APIs
func fetchAllNotifications() {
Task {
do {
// This does a network call to get all the notifications.
notifications = try await NotificationsService.shared.getAllNotifications()
} catch {
printError(error)
}
}
}
}
class NotificationsViewController: UIViewController {
private let viewModel = NotificationsViewModel()
// Here are some more properties..
override func viewDidLoad() {
super.viewDidLoad()
// This sets up the UI, such as adding a table view.
configureUI()
// This binds the current VC to the View Model.
bindToViewModel()
}
func bindToViewModel() {
viewModel.fetchAllNotifications()
viewModel.$notifications.receive(on: DispatchQueue.main).sink { [weak self] notifs in
if self?.viewModel.notifications.count != notifs.count {
print("debug: notifs.count - \(notifs.count), viewModel.notifications.count - \(self?.viewModel.notifications.count)")
}
self?.tableView.reloadData()
}.store(in: &cancellables)
}
}
Surprisingly, sometimes the table view is empty, even if there are notifications for my user. After some debugging, I found when I try to reload the table view after viewModel.$notifications
notifies my VC about the updates, the actual viewModel.notifications
property didn't get updated, while the notifs
in the subscription receive handler is correctly updated.
A sample output of my issue is: debug: notifs.count - 8, viewModel.notifications.count - Optional(0)
Is this due to some race condition of @Published
property? And what is the best practice of solving this issue? I know I can add a didSet
to notifications
and imperatively asks my VC to refresh itself, or simply call self?.tableView.reloadData()
in the next main runloop. But neither of them look clean.
I found the actual answer for this question in another stack overflow question: Difference between CurrentValueSubject and @Published. @Published triggers the update in willSet of the properties, so there could be a race condition between it is going to be set VS it is actually set