我在使用
@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()
。但它们看起来都不干净。
有几个问题:
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
数组初始化之前不会开始接收更新。
您正在观察一组
NotificationData
,但我们通常会建立一个发布者,在它们进入时发布单独的 NotificationData
有效负载,而不是整个数组。
视图模型正在从
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)
}
}
如果您有兴趣捕获过去通知的历史记录(这有点不寻常),我想您可以让
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
发布的传递通知。