Я обнаружил неожиданное поведение при использовании @Published для прослушивания обновлений модели просмотра. Вот что я нашел:
// 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)
}
}
Удивительно, но иногда табличное представление пусто, даже если для моего пользователя есть уведомления. После некоторой отладки я обнаружил, что когда я пытаюсь перезагрузить представление таблицы после того, как viewModel.$notifications уведомляет мой виртуальный капитал об обновлениях, фактическое свойство viewModel.notifications не обновляется, в то время как notifs в обработчике получения подписки обновляется правильно.
Пример вывода моей проблемы: debug: notifs.count - 8, viewModel.notifications.count - Optional(0)
Это из-за какого-то состояния собственности @Published? И какова наилучшая практика решения этой проблемы? Я знаю, что могу добавить didSet к notifications и настоятельно попросить мой VC обновиться или просто вызвать self?.tableView.reloadData() в следующем основном цикле выполнения. Но ни один из них не выглядит чистым.
Эта плавающая задача является идеальным источником состояния гонки. Также вам, вероятно, следует использовать @MainActor в этой функции, чтобы попытаться синхронизировать Комбинат с async/await. Обычно они не работают вместе, поэтому AsyncStream, вероятно, будет лучшим вариантом.
Привет @workingdogsupportUkraine, я пробовал ObservableObject и делаю viewModel.objectWillChange.sink(XXX), но это тоже не решает проблему
@loremipsum Я могу воспроизвести проблему, если изменю функцию fetchAllNotifications, чтобы она всегда запускалась на главном актере, с помощью Task {@MainActor in XXX }
Создание ObservableObject не требуется для использования @Published и не меняет семантику работы @Published, кроме подключения к objectWillChange. @Published — это, по сути, просто оболочка свойства CurrentValueSubject.





Уведомление о свойстве @Published происходит в обработчике willSet свойства. Это означает, что значение свойства фактически не изменится до тех пор, пока не будут отправлены все уведомления.
Внутри вашего sink вызова, где вы запрашиваете self?.viewModel.notifications, вы получаете предыдущее значение notifications, а не новое значение, которое должно храниться в свойстве. Новое значение недоступно в вашем методе приемника.
Если вы хотите использовать Объединение для публикации уведомлений, используйте свойство CurrentValueSubject вместо свойства @Published. @Published на самом деле нацелен на синтаксический сахар для SwiftUI, а не на общий механизм публикации/подписки для моделей.
Я бы рекомендовал использовать UITableViewDiffableDataSource и не использовать reloadData. Перезагрузка таблицы требует много работы: вся работа, проделанная ОС для подготовки к отрисовке, выполняется заново.
Спасибо за ответ! Для The notification of an @Published property occurs in the willSet handler for the property. это звучит так, будто поведение должно быть детерминированным, и я всегда должен воспроизводить свою проблему. Но это не так, я воспроизвожу проблему только время от времени (т. е. модель представления большую часть времени фактически имеет обновленные значения). Поэтому я считаю, что поведение не является детерминированным, что, по-видимому, вызвано некоторым состоянием гонки в реализации оболочки свойства @Published.
Это могло быть так. Или вы столкнулись с проблемой, вызванной тем, что изменение происходит в одном потоке, а уведомление поступает в основной поток. Это не меняет моих рекомендаций. Я рекомендую вам не использовать @Published в качестве общего шаблона наблюдателя (это для SwiftUI), и вам, вероятно, будет лучше использовать один из объектов DataSource для обработки обновлений, а не перезагружать представление каждый раз.
Фактический ответ на этот вопрос я нашел в другом вопросе о переполнении стека: Разница между CurrentValueSubject и @Published. @Published запускает обновление в willSet свойств, поэтому может возникнуть состояние гонки между тем, что оно будет установлено, и тем, что оно фактически установлено.
Вам необходимо иметь
class NotificationsViewModel: ObservableObject {...}, если вы хотите использовать@Published. Это в первую очередь предназначено для использования в представлениях SwiftUI. Посмотрите на эту ссылку, она дает вам несколько хороших примеров того, как управлять данными в вашем приложении SwiftUI: Мониторинг данных. Если у вас есть приложение UIKit, это не имеет значения.