diff --git a/README.md b/README.md index 5132a99..9ad8e32 100644 --- a/README.md +++ b/README.md @@ -165,8 +165,8 @@ e.g. ### Model -* `let model: M` - The associated model object. This is typically a value type. +* `let modelStorage: M` + The associated storage for the model (typically a value type). * `let binding: BindingProxy` Read-write access to the model through `@Binding` in SwiftUI. @@ -193,13 +193,6 @@ won't pubblish any update. ### Combine Stores -* `func parent(type: T.Type) -> Store?` -Recursively traverse the parents until it founds one that matches the specified model type. - -* `var combine: AnyCombineStore? { get }` -Wraps a reference to its parent store (if applicable) and describes how this store should -be merged back. This is done by running `reconcile()` every time the model wrapped by -this store changes. * `func makeChildStore(keyPath: WritableKeyPath) -> Store` Used to express a parent-child relationship between two stores. @@ -212,11 +205,6 @@ struct Model { let items: [Item] } let store = Store(model: Model()) let child = store.makeChildStore(keyPath: \.[0]) ``` - This is equivalent to - ```swift -[...] -let child = Store(model: items[0], combine: CombineStore(parent: store, merge: .keyPath(\.[0]))) - ``` ### Transactions @@ -503,38 +491,3 @@ cancellable.cancel() ``` ▩ 𝙄𝙉𝙁𝙊 (-Lo4riSWZ3m5v1AvhgOb) INCREASE [✖ canceled] ``` - -### Combine Stores - -Support for children store (similar to Redux `combineStores`). - -```swift -struct Root { - struct Todo { - var name: String = "Untitled" - var done: Bool = false - } - struct Note { - var text: String = "" - var upvotes: Int = 0 - } - var todo: Todo = Todo() - var note: Note = Note() -} - -/// A child store pointing at the todo model. -var todoStore Store(model: model.todo, combine: CombineStore( - parent: rootStore, - notify: true, - merge: .keyPath(keyPath: \.todo))) - -extension Root.Todo { - struct Action_MarkAsDone: ActionProtocol { - func reduce(context: TransactionContext, Self>) { - defer { context.fulfill() } - context.reduceModel { $0.done = true } - } - } -} - -``` diff --git a/Sources/Store/store/CodableStore.swift b/Sources/Store/store/CodableStore.swift index 4ecd4ee..45d50c9 100644 --- a/Sources/Store/store/CodableStore.swift +++ b/Sources/Store/store/CodableStore.swift @@ -43,30 +43,19 @@ open class CodableStore: Store { /// - parameter diffing: The store diffing option. /// This will aftect how `lastTransactionDiff` is going to be produced. public init( - model: M, + modelStorage: ModelStorageBase, diffing: Diffing = .async ) { self.diffing = diffing - super.init(model: model) - self._lastModelSnapshot = CodableStore.encodeFlat(model: model) + super.init(modelStorage: modelStorage) + self._lastModelSnapshot = CodableStore.encodeFlat(model: modelStorage.model) } - - /// Constructs a new Store instance with a given initial model. - /// - /// - parameter model: The initial model state. - /// - parameter diffing: The store diffing option. - /// This will aftect how `lastTransactionDiff` is going to be produced. - /// - parameter combine: A associated parent store. Useful whenever it is desirable to merge - /// back changes from a child store to its parent. - public init

