Skip to content

Commit 7ba09d2

Browse files
committed
Enables customizing name of nested spice store
1 parent a9f3085 commit 7ba09d2

8 files changed

Lines changed: 54 additions & 49 deletions

File tree

‎Examples/SwiftUIExample/AppSpiceStore.swift‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ final class AppSpiceStore: SpiceStore {
1313
try await Task.sleep(for: .seconds(1))
1414
URLCache.shared.removeAllCachedResponses()
1515
}
16-
17-
let featureFlags = FeatureFlagsSpiceStore()
16+
@Spice var featureFlags = FeatureFlagsSpiceStore()
1817
}
1918

2019
final class FeatureFlagsSpiceStore: SpiceStore {

‎Examples/UIKitExample/AppSpiceStore.swift‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ final class AppSpiceStore: SpiceStore {
1616
try await Task.sleep(for: .seconds(1))
1717
URLCache.shared.removeAllCachedResponses()
1818
}
19-
20-
let featureFlags = FeatureFlagsSpiceStore()
19+
@Spice var featureFlags = FeatureFlagsSpiceStore()
2120

2221
private init() {}
2322
}

‎README.md‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ class AppSpiceStore: SpiceStore {
7676
try await Task.sleep(for: .seconds(1))
7777
URLCache.shared.removeAllCachedResponses()
7878
}
79-
80-
let featureFlags = FeatureFlagsSpiceStore()
79+
@Spice var featureFlags = FeatureFlagsSpiceStore()
8180
}
8281

8382
class FeatureFlagsSpiceStore: SpiceStore {
@@ -294,7 +293,7 @@ Spice stores can be nested to create a hierarchical user interface.
294293

295294
```swift
296295
final class AppSpiceStore: SpiceStore {
297-
let featureFlags = FeatureFlagsSpiceStore()
296+
@Spice var featureFlags = FeatureFlagsSpiceStore()
298297
}
299298

300299
final class FeatureFlagsSpiceStore: SpiceStore {
@@ -303,8 +302,6 @@ final class FeatureFlagsSpiceStore: SpiceStore {
303302
}
304303
```
305304

306-
Note that nested spice stores should not use the `@Spice` property wrapper.
307-
308305
### Require Restart
309306

310307
Setting `requiresRestart` to true will cause the app to be shut down after changing the value. Use this only when necessary, as users do not expect a restart.

‎Sources/Spices/Internal/MenuItems/ChildSpiceStoreMenuItem.swift‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ import Foundation
22

33
struct ChildSpiceStoreMenuItem: MenuItem {
44
let id = UUID().uuidString
5+
let name: Name
56
let spiceStore: any SpiceStore
67
}

‎Sources/Spices/Internal/Storage/ThrowingStorage.swift‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@ final class ThrowingStorage<Value>: Storage {
77
}
88
// swiftlint:disable:next unused_setter_value
99
set {
10-
fatalError("\(type(of: self)) does not support persisting values")
10+
fatalError(setterMessage)
1111
}
1212
}
1313

1414
let publisher: AnyPublisher<Value, Never>
1515

1616
private let initialValue: Value
17+
private let setterMessage: String
1718
private let passthroughSubject = PassthroughSubject<Value, Never>()
1819

19-
init(default initialValue: Value) {
20+
init(default initialValue: Value, setterMessage: String) {
2021
self.initialValue = initialValue
22+
self.setterMessage = setterMessage
2123
self.publisher = passthroughSubject.eraseToAnyPublisher()
2224
}
2325
}

‎Sources/Spices/Internal/Views/ChildSpiceStoreMenuItemView.swift‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ struct ChildSpiceStoreMenuItemView: View {
88
NavigationLink {
99
MenuItemListView(
1010
items: menuItem.spiceStore.menuItems,
11-
title: menuItem.spiceStore.name,
11+
title: menuItem.name.rawValue,
1212
dismiss: dismiss
1313
)
1414
.navigationBarTitleDisplayMode(.inline)
1515
} label: {
16-
Text(menuItem.spiceStore.name)
16+
Text(menuItem.name.rawValue)
1717
}
1818
}
1919
}

‎Sources/Spices/Spice.swift‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,10 @@ import Foundation
143143
) where Value == ButtonHandler {
144144
self.initialValue = wrappedValue
145145
self.name = Name(name)
146-
self.storage = AnyStorage(ThrowingStorage(default: wrappedValue))
146+
self.storage = AnyStorage(ThrowingStorage(
147+
default: wrappedValue,
148+
setterMessage: "Cannot set closure of Spices button."
149+
))
147150
self.userDefaultsStorage = nil
148151
self.menuItem = ButtonMenuItem(
149152
name: self.name,
@@ -152,7 +155,7 @@ import Foundation
152155
)
153156
}
154157

155-
/// Initializes a `Spice` property wrapper for a assynchronous button action.
158+
/// Initializes a `Spice` property wrapper for a asynchronous button action.
156159
/// - Parameters:
157160
/// - wrappedValue: The closure representing the button's action.
158161
/// - name: The display name of the setting. Defaults to a formatted version of the property name.
@@ -164,7 +167,10 @@ import Foundation
164167
) where Value == AsyncButtonHandler {
165168
self.initialValue = wrappedValue
166169
self.name = Name(name)
167-
self.storage = AnyStorage(ThrowingStorage(default: wrappedValue))
170+
self.storage = AnyStorage(ThrowingStorage(
171+
default: wrappedValue,
172+
setterMessage: "Cannot set closure of Spices button."
173+
))
168174
self.userDefaultsStorage = nil
169175
self.menuItem = AsyncButtonMenuItem(
170176
name: self.name,
@@ -173,6 +179,21 @@ import Foundation
173179
)
174180
}
175181

