Skip to content

Commit aa4e884

Browse files
authored
V4.0.0 (#25)
* Add breaking changes for 4.0.0 * Improve logging and test benchmarks for issues * Test removing main from didChange * Update c to bug/ARC-issue branch * Protect setting effect task with lock * Add weak for scoped Store * Use errors from c * Resolve memory issue * Update c to 3.0 and remove debug tests * Uncomment temp removed code * Add back main queue receive on * Update StoreView to improve performance * Switch Binding param order * Update README * Add StoreContentTests
1 parent 75774cd commit aa4e884

File tree

9 files changed

+180
-101
lines changed

9 files changed

+180
-101
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ let package = Package(
2121
// Dependencies declare other packages that this package depends on.
2222
.package(
2323
url: "https://github.com/0xOpenBytes/c",
24-
from: "1.1.1"
24+
from: "3.0.0"
2525
),
2626
.package(
2727
url: "https://github.com/0xLeif/swift-custom-dump",

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ let actionHandler = StoreActionHandler<StoreKey, Action, Dependency> { cacheStor
107107
struct ContentView: View {
108108
@ObservedObject var store: Store<StoreKey, Action, Dependency> = .init(
109109
initialValues: [
110-
.url: URL(string: "https://jsonplaceholder.typicode.com/posts") as Any
110+
.url: URL(string: "https://jsonplaceholder.typicode.com/posts")!
111111
],
112112
actionHandler: actionHandler,
113113
dependency: .live

Sources/CacheStore/Stores/CacheStore/CacheStore.swift

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,14 @@ import SwiftUI
88
/// An `ObservableObject` that has a `cache` which is the source of truth for this object
99
open class CacheStore<Key: Hashable>: ObservableObject, Cacheable {
1010
/// `Error` that reports the missing keys for the `CacheStore`
11-
public struct MissingRequiredKeysError<Key: Hashable>: LocalizedError {
12-
/// Required keys
13-
public let keys: Set<Key>
14-
15-
/// init for `MissingRequiredKeysError<Key>`
16-
public init(keys: Set<Key>) {
17-
self.keys = keys
18-
}
19-
20-
/// Error description for `LocalizedError`
21-
public var errorDescription: String? {
22-
"Missing Required Keys: \(keys.map { "\($0)" }.joined(separator: ", "))"
23-
}
24-
}
11+
public typealias MissingRequiredKeysError = c.MissingRequiredKeysError
12+
13+
/// `Error` that reports the expected type for a value in the `CacheStore`
14+
public typealias InvalidTypeError = c.InvalidTypeError
2515

2616
private var lock: NSLock
2717
@Published var cache: [Key: Any]
28-
29-
/// The values in the `cache` of type `Any`
30-
public var valuesInCache: [Key: Any] { cache }
31-
18+
3219
/// init for `CacheStore<Key>`
3320
required public init(initialValues: [Key: Any]) {
3421
lock = NSLock()
@@ -63,7 +50,17 @@ open class CacheStore<Key: Hashable>: ObservableObject, Cacheable {
6350
}
6451

6552
/// Resolve the `Value` for the `Key` by force casting `get`
66-
public func resolve<Value>(_ key: Key, as: Value.Type = Value.self) -> Value { get(key)! }
53+
public func resolve<Value>(_ key: Key, as: Value.Type = Value.self) throws -> Value {
54+
guard contains(key) else {
55+
throw MissingRequiredKeysError(keys: [key])
56+
}
57+
58+
guard let value: Value = get(key) else {
59+
throw InvalidTypeError(expectedType: Value.self, actualValue: get(key))
60+
}
61+
62+
return value
63+
}
6764

6865
/// Set the `Value` for the `Key`
6966
public func set<Value>(value: Value, forKey key: Key) {
@@ -190,10 +187,11 @@ public extension CacheStore {
190187
/// Creates a `Binding` for the given `Key`
191188
func binding<Value>(
192189
_ key: Key,
193-
as: Value.Type = Value.self
190+
as: Value.Type = Value.self,
191+
fallback: Value
194192
) -> Binding<Value> {
195193
Binding(
196-
get: { self.resolve(key) },
194+
get: { self.get(key) ?? fallback },
197195
set: { self.set(value: $0, forKey: key) }
198196
)
199197
}

Sources/CacheStore/Stores/Store/Content/StoreView.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,22 @@ public protocol StoreView: View {
1010
associatedtype Dependency
1111
/// The content the View cares about and uses
1212
associatedtype Content: StoreContent
13-
13+
/// The View created from the current Content
14+
associatedtype ContentView: View
15+
1416
/// An `ObservableObject` that uses actions to modify the state which is a `CacheStore`
1517
var store: Store<Key, Action, Dependency> { get set }
16-
/// The content a StoreView uses when creating SwiftUI views
17-
var content: Content { get }
18-
18+
1919
init(store: Store<Key, Action, Dependency>)
20+
21+
/// Create the body view with the current Content of the Store. View's body property is defaulted to using this function.
22+
/// - Parameters:
23+
/// - content: The content a StoreView uses when creating SwiftUI views
24+
func body(content: Content) -> ContentView
2025
}
2126

2227
public extension StoreView where Content.Key == Key {
23-
var content: Content { store.content() }
28+
var body: some View {
29+
body(content: store.content())
30+
}
2431
}

Sources/CacheStore/Stores/Store/Store.swift

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,16 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
1010
private var isDebugging: Bool
1111
private var cacheStoreObserver: AnyCancellable?
1212
private var effects: [AnyHashable: Task<(), Never>]
13-
14-
var cacheStore: CacheStore<Key>
15-
var actionHandler: StoreActionHandler<Key, Action, Dependency>
16-
let dependency: Dependency
13+
private(set) var cacheStore: CacheStore<Key>
14+
private(set) var actionHandler: StoreActionHandler<Key, Action, Dependency>
15+
private let dependency: Dependency
1716

1817
/// The values in the `cache` of type `Any`
19-
public var valuesInCache: [Key: Any] {
18+
public var allValues: [Key: Any] {
2019
lock.lock()
2120
defer { lock.unlock() }
2221

23-
return cacheStore.valuesInCache
22+
return cacheStore.allValues
2423
}
2524

2625
/// A publisher for the private `cache` that is mapped to a CacheStore
@@ -32,7 +31,7 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
3231
}
3332

3433
/// An identifier of the Store and CacheStore
35-
var debugIdentifier: String {
34+
public var debugIdentifier: String {
3635
lock.lock()
3736
defer { lock.unlock() }
3837

@@ -66,11 +65,11 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
6665
cacheStore = CacheStore(initialValues: initialValues)
6766
self.actionHandler = actionHandler
6867
self.dependency = dependency
69-
cacheStoreObserver = publisher.sink { [weak self] _ in
70-
DispatchQueue.main.async {
68+
cacheStoreObserver = cacheStore.$cache
69+
.receive(on: DispatchQueue.main)
70+
.sink { [weak self] _ in
7171
self?.objectWillChange.send()
7272
}
73-
}
7473
}
7574

7675
/// Get the value in the `cache` using the `key`. This returns an optional value. If the value is `nil`, that means either the value doesn't exist or the value is not able to be casted as `Value`.
@@ -82,11 +81,11 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
8281
}
8382

8483
/// Resolve the value in the `cache` using the `key`. This function uses `get` and force casts the value. This should only be used when you know the value is always in the `cache`.
85-
public func resolve<Value>(_ key: Key, as: Value.Type = Value.self) -> Value {
84+
public func resolve<Value>(_ key: Key, as: Value.Type = Value.self) throws -> Value {
8685
lock.lock()
8786
defer { lock.unlock() }
8887

89-
return cacheStore.resolve(key)
88+
return try cacheStore.resolve(key)
9089
}
9190

9291
/// Checks to make sure the cache has the required keys, otherwise it will throw an error
@@ -125,6 +124,9 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
125124

126125
/// Cancel an effect with the ID
127126
public func cancel(id: AnyHashable) {
127+
lock.lock()
128+
defer { lock.unlock() }
129+
128130
effects[id]?.cancel()
129131
effects[id] = nil
130132
}
@@ -171,11 +173,11 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
171173

172174
scopedStore.cacheStore = scopedCacheStore
173175
scopedStore.parentStore = self
174-
scopedStore.actionHandler = StoreActionHandler { (store: inout CacheStore<ScopedKey>, action: ScopedAction, dependency: ScopedDependency) in
176+
scopedStore.actionHandler = StoreActionHandler { [weak scopedStore] (store: inout CacheStore<ScopedKey>, action: ScopedAction, dependency: ScopedDependency) in
175177
let effect = actionHandler.handle(store: &store, action: action, dependency: dependency)
176178

177179
if let parentAction = actionTransformation(action) {
178-
scopedStore.parentStore?.handle(action: parentAction)
180+
scopedStore?.parentStore?.handle(action: parentAction)
179181
}
180182

181183
return effect
@@ -208,11 +210,11 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
208210
/// Creates a `Binding` for the given `Key` using an `Action` to set the value
209211
public func binding<Value>(
210212
_ key: Key,
211-
as: Value.Type = Value.self,
213+
fallback: Value,
212214
using: @escaping (Value) -> Action
213215
) -> Binding<Value> {
214216
Binding(
215-
get: { self.resolve(key) },
217+
get: { self.get(key) ?? fallback },
216218
set: { self.handle(action: using($0)) }
217219
)
218220
}
@@ -233,6 +235,18 @@ open class Store<Key: Hashable, Action, Dependency>: ObservableObject, ActionHan
233235
// MARK: - Void Dependency
234236

235237
public extension Store where Dependency == Void {
238+
/// init for `Store<Key, Action, Void>`
239+
convenience init(
240+
initialValues: [Key: Any],
241+
actionHandler: StoreActionHandler<Key, Action, Dependency>
242+
) {
243+
self.init(
244+
initialValues: initialValues,
245+
actionHandler: actionHandler,
246+
dependency: ()
247+
)
248+
}
249+
236250
/// Creates a `ScopedStore`
237251
func scope<ScopedKey: Hashable, ScopedAction>(
238252
keyTransformation: BiDirectionalTransformation<Key?, ScopedKey?>,
@@ -285,15 +299,20 @@ extension Store {
285299

286300
if let actionEffect = actionEffect {
287301
cancel(id: actionEffect.id)
288-
effects[actionEffect.id] = Task {
302+
let effectTask = Task { [weak self] in
303+
defer { self?.cancel(id: actionEffect.id) }
304+
289305
if Task.isCancelled { return }
290-
306+
291307
guard let nextAction = await actionEffect.effect() else { return }
292-
308+
293309
if Task.isCancelled { return }
294-
295-
handle(action: nextAction)
310+
311+
self?.handle(action: nextAction)
296312
}
313+
lock.lock()
314+
effects[actionEffect.id] = effectTask
315+
lock.unlock()
297316
}
298317

299318
if isDebugging {
@@ -303,7 +322,7 @@ extension Store {
303322
--------------- State Output ------------
304323
"""
305324
)
306-
325+
307326
if cacheStore.isCacheEqual(to: cacheStoreCopy) {
308327
print("\t🙅 No State Change")
309328
} else {
@@ -329,15 +348,15 @@ extension Store {
329348
)
330349
}
331350
}
332-
351+
333352
print(
334353
"""
335354
--------------- State End ---------------
336355
[\(formattedDate)] 🏁 End Action: \(customDump(action)) \(debugIdentifier)
337356
"""
338357
)
339358
}
340-
359+
341360
cacheStore.cache = cacheStoreCopy.cache
342361

343362
return actionEffect
@@ -359,7 +378,7 @@ extension Store {
359378

360379
var updatedStateChanges: [String] = []
361380

362-
for (key, value) in updatedStore.valuesInCache {
381+
for (key, value) in updatedStore.allValues {
363382
let isValueEqual: Bool = cacheStore.isValueEqual(toUpdatedValue: value, forKey: key)
364383
let valueInfo: String = "\(type(of: value))"
365384
let valueOutput: String

Sources/CacheStore/Stores/Store/TestStore.swift

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ open class TestStore<Key: Hashable, Action, Dependency> {
3333
file: StaticString = #filePath,
3434
line: UInt = #line
3535
) {
36-
self.store = store.debug
36+
self.store = store
3737
effects = []
3838
initFile = file
3939
initLine = line
@@ -47,11 +47,18 @@ open class TestStore<Key: Hashable, Action, Dependency> {
4747
file: StaticString = #filePath,
4848
line: UInt = #line
4949
) {
50-
store = Store(initialValues: initialValues, actionHandler: actionHandler, dependency: dependency).debug
50+
store = Store(initialValues: initialValues, actionHandler: actionHandler, dependency: dependency)
5151
effects = []
5252
initFile = file
5353
initLine = line
5454
}
55+
56+
/// Modifies and returns the `TestStore` with debugging mode on
57+
public var debug: Self {
58+
_ = store.debug
59+
60+
return self
61+
}
5562

5663
/// Send an action and provide an expectation for the changes from handling the action
5764
public func send(
@@ -61,7 +68,7 @@ open class TestStore<Key: Hashable, Action, Dependency> {
6168
expecting: (inout CacheStore<Key>) throws -> Void
6269
) {
6370
var expectedCacheStore = store.cacheStore.copy()
64-
71+
6572
let actionEffect = store.send(action)
6673

6774
do {
@@ -70,32 +77,31 @@ open class TestStore<Key: Hashable, Action, Dependency> {
7077
TestStoreFailure.handler("❌ Expectation failed", file, line)
7178
return
7279
}
73-
80+
7481
guard expectedCacheStore.isCacheEqual(to: store.cacheStore) else {
7582
TestStoreFailure.handler(
7683
"""
7784
❌ Expectation failed
7885
--- Expected ---
79-
\(customDump(expectedCacheStore.valuesInCache))
86+
\(customDump(expectedCacheStore.allValues))
8087
----------------
8188
****************
8289
---- Actual ----
83-
\(customDump(store.cacheStore.valuesInCache))
90+
\(customDump(store.cacheStore.allValues))
8491
----------------
8592
""",
8693
file,
8794
line
8895
)
8996
return
9097
}
91-
92-
98+
9399
if let actionEffect = actionEffect {
94100
let predicate: (ActionEffect<Action>) -> Bool = { $0.id == actionEffect.id }
95101
if effects.contains(where: predicate) {
96102
effects.removeAll(where: predicate)
97103
}
98-
104+
99105
effects.append(actionEffect)
100106
}
101107
}

0 commit comments

Comments
 (0)