( + public convenience init( model: M, - diffing: Diffing = .async, - combine: CombineStore + diffing: Diffing = .async ) { - self.diffing = diffing - super.init(model: model, combine: combine) - self._lastModelSnapshot = CodableStore.encodeFlat(model: model) + self.init(modelStorage: ModelStorage(model: model), diffing: diffing) } // MARK: Model updates @@ -120,6 +109,16 @@ open class CodableStore: Store { } } + /// Creates a store for a subtree of this store model. e.g. + /// + /// - parameter keyPath: The keypath pointing at a subtree of the model object. + public func makeCodableChildStore( + keyPath: WritableKeyPath + ) -> CodableStore where M: Codable, C: Codable { + let childModelStorage: ModelStorageBase = modelStorage.makeChild(keyPath: keyPath) + return CodableStore(modelStorage: childModelStorage) + } + // MARK: - Model Encode/Decode /// Encodes the model into a dictionary. diff --git a/Sources/Store/store/CombineStore.swift b/Sources/Store/store/CombineStore.swift deleted file mode 100644 index c1a62a0..0000000 --- a/Sources/Store/store/CombineStore.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Foundation - -/// Represents a type-erased reference to a `CombineStore` object. -public protocol AnyCombineStore { - /// Reference to the parent store. - /// This is going to be the target for the `reconcile()` function. - var parentStore: AnyStore? { get } - /// Reconciles the child store with the parent (if applicable). - func reconcile() -} - -/// This class is used to express a parent-child relationship between two stores. -/// This is the case when it is desired to have a store (child) to manage to a subtree of the -/// store (parent) model. -/// `CombineStore` define a merge strategy to reconcile back the changes from the child to the -/// parent. -/// e.g. -/// ``` -/// struct Model { let items: [Item] } -/// let store = Store(model: Model()) -/// let child = store.makeChildStore(keyPath: \.[0]) -/// ``` -/// This is equivalent to -/// ``` -/// [...] -/// let child = Store( -/// model: items[0], -/// combine: CombineStore(parent: store, merge: .keyPath(\.[0]))) -/// ``` -public final class CombineStore: AnyCombineStore { - - public enum MergeStrategy { - /// The child store does not get reconciled with its parent. - case none - /// The child store gets reconciled with the given parent's keyPath. - case keyPath(keyPath: WritableKeyPath) - /// The child store gets reconciled by running the custom closure. - case merge(closure: (P, C) -> Void) - } - - /// Type-erased parent store. - public var parentStore: AnyStore? { parent } - - /// Whether the parent store should notify its observers when a child store merges back - /// its values. - public let notifyParentAfterReconciliation: Bool - - /// The desired merge strategy for child/parent model reconciliation. - public let mergeStrategy: MergeStrategy - - // Internal. - weak var parent: Store

? - weak var child: Store? - - /// - note: This constructor should be called only as an argument of `Store.init(model:combine)`. - public init(parent: Store

, notify: Bool = false, merge: MergeStrategy = .none) { - self.parent = parent - self.notifyParentAfterReconciliation = notify - self.mergeStrategy = merge - } - - /// Reconcile the model managed by the child store with the associated parent store using - /// the given `MergeStrategy`. - public func reconcile() { - func perform() { - guard let child = child, let parent = parent else { return } - switch mergeStrategy { - case .none: - return - case .keyPath(let keyPath): - parent.reduceModel { model in model[keyPath: keyPath] = child.model } - case .merge(let closure): - parent.reduceModel { model in closure(model, child.model) } - } - } - if !notifyParentAfterReconciliation { - parent?.performWithoutNotifyingObservers(perform) - } else { - perform() - } - } -} diff --git a/Sources/Store/store/ModelStorage.swift b/Sources/Store/store/ModelStorage.swift new file mode 100644 index 0000000..36a1189 --- /dev/null +++ b/Sources/Store/store/ModelStorage.swift @@ -0,0 +1,85 @@ +import Foundation +#if canImport(Combine) +import Combine +#else +import OpenCombine +import OpenCombineDispatch +#endif + +/// Abstract base class for `ModelStorage` and `ChildModelStorage`. +@dynamicMemberLookup +open class ModelStorageBase: ObservableObject { + + fileprivate init() { } + + /// A publisher that publishes changes from observable objects. + public let objectWillChange = ObservableObjectPublisher() + + /// (Internal only): Wrapped immutable model. + public var model: M { fatalError() } + + /// Managed acccess to the wrapped model. + open subscript(dynamicMember keyPath: WritableKeyPath) -> T { + fatalError() + } + + /// Thread-safe access to the underlying wrapped immutable model. + public func reduce(_ closure: (inout M) -> Void) { + fatalError() + } + + /// Returns a child model storage that points at a subtree of the immutable model wrapped by + /// this object. + public func makeChild(keyPath: WritableKeyPath) -> ModelStorageBase { + ChildModelStorage(parent: self, keyPath: keyPath) + } +} + +public final class ModelStorage: ModelStorageBase { + + override public var model: M { _model } + + override public final subscript(dynamicMember keyPath: WritableKeyPath) -> T { + get { _model[keyPath: keyPath] } + set { reduce { $0[keyPath: keyPath] = newValue } } + } + + private var _model: M + private var _modelLock = SpinLock() + + public init(model: M) { + _model = model + super.init() + } + + override public func reduce(_ closure: (inout M) -> Void) { + _modelLock.lock() + let new = assign(_model, changes: closure) + _model = new + _modelLock.unlock() + objectWillChange.send() + } +} + +public final class ChildModelStorage: ModelStorageBase { + + private let _parent: ModelStorageBase

