swiftuitableviewconcurrencyobservablecombine

Potential race condition on Combine's @Published property wrapper


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.


Solution

  • 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