Combine 的 @Published 属性包装器上的潜在竞争条件

问题描述 投票:0回答:1

我在使用

@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
通知我的 VC 有关更新后尝试重新加载表视图时,实际的
viewModel.notifications
属性没有更新,而订阅接收处理程序中的
notifs
是正确的已更新。

我的问题的示例输出是:

debug: notifs.count - 8, viewModel.notifications.count - Optional(0)

这是由于

@Published
财产的某些竞争条件造成的吗?解决这个问题的最佳实践是什么?我知道我可以将
didSet
添加到
notifications
并强制要求我的 VC 刷新自身,或者简单地在下一个主运行循环中调用
self?.tableView.reloadData()
。但它们看起来都不干净。

swift uitableview concurrency observable combine
1个回答
0
投票

有几个问题:

  1. notifications
    数组的初始化和观察开始之间存在竞争。

    如果你真的要使用这种模式,为了消除竞争,你应该消除

    fetchAllNotifications
    中的非结构化并发。相反,保持结构化并发。例如,也许制作
    fetchAllNotifications
    async
    方法并消除
    Task {…}
    :

    func fetchAllNotifications() async {
        do {
            // This does a network call to get all the notifications.
            notifications = try await NotificationsService.shared.getAllNotifications()
        } catch {
            printError(error)
        }
    }
    

    然后

    bindToViewModel
    应该是
    async
    await fetchAllNotifications()
    :

    func bindToViewModel() async {
        await viewModel.fetchAllNotifications()
        viewModel.$notifications
            .receive(on: DispatchQueue.main)
            .sink { … }
            .store(in: &cancellables)
    }
    

    这可确保您在

    notifications
    数组初始化之前不会开始接收更新。

  2. 您正在观察一组

    NotificationData
    ,但我们通常会建立一个发布者,在它们进入时发布单独的
    NotificationData
    有效负载,而不是整个数组。

  3. 视图模型正在从

    NotificationsService
    检索通知列表,但不会检测新通知。

    您可能希望视图模型观察

    NotificationsService
    中发布的值。一种方法是视图模型有一个
    PassthroughSubject
    ,然后视图模型将为来自
    sink
    发布者的通知建立
    NotificationsService

    struct NotificationData { … }
    
    extension Notification.Name {
        static let customNotification = Notification.Name("CustomNotification")
    }
    
    class NotificationsService {
        static let shared = NotificationsService()
    
        let notifications = NotificationCenter.default.publisher(for: .customNotification, object: nil)
    }
    
    // My View Model Class
    
    class NotificationsViewModel {
        // MARK: - Properties
    
        private(set) var notifications = PassthroughSubject<NotificationData, Never>()
        private(set) var cancellables: [AnyCancellable] = []
    
        init() {
            connectPublishers()
        }
    
        private func connectPublishers() {
            NotificationsService.shared.notifications
                .receive(on: DispatchQueue.main)
                .compactMap { $0.object as? NotificationData }
                .sink { [weak self] in self?.notifications.send($0) }
                .store(in: &cancellables)
        }
    }
    
    class NotificationsViewController: UIViewController {
        private let viewModel = NotificationsViewModel()
        private var cancellables: [AnyCancellable] = []
    
        // 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() {
            // await viewModel.fetchAllNotifications()
            viewModel.notifications
                .receive(on: DispatchQueue.main)
                .sink { [weak self] notificationData in
                    // … update model with `notificationData` object
                    self?.tableView.reloadData()
                }
                .store(in: &cancellables)
        }
    }
    
  4. 如果您有兴趣捕获过去通知的历史记录(这有点不寻常),我想您可以让

    NotificationsService
    也观察它自己的通知并构建它们的历史记录,以便视图模型可以“捕获上”:

    class NotificationsService {
        static let shared = NotificationsService()
    
        let notifications = NotificationCenter.default.publisher(for: .customNotification, object: nil)
        private(set) var history: [Notification] = []
        private var cancellables: [AnyCancellable] = []
    
        init() {
            notifications
                .receive(on: DispatchQueue.main)
                .sink { [weak self] self?.history.append($0) }
                .store(in: &cancellables)
        }
    }
    

    然后,视图模型可以公开这一点:

    extension NotificationsViewModel {
        var history: [NotificationData] {
            NotificationsService.shared.history
                .compactMap { $0.object as? NotificationData }
        }
    }
    

    就我个人而言,这种模式对我来说是不可持续的(你真的要捕捉永无止境的历史吗?!),但它捕捉了你在观察新通知之前发布的

    NotificationData
    历史的概念。

显然,在上面,我对

NotificationsService
做了很多假设(您没有与我们分享),所以不要迷失在这些细节中。关键是视图模型不应该有
@Published
存储属性,而只是由
NotificationsService
发布的传递通知。

© www.soinside.com 2019 - 2024. All rights reserved.