+ private let _keyPath: WritableKeyPath + override public var model: M { _parent[dynamicMember: _keyPath] } + + public init(parent: ModelStorageBase

, keyPath: WritableKeyPath) { + _parent = parent + _keyPath = keyPath + super.init() + } + + override public final subscript(dynamicMember keyPath: WritableKeyPath) -> T { + get { model[keyPath: keyPath] } + set { reduce { $0[keyPath: keyPath] = newValue } } + } + + override public func reduce(_ closure: (inout M) -> Void) { + _parent.reduce { closure(&$0[keyPath: _keyPath]) } + objectWillChange.send() + } +} diff --git a/Sources/Store/store/Store.swift b/Sources/Store/store/Store.swift index acb25cd..469fbfa 100644 --- a/Sources/Store/store/Store.swift +++ b/Sources/Store/store/Store.swift @@ -13,13 +13,7 @@ import OpenCombineDispatch public protocol AnyStore: class { // MARK: Observation - - /// Notify the store observers for the change of this store. - /// `Store` and `CodableStore` are `ObservableObject`s and they automatically call this - /// function (that triggers a `objectWillChange` publlisher) every time the model changes. - /// - note: Observers are always scheduled on the main run loop. - func notifyObservers() - + /// The block passed as argument does not trigger any notification for the Store observers. /// e.g. By calling `reduceModel(transaction:closure:)` inside the `perform` block the store /// won't pubblish any update. @@ -41,16 +35,6 @@ public protocol AnyStore: class { /// Manually notify all of the registered middleware services. /// - note: See `MiddlewareType.onTransactionStateChange`. func notifyMiddleware(transaction: AnyTransaction) - - // MARK: Parent Store - - /// Recursively traverse the parents until it founds one that matches the specified model type. - func parent(type: T.Type) -> Store? - - /// Wraps a reference to its parent store (if applicable) and describes how this store should - /// be merged back. - /// This is done by running `reconcile()` every time the model wrapped by this store changes. - var combine: AnyCombineStore? { get } } /// Represents a store that has an typed associated model. @@ -61,7 +45,7 @@ public protocol ReducibleStore: AnyStore { /// The associated model object. /// -note: This is typically a value type. - var model: ModelType { get } + var modelStorage: ModelStorageBase { get } /// Atomically update the model and notifies all of the observers. func reduceModel(transaction: AnyTransaction?, closure: (inout ModelType) -> Void) @@ -92,7 +76,7 @@ public protocol ReducibleStore: AnyStore { /// } /// } /// ``` -open class Store: ReducibleStore, ObservableObject, Identifiable { +open class Store: ReducibleStore, Identifiable { /// A publisher that emits when the model has changed. public let objectWillChange = ObservableObjectPublisher() /// Used to have read-write access to the model through `@Binding` in SwiftUI. @@ -103,59 +87,62 @@ open class Store: ReducibleStore, ObservableObject, Identifiable { public var binding: BindingProxy! = nil // See `AnyStore`. - public let combine: AnyCombineStore? public var middleware: [Middleware] = [] - // See `ReducibleStore`. - public private(set) var model: M + public private(set) var modelStorage: ModelStorageBase // Private. - private var _stateLock = SpinLock() private var _performWithoutNotifyingObservers: Bool = false + private var _modelStorageObserver: AnyCancellable? /// Constructs a new Store instance with a given initial model. - public init(model: M) { - self.model = model - self.combine = nil + public convenience init(model: M) { + self.init(modelStorage: ModelStorage(model: model)) + } + + public init(modelStorage: ModelStorageBase) { + self.modelStorage = modelStorage self.binding = BindingProxy(store: self) register(middleware: LoggerMiddleware()) + + _modelStorageObserver = modelStorage.objectWillChange + .receive(on: RunLoop.main) + .sink { [weak self] in + guard let self = self else { return } + guard !self._performWithoutNotifyingObservers else { return } + self.objectWillChange.send() + } } - /// Constructs a new Store instance with a given initial model. + /// Creates a store for a subtree of this store model. e.g. + /// ``` + /// struct Subject { + /// struct Teacher { var name } + /// let title: String + /// let teacher: Teacher + /// } + /// let subjectStore = Store(model: Subject(...)) + /// let teacherStore = subjectStore.makeChild(keyPath: \.teacher) + /// ``` + /// When the child store is being updated the parent store (this object) will also trigger + /// a `objectWillChange` notification. /// - /// - parameter model: The initial model state. - /// - parameter combine: A associated parent store. Useful whenever it is desirable to merge - /// back changes from a child store to its parent. - public init

(model: M, combine: CombineStore) { - self.model = model - self.combine = combine - self.binding = BindingProxy(store: self) - register(middleware: LoggerMiddleware()) - combine.child = self - + /// - parameter keyPath: The keypath pointing at a subtree of the model object. + public func makeChildStore(keyPath: WritableKeyPath) -> Store { + let childModelStorage: ModelStorageBase = modelStorage.makeChild(keyPath: keyPath) + return Store(modelStorage: childModelStorage) } // MARK: Model updates open func reduceModel(transaction: AnyTransaction? = nil, closure: (inout M) -> Void) { - self._stateLock.lock() - let old = self.model - let new = assign(model, changes: closure) - self.model = new - self._stateLock.unlock() + let old = modelStorage.model + modelStorage.reduce(closure) + let new = modelStorage.model didUpdateModel(transaction: transaction, old: old, new: new) } /// Emits the `objectWillChange` event and propage the changes to its parent. /// - note: Call `super` implementation if you override this function. open func didUpdateModel(transaction: AnyTransaction?, old: M, new: M) { - combine?.reconcile() - notifyObservers() - } - - open func notifyObservers() { - guard !_performWithoutNotifyingObservers else { return } - RunLoop.main.schedule { - self.objectWillChange.send() - } } public func performWithoutNotifyingObservers(_ perform: () -> Void) { @@ -183,25 +170,6 @@ open class Store: ReducibleStore, ObservableObject, Identifiable { self.middleware.removeAll { $0 === middleware } } - // MARK: Parent Store - - public func makeChildStore(keyPath: WritableKeyPath) -> Store { - Store(model: model[keyPath: keyPath], combine: CombineStore( - parent: self, - notify: true, - merge: .keyPath(keyPath: keyPath))) - } - - public func parent(type: T.Type) -> Store? { - if let parent = combine?.parentStore as? Store { - return parent - } - if let parent = combine?.parentStore { - return parent.parent(type: type) - } - return nil - } - // MARK: Transactions /// Builds a transaction object for the action passed as argument. @@ -306,7 +274,7 @@ open class Store: ReducibleStore, ObservableObject, Identifiable { } public subscript(dynamicMember keyPath: WritableKeyPath) -> T { - get { store.model[keyPath: keyPath] } + get { store.modelStorage[dynamicMember: keyPath] } set { store.run(action: TemplateAction.Assign(keyPath, newValue), mode: .mainThread) } } } diff --git a/Sources/Store/transactions/Transaction.swift b/Sources/Store/transactions/Transaction.swift index e11cd99..d3f8473 100644 --- a/Sources/Store/transactions/Transaction.swift +++ b/Sources/Store/transactions/Transaction.swift @@ -124,9 +124,7 @@ public final class Transaction: AnyTransaction, Identifiable { /// Returns the asynchronous operation that is going to be executed with this transaction. public lazy var operation: AsyncOperation = { let operation = TransactionOperation(transaction: self) - operation._finishBlock = { [weak self] in - self?.store?.notifyObservers() - } + operation._finishBlock = { [weak self] in } return operation }() diff --git a/Tests/StoreTests/CombinedStores.swift b/Tests/StoreTests/ChildStoreTests.swift similarity index 62% rename from Tests/StoreTests/CombinedStores.swift rename to Tests/StoreTests/ChildStoreTests.swift index 6e4f583..49c5198 100644 --- a/Tests/StoreTests/CombinedStores.swift +++ b/Tests/StoreTests/ChildStoreTests.swift @@ -22,24 +22,15 @@ struct Root: Codable { class RootStore: CodableStore { // Children test. lazy var todoStore = { - Store(model: model.todo, combine: CombineStore( - parent: self, - notify: true, - merge: .keyPath(keyPath: \.todo))) + self.makeChildStore(keyPath: \.todo) }() lazy var noteStore = { - Store(model: model.note, combine: CombineStore( - parent: self, - notify: true, - merge: .keyPath(keyPath: \.note))) + self.makeChildStore(keyPath: \.note) }() lazy var listStore = { - Store(model: model.list, combine: CombineStore( - parent: self, - notify: true, - merge: .keyPath(keyPath: \.list))) + self.makeChildStore(keyPath: \.list) }() } @@ -89,7 +80,7 @@ extension Root.Todo { } @available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *) -final class CombinedStoreTests: XCTestCase { +final class ChildStoreTests: XCTestCase { var sink: AnyCancellable? @@ -102,11 +93,11 @@ final class CombinedStoreTests: XCTestCase { rootStore.noteStore.register(middleware: LoggerMiddleware()) rootStore.listStore.register(middleware: LoggerMiddleware()) - XCTAssertFalse(rootStore.model.todo.done) - XCTAssertFalse(rootStore.todoStore.model.done) + XCTAssertFalse(rootStore.modelStorage.todo.done) + XCTAssertFalse(rootStore.todoStore.modelStorage.done) rootStore.todoStore.run(action: Root.Todo.Action_MarkAsDone(), mode: .sync) - XCTAssertTrue(rootStore.todoStore.model.done) - XCTAssertTrue(rootStore.model.todo.done) + XCTAssertTrue(rootStore.todoStore.modelStorage.done) + XCTAssertTrue(rootStore.modelStorage.todo.done) } func testChildStoreChangesTriggersRootObserver() { @@ -118,13 +109,13 @@ final class CombinedStoreTests: XCTestCase { rootStore.listStore.register(middleware: LoggerMiddleware()) sink = rootStore.objectWillChange.sink { - XCTAssertTrue(rootStore.model.todo.done) - XCTAssertTrue(rootStore.todoStore.model.done) + XCTAssertTrue(rootStore.modelStorage.todo.done) + XCTAssertTrue(rootStore.todoStore.modelStorage.done) observerExpectation.fulfill() } rootStore.todoStore.run(action: Root.Todo.Action_MarkAsDone(), mode: .sync) - XCTAssertTrue(rootStore.todoStore.model.done) - XCTAssertTrue(rootStore.model.todo.done) + XCTAssertTrue(rootStore.todoStore.modelStorage.done) + XCTAssertTrue(rootStore.modelStorage.todo.done) waitForExpectations(timeout: 1) } @@ -136,36 +127,32 @@ final class CombinedStoreTests: XCTestCase { rootStore.register(middleware: LoggerMiddleware()) rootStore.listStore.register(middleware: LoggerMiddleware()) - XCTAssertTrue(rootStore.listStore.model.count == 1) - XCTAssertTrue(rootStore.listStore.model[0].name == "New") - XCTAssertTrue(rootStore.listStore.model[0].description == "New") - XCTAssertTrue(rootStore.listStore.model[0].done == false) - XCTAssertTrue(rootStore.model.list.count == 1) - XCTAssertTrue(rootStore.model.list[0].name == "New") - XCTAssertTrue(rootStore.model.list[0].description == "New") - XCTAssertTrue(rootStore.model.list[0].done == false) + XCTAssertTrue(rootStore.listStore.modelStorage.model.count == 1) + XCTAssertTrue(rootStore.listStore.modelStorage[0].name == "New") + XCTAssertTrue(rootStore.listStore.modelStorage[0].description == "New") + XCTAssertTrue(rootStore.listStore.modelStorage[0].done == false) + XCTAssertTrue(rootStore.modelStorage.list.count == 1) + XCTAssertTrue(rootStore.modelStorage.list[0].name == "New") + XCTAssertTrue(rootStore.modelStorage.list[0].description == "New") + XCTAssertTrue(rootStore.modelStorage.list[0].done == false) let listStore = rootStore.listStore - let todoStore = Store(model: listStore.model.first!, combine: CombineStore( - parent: listStore, - notify: true, - merge: .keyPath(keyPath: \.[0]))) - + let todoStore = listStore.makeChildStore(keyPath: \.[0]) todoStore.register(middleware: LoggerMiddleware()) todoStore.run(action: Root.Todo.Action_MarkAsDone(), mode: .sync) - XCTAssertTrue(todoStore.model.name == "New") - XCTAssertTrue(todoStore.model.description == "New") - XCTAssertTrue(todoStore.model.done == true) - XCTAssertTrue(rootStore.listStore.model.count == 1) - XCTAssertTrue(rootStore.listStore.model[0].name == "New") - XCTAssertTrue(rootStore.listStore.model[0].description == "New") - XCTAssertTrue(rootStore.listStore.model[0].done == true) - XCTAssertTrue(rootStore.model.list.count == 1) - XCTAssertTrue(rootStore.model.list[0].name == "New") - XCTAssertTrue(rootStore.model.list[0].description == "New") - XCTAssertTrue(rootStore.model.list[0].done == true) + XCTAssertTrue(todoStore.modelStorage.name == "New") + XCTAssertTrue(todoStore.modelStorage.description == "New") + XCTAssertTrue(todoStore.modelStorage.done == true) + XCTAssertTrue(rootStore.listStore.modelStorage.model.count == 1) + XCTAssertTrue(rootStore.listStore.modelStorage[0].name == "New") + XCTAssertTrue(rootStore.listStore.modelStorage[0].description == "New") + XCTAssertTrue(rootStore.listStore.modelStorage[0].done == true) + XCTAssertTrue(rootStore.modelStorage.list.count == 1) + XCTAssertTrue(rootStore.modelStorage.list[0].name == "New") + XCTAssertTrue(rootStore.modelStorage.list[0].description == "New") + XCTAssertTrue(rootStore.modelStorage.list[0].done == true) } } diff --git a/Tests/StoreTests/StoreTests.swift b/Tests/StoreTests/StoreTests.swift index 8c2a329..83f3c17 100644 --- a/Tests/StoreTests/StoreTests.swift +++ b/Tests/StoreTests/StoreTests.swift @@ -13,7 +13,7 @@ final class StoreTests: XCTestCase { store.register(middleware: LoggerMiddleware()) store.run(action: TestAction.increase(amount: 42)) { error in XCTAssert(error == nil) - XCTAssert(store.model.count == 42) + XCTAssert(store.modelStorage.count == 42) transactionExpectation.fulfill() } waitForExpectations(timeout: 1) @@ -28,7 +28,7 @@ final class StoreTests: XCTestCase { TestAction.increase(amount: 1), TestAction.increase(amount: 1), ]) { context in - XCTAssert(store.model.count == 3) + XCTAssert(store.modelStorage.count == 3) transactionExpectation.fulfill() } waitForExpectations(timeout: 10) @@ -38,18 +38,18 @@ final class StoreTests: XCTestCase { let store = CodableStore(model: TestModel(), diffing: .sync) store.register(middleware: LoggerMiddleware()) store.run(action: TestAction.updateLabel(newLabel: "Bar"), mode: .sync) - XCTAssert(store.model.label == "Bar") - XCTAssert(store.model.nested.label == "Bar") + XCTAssert(store.modelStorage.label == "Bar") + XCTAssert(store.modelStorage.nested.label == "Bar") store.run(action: TestAction.updateLabel(newLabel: "Foo"), mode: .sync) - XCTAssert(store.model.label == "Foo") - XCTAssert(store.model.nested.label == "Foo") + XCTAssert(store.modelStorage.label == "Foo") + XCTAssert(store.modelStorage.nested.label == "Foo") } func testAccessNestedKeyPathInArray() { let store = CodableStore(model: TestModel(), diffing: .sync) store.register(middleware: LoggerMiddleware()) store.run(action: TestAction.setArray(index: 1, value: "Foo"), mode: .sync) - XCTAssert(store.model.array[1].label == "Foo") + XCTAssert(store.modelStorage.array[1].label == "Foo") } func testDiffResult() { @@ -74,7 +74,7 @@ final class StoreTests: XCTestCase { let transaction = store.transaction(action: TestAction.increase(amount: 1)) sink = transaction.$state.sink { state in XCTAssert(state != .completed) - XCTAssert(store.model.count == 0) + XCTAssert(store.modelStorage.count == 0) if state == .canceled { transactionExpectation.fulfill() } @@ -93,7 +93,7 @@ final class StoreTests: XCTestCase { sink = store.futureOf(action: action1) .replaceError(with: ()) .sink { - XCTAssert(store.model.count == 5) + XCTAssert(store.modelStorage.count == 5) transactionExpectation.fulfill() } waitForExpectations(timeout: 1) @@ -102,7 +102,7 @@ final class StoreTests: XCTestCase { func testAccessToBindingProxy() { let store = CodableStore(model: TestModel(), diffing: .sync) store.binding.count = 3 - XCTAssert(store.model.count == 3) + XCTAssert(store.modelStorage.count == 3) } static var allTests = [