Skip to content

Commit

Permalink
Introduce ModelStorage.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Usbergo committed Jan 31, 2021
1 parent f92b894 commit 41dd6f5
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 277 deletions.
51 changes: 2 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<M>`
Read-write access to the model through `@Binding` in SwiftUI.
Expand All @@ -193,13 +193,6 @@ won't pubblish any update.

### Combine Stores

* `func parent<T>(type: T.Type) -> Store<T>?`
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<C>(keyPath: WritableKeyPath<M, C>) -> Store<C>`
Used to express a parent-child relationship between two stores.
Expand All @@ -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

Expand Down Expand Up @@ -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<Store<Root.Todo>, Self>) {
defer { context.fulfill() }
context.reduceModel { $0.done = true }
}
}
}

```
33 changes: 16 additions & 17 deletions Sources/Store/store/CodableStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,30 +43,19 @@ open class CodableStore<M: Codable>: Store<M> {
/// - parameter diffing: The store diffing option.
/// This will aftect how `lastTransactionDiff` is going to be produced.
public init(
model: M,
modelStorage: ModelStorageBase<M>,
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<P>(
public convenience init(
model: M,
diffing: Diffing = .async,
combine: CombineStore<P, M>
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
Expand Down Expand Up @@ -120,6 +109,16 @@ open class CodableStore<M: Codable>: Store<M> {
}
}

/// 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<C>(
keyPath: WritableKeyPath<M, C>
) -> CodableStore<C> where M: Codable, C: Codable {
let childModelStorage: ModelStorageBase<C> = modelStorage.makeChild(keyPath: keyPath)
return CodableStore<C>(modelStorage: childModelStorage)
}

// MARK: - Model Encode/Decode

/// Encodes the model into a dictionary.
Expand Down
82 changes: 0 additions & 82 deletions Sources/Store/store/CombineStore.swift

This file was deleted.

85 changes: 85 additions & 0 deletions Sources/Store/store/ModelStorage.swift
Original file line number Diff line number Diff line change
@@ -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<M>: 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<T>(dynamicMember keyPath: WritableKeyPath<M, T>) -> 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<N>(keyPath: WritableKeyPath<M, N>) -> ModelStorageBase<N> {
ChildModelStorage(parent: self, keyPath: keyPath)
}
}

public final class ModelStorage<M>: ModelStorageBase<M> {

override public var model: M { _model }

override public final subscript<T>(dynamicMember keyPath: WritableKeyPath<M, T>) -> 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<P, M>: ModelStorageBase<M> {

private let _parent: ModelStorageBase<P>
private let _keyPath: WritableKeyPath<P, M>
override public var model: M { _parent[dynamicMember: _keyPath] }

public init(parent: ModelStorageBase<P>, keyPath: WritableKeyPath<P, M>) {
_parent = parent
_keyPath = keyPath
super.init()
}

override public final subscript<T>(dynamicMember keyPath: WritableKeyPath<M, T>) -> 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()
}
}
Loading

0 comments on commit 41dd6f5

Please sign in to comment.