182+
/// Initializes a `Spice` property wrapper for a child spice store.
183+
/// - Parameters:
184+
/// - wrappedValue: The spice store to creaete hierarchial navigation to.
185+
/// - name: The display name of the spice store. Defaults to a formatted version of the property name.
186+
public init(wrappedValue: Value, name: String? = nil) where Value: SpiceStore {
187+
self.initialValue = wrappedValue
188+
self.name = Name(name)
189+
self.storage = AnyStorage(ThrowingStorage(
190+
default: wrappedValue,
191+
setterMessage: "Cannot assign new reference to nested spice store."
192+
))
193+
self.userDefaultsStorage = nil
194+
self.menuItem = ChildSpiceStoreMenuItem(name: self.name, spiceStore: wrappedValue)
195+
}
196+
176197
/// A static subscript that provides access to the `Spice` property wrapper's value within a `SpiceStore`.
177198
///
178199
/// This allows for reading and writing the value of the setting.
@@ -196,6 +217,14 @@ extension Spice: Preparable {
196217
func prepare(propertyName: String, ownedBy spiceStore: some SpiceStore) {
197218
name.rawValue = propertyName.camelCaseToNaturalText()
198219
userDefaultsStorage?.prepare(propertyName: propertyName, ownedBy: spiceStore)
220+
if let childSpiceStore = initialValue as? any SpiceStore {
221+
if childSpiceStore.parent != nil {
222+
fatalError("A child spice store can only be referenced from one parent.")
223+
}
224+
childSpiceStore.parent = spiceStore
225+
childSpiceStore.propertyName = propertyName
226+
childSpiceStore.prepareIfNeeded()
227+
}
199228
}
200229
}
201230

‎Sources/Spices/SpiceStore.swift‎

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,6 @@ extension SpiceStore {
5252
}
5353
}
5454

55-
var name: String {
56-
get {
57-
objc_getAssociatedObject(self, &nameKey) as? String ?? "<name unavailable>"
58-
}
59-
set {
60-
objc_setAssociatedObject(self, &nameKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
61-
}
62-
}
63-
6455
var propertyName: String {
6556
get {
6657
objc_getAssociatedObject(self, &propertyNameKey) as? String ?? "<name unavailable>"
@@ -70,21 +61,21 @@ extension SpiceStore {
7061
}
7162
}
7263

73-
private var isPrepared: Bool {
64+
var parent: (any SpiceStore)? {
7465
get {
75-
(objc_getAssociatedObject(self, &isPreparedKey) as? Bool) ?? false
66+
objc_getAssociatedObject(self, &parentKey) as? any SpiceStore
7667
}
7768
set {
78-
objc_setAssociatedObject(self, &isPreparedKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
69+
objc_setAssociatedObject(self, &parentKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
7970
}
8071
}
8172

82-
private var parent: (any SpiceStore)? {
73+
private var isPrepared: Bool {
8374
get {
84-
objc_getAssociatedObject(self, &parentKey) as? any SpiceStore
75+
(objc_getAssociatedObject(self, &isPreparedKey) as? Bool) ?? false
8576
}
8677
set {
87-
objc_setAssociatedObject(self, &parentKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
78+
objc_setAssociatedObject(self, &isPreparedKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
8879
}
8980
}
9081

@@ -100,13 +91,10 @@ extension SpiceStore {
10091
prepareIfNeeded()
10192
let mirror = Mirror(reflecting: self)
10293
return mirror.children.compactMap { _, value in
103-
if let spice = value as? MenuItemProvider {
104-
return spice.menuItem
105-
} else if let spiceStore = value as? any SpiceStore {
106-
return ChildSpiceStoreMenuItem(spiceStore: spiceStore)
107-
} else {
94+
guard let spice = value as? MenuItemProvider else {
10895
return nil
10996
}
97+
return spice.menuItem
11098
}
11199
}
112100

@@ -131,21 +119,11 @@ extension SpiceStore {
131119
private func prepare() {
132120
let mirror = Mirror(reflecting: self)
133121
for (name, value) in mirror.children {
134-
guard let name else {
122+
guard let name, let spice = value as? Preparable else {
135123
continue
136124
}
137-
if let spice = value as? Preparable {
138-
let propertyName = name.removing(prefix: "_")
139-
spice.prepare(propertyName: propertyName, ownedBy: self)
140-
} else if let spiceStore = value as? any SpiceStore {
141-
if spiceStore.parent != nil {
142-
fatalError("A child spice store can only be referenced from one parent.")
143-
}
144-
spiceStore.parent = self
145-
spiceStore.propertyName = name
146-
spiceStore.name = name.camelCaseToNaturalText()
147-
spiceStore.prepareIfNeeded()
148-
}
125+
let propertyName = name.removing(prefix: "_")
126+
spice.prepare(propertyName: propertyName, ownedBy: self)
149127
}
150128
}
151129
}

0 commit comments

Comments
 (0)