当 Store 的对象更新时,自动触发 ViewModel ObservableObjects 中的 objectWillChange.send()
When a Store's object is updated, auto-trigger objectWillChange.send() in ViewModel ObservableObjects
对于使用 Combine 和 SwiftUI 的 Store/Factory/ViewModel 模式,我想要一个符合 Store 协议的 class 在指定模型对象更改内部属性时公开发布者。然后任何订阅的 ViewModel 都可以触发 objectWillChange 来显示更改。
(这是必要的,因为通过引用传递的模型对象 中的更改被忽略 ,因此 @Published/ObservableObject 不会为工厂传递自动触发Store-owned 模型。它可以在 Store 和 VM 中调用 objectWillChange,但这会排除任何被动监听的 VM。)
这是一个委托模式,对吧,将@Published/ObservableObject 扩展到按引用传递的对象?通过结合博客、书籍和文档进行梳理并没有引发什么可能是非常标准的想法。
粗暴的工作尝试
我认为如果我在外部公开 VM 的 objectWillChange PassthroughSubject 会很有用,但是 PassthroughSubject.send() 将为模型对象中的每个对象触发。可能很浪费(尽管 ViewModel 只触发它的 objectWillChange 一次)。
在 Ext+VM republishChanges(of myStore: Store)
上附加一个限制器(例如节流阀、removeDuplicates)似乎并没有限制 .sink 调用,我也没有看到一个明显的方法来重置 PassthroughSubject 和 VM 的接收器之间的需求... 或了解如何将订阅者附加到符合协议的 PassthroughSubject。有什么建议吗?
店面
struct Library {
var books: // some dictionary
}
class LocalLibraryStore: LibraryStore {
private(set) var library: Library {
didSet { publish() }
}
var changed = PassthroughSubject<Any,Never>()
func removeBook() {}
}
protocol LibraryStore: Store {
var changed: PassthroughSubject<Any,Never> { get }
var library: Library { get }
}
protocol Store {
var changed: PassthroughSubject<Any,Never> { get }
}
extension Store {
func publish() {
changed.send(1)
print("This will fire once.")
}
}
虚拟机端
class BadgeVM: VM {
init(store: LibraryStore) {
self.specificStore = store
republishChanges(of: jokesStore)
}
var objectWillChange = ObservableObjectPublisher() // Exposed {set} for external call
internal var subscriptions = Set<AnyCancellable>()
@Published private var specificStore: LibraryStore
var totalBooks: Int { specificStore.library.books.keys.count }
}
protocol VM: ObservableObject {
var subscriptions: Set<AnyCancellable> { get set }
var objectWillChange: ObservableObjectPublisher { get set }
}
extension VM {
internal func republishChanges(of myStore: Store) {
myStore.changed
// .throttle() doesn't silence as hoped
.sink { [unowned self] _ in
print("Executed for each object inside the Store's published object.")
self.objectWillChange.send()
}
.store(in: &subscriptions)
}
}
class OtherVM: VM {
init(store: LibraryStore) {
self.specificStore = store
republishChanges(of: store)
}
var objectWillChange = ObservableObjectPublisher() // Exposed {set} for external call
internal var subscriptions = Set<AnyCancellable>()
@Published private var specificStore: LibraryStore
var isBookVeryExpensive: Bool { ... }
func bookMysteriouslyDisappears() {
specificStore.removeBook()
}
}
看来你想要的是一种在其内部属性发生变化时发出通知的类型。这听起来很像 ObservableObject
所做的事情。
所以,让你的 Store
协议继承自 ObservableObject
:
protocol Store: ObservableObject {}
然后符合 Store
的类型可以决定要通知哪些属性,例如 @Published
:
class StringStore: Store {
@Published var text: String = ""
}
其次,您希望您的视图模型在商店通知他们时自动触发他们的 objectWillChange
发布者。
自动部分可以使用基础 class 来完成 - 而不是使用协议 - 因为它需要存储订阅。您可以保留协议要求,如果您需要:
protocol VM {
associatedtype S: Store
var store: S { get }
}
class BaseVM<S: Store>: ObservableObject, VM {
var c : AnyCancellable? = nil
let store: S
init(store: S) {
self.store = store
c = self.store.objectWillChange.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
}
class MainVM: BaseVM<StringStore> {
// ...
}
这是一个如何使用它的例子:
let stringStore = StringStore();
let mainVm = MainVM(store: stringStore)
// this is conceptually what @ObservedObject does
let c = mainVm.objectWillChange.sink {
print("change!") // this will fire after next line
}
stringStore.text = "new text"
感谢@NewDev 指出子类化是一条更聪明的路线。
如果你想嵌套 ObservableObjects 或让 ObservableObject re-publish 改变传递给它的对象中的对象,这种方法比我的问题使用更少的代码。
在搜索使用 属性 包装器进一步简化(获取父 objectWillChange 并进一步简化它)时,我注意到此线程中的类似方法:。这仅在使用可变参数时有所不同。
定义 VM 和 Store/Repo 类
import Foundation
import Combine
class Repo: ObservableObject {
func publish() {
objectWillChange.send()
}
}
class VM: ObservableObject {
private var repoSubscriptions = Set<AnyCancellable>()
init(subscribe repos: Repo...) {
repos.forEach { repo in
repo.objectWillChange
.receive(on: DispatchQueue.main) // Optional
.sink(receiveValue: { [weak self] _ in
self?.objectWillChange.send()
})
.store(in: &repoSubscriptions)
}
}
}
示例实现
- 回购:将 didSet { publish() } 添加到模型对象
- VM:super.init() 接受任意数量的 repos 以重新发布
import Foundation
class UserDirectoriesRepo: Repo, DirectoriesRepository {
init(persistence: Persistence) {
self.userDirs = persistence.loadDirectories()
self.persistence = persistence
super.init()
restoreBookmarksAccess()
}
private var userDirs: UserDirectories {
didSet { publish() }
}
var someExposedSliceOfTheModel: [RootDirectory] {
userDirs.rootDirectories.filter { [=11=].restoredURL != nil }
}
...
}
import Foundation
class FileStructureVM: VM {
init(directoriesRepo: DirectoriesRepository) {
self.repo = directoriesRepo
super.init(subscribe: directoriesRepo)
}
@Published // No longer necessary
private var repo: DirectoriesRepository
var rootDirectories: [RootDirectory] {
repo.rootDirectories.sorted ...
}
...
}
对于使用 Combine 和 SwiftUI 的 Store/Factory/ViewModel 模式,我想要一个符合 Store 协议的 class 在指定模型对象更改内部属性时公开发布者。然后任何订阅的 ViewModel 都可以触发 objectWillChange 来显示更改。
(这是必要的,因为通过引用传递的模型对象 中的更改被忽略 ,因此 @Published/ObservableObject 不会为工厂传递自动触发Store-owned 模型。它可以在 Store 和 VM 中调用 objectWillChange,但这会排除任何被动监听的 VM。)
这是一个委托模式,对吧,将@Published/ObservableObject 扩展到按引用传递的对象?通过结合博客、书籍和文档进行梳理并没有引发什么可能是非常标准的想法。
粗暴的工作尝试
我认为如果我在外部公开 VM 的 objectWillChange PassthroughSubject
在 Ext+VM republishChanges(of myStore: Store)
上附加一个限制器(例如节流阀、removeDuplicates)似乎并没有限制 .sink 调用,我也没有看到一个明显的方法来重置 PassthroughSubject 和 VM 的接收器之间的需求... 或了解如何将订阅者附加到符合协议的 PassthroughSubject。有什么建议吗?
店面
struct Library {
var books: // some dictionary
}
class LocalLibraryStore: LibraryStore {
private(set) var library: Library {
didSet { publish() }
}
var changed = PassthroughSubject<Any,Never>()
func removeBook() {}
}
protocol LibraryStore: Store {
var changed: PassthroughSubject<Any,Never> { get }
var library: Library { get }
}
protocol Store {
var changed: PassthroughSubject<Any,Never> { get }
}
extension Store {
func publish() {
changed.send(1)
print("This will fire once.")
}
}
虚拟机端
class BadgeVM: VM {
init(store: LibraryStore) {
self.specificStore = store
republishChanges(of: jokesStore)
}
var objectWillChange = ObservableObjectPublisher() // Exposed {set} for external call
internal var subscriptions = Set<AnyCancellable>()
@Published private var specificStore: LibraryStore
var totalBooks: Int { specificStore.library.books.keys.count }
}
protocol VM: ObservableObject {
var subscriptions: Set<AnyCancellable> { get set }
var objectWillChange: ObservableObjectPublisher { get set }
}
extension VM {
internal func republishChanges(of myStore: Store) {
myStore.changed
// .throttle() doesn't silence as hoped
.sink { [unowned self] _ in
print("Executed for each object inside the Store's published object.")
self.objectWillChange.send()
}
.store(in: &subscriptions)
}
}
class OtherVM: VM {
init(store: LibraryStore) {
self.specificStore = store
republishChanges(of: store)
}
var objectWillChange = ObservableObjectPublisher() // Exposed {set} for external call
internal var subscriptions = Set<AnyCancellable>()
@Published private var specificStore: LibraryStore
var isBookVeryExpensive: Bool { ... }
func bookMysteriouslyDisappears() {
specificStore.removeBook()
}
}
看来你想要的是一种在其内部属性发生变化时发出通知的类型。这听起来很像 ObservableObject
所做的事情。
所以,让你的 Store
协议继承自 ObservableObject
:
protocol Store: ObservableObject {}
然后符合 Store
的类型可以决定要通知哪些属性,例如 @Published
:
class StringStore: Store {
@Published var text: String = ""
}
其次,您希望您的视图模型在商店通知他们时自动触发他们的 objectWillChange
发布者。
自动部分可以使用基础 class 来完成 - 而不是使用协议 - 因为它需要存储订阅。您可以保留协议要求,如果您需要:
protocol VM {
associatedtype S: Store
var store: S { get }
}
class BaseVM<S: Store>: ObservableObject, VM {
var c : AnyCancellable? = nil
let store: S
init(store: S) {
self.store = store
c = self.store.objectWillChange.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
}
class MainVM: BaseVM<StringStore> {
// ...
}
这是一个如何使用它的例子:
let stringStore = StringStore();
let mainVm = MainVM(store: stringStore)
// this is conceptually what @ObservedObject does
let c = mainVm.objectWillChange.sink {
print("change!") // this will fire after next line
}
stringStore.text = "new text"
感谢@NewDev 指出子类化是一条更聪明的路线。
如果你想嵌套 ObservableObjects 或让 ObservableObject re-publish 改变传递给它的对象中的对象,这种方法比我的问题使用更少的代码。
在搜索使用 属性 包装器进一步简化(获取父 objectWillChange 并进一步简化它)时,我注意到此线程中的类似方法:
定义 VM 和 Store/Repo 类
import Foundation
import Combine
class Repo: ObservableObject {
func publish() {
objectWillChange.send()
}
}
class VM: ObservableObject {
private var repoSubscriptions = Set<AnyCancellable>()
init(subscribe repos: Repo...) {
repos.forEach { repo in
repo.objectWillChange
.receive(on: DispatchQueue.main) // Optional
.sink(receiveValue: { [weak self] _ in
self?.objectWillChange.send()
})
.store(in: &repoSubscriptions)
}
}
}
示例实现
- 回购:将 didSet { publish() } 添加到模型对象
- VM:super.init() 接受任意数量的 repos 以重新发布
import Foundation
class UserDirectoriesRepo: Repo, DirectoriesRepository {
init(persistence: Persistence) {
self.userDirs = persistence.loadDirectories()
self.persistence = persistence
super.init()
restoreBookmarksAccess()
}
private var userDirs: UserDirectories {
didSet { publish() }
}
var someExposedSliceOfTheModel: [RootDirectory] {
userDirs.rootDirectories.filter { [=11=].restoredURL != nil }
}
...
}
import Foundation
class FileStructureVM: VM {
init(directoriesRepo: DirectoriesRepository) {
self.repo = directoriesRepo
super.init(subscribe: directoriesRepo)
}
@Published // No longer necessary
private var repo: DirectoriesRepository
var rootDirectories: [RootDirectory] {
repo.rootDirectories.sorted ...
}
...
}