Потенциальное состояние гонки в оболочке свойства @Published компании Joint

Я обнаружил неожиданное поведение при использовании @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() в следующем основном цикле выполнения. Но ни один из них не выглядит чистым.

Вам необходимо иметь class NotificationsViewModel: ObservableObject {...}, если вы хотите использовать @Published. Это в первую очередь предназначено для использования в представлениях SwiftUI. Посмотрите на эту ссылку, она дает вам несколько хороших примеров того, как управлять данными в вашем приложении SwiftUI: Мониторинг данных. Если у вас есть приложение UIKit, это не имеет значения.

workingdog support Ukraine 17.03.2024 02:01

Эта плавающая задача является идеальным источником состояния гонки. Также вам, вероятно, следует использовать @MainActor в этой функции, чтобы попытаться синхронизировать Комбинат с async/await. Обычно они не работают вместе, поэтому AsyncStream, вероятно, будет лучшим вариантом.

lorem ipsum 17.03.2024 02:48

Привет @workingdogsupportUkraine, я пробовал ObservableObject и делаю viewModel.objectWillChange.sink(XXX), но это тоже не решает проблему

Shuaiqing Luo 17.03.2024 16:26

@loremipsum Я могу воспроизвести проблему, если изменю функцию fetchAllNotifications, чтобы она всегда запускалась на главном актере, с помощью Task {@MainActor in XXX }

Shuaiqing Luo 17.03.2024 16:40

Создание ObservableObject не требуется для использования @Published и не меняет семантику работы @Published, кроме подключения к objectWillChange. @Published — это, по сути, просто оболочка свойства CurrentValueSubject.

jnpdx 18.03.2024 16:17
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
141
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Уведомление о свойстве @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.

Shuaiqing Luo 19.03.2024 16:25

Это могло быть так. Или вы столкнулись с проблемой, вызванной тем, что изменение происходит в одном потоке, а уведомление поступает в основной поток. Это не меняет моих рекомендаций. Я рекомендую вам не использовать @Published в качестве общего шаблона наблюдателя (это для SwiftUI), и вам, вероятно, будет лучше использовать один из объектов DataSource для обработки обновлений, а не перезагружать представление каждый раз.

Scott Thompson 19.03.2024 20:57
Ответ принят как подходящий

Фактический ответ на этот вопрос я нашел в другом вопросе о переполнении стека: Разница между CurrentValueSubject и @Published. @Published запускает обновление в willSet свойств, поэтому может возникнуть состояние гонки между тем, что оно будет установлено, и тем, что оно фактически установлено.

Другие вопросы по теме