Skip to content

Commit 3611105

Browse files
committed
fix(codegen): add Equatable protocol to generated Swift types
1 parent 9d5b90b commit 3611105

7 files changed

Lines changed: 73 additions & 65 deletions

File tree

apps/TesterIntegrated/swift/App.swift

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ let initialState = BrownfieldStore(
1212
Toggles testing playground for side by side brownie mode.
1313
Default: false
1414
*/
15-
let isSideBySideMode = true
15+
let isSideBySideMode = false
1616

1717
@main
1818
struct MyApp: App {
@@ -87,25 +87,21 @@ struct MyApp: App {
8787
var body: some View {
8888
VStack {
8989
Text("Count: \(Int(counter))")
90-
Button("Increment") {
91-
$counter.set { $0 + 1 }
92-
}
90+
Stepper(value: $counter, label: { Text("Increment") })
91+
9392
.buttonStyle(.borderedProminent)
9493
.padding(.bottom)
9594
}
9695
}
9796
}
9897

9998
struct UserView: View {
100-
@UseStore(\BrownfieldStore.user) var user
99+
@UseStore(\BrownfieldStore.user.name) var name
101100

102101
var body: some View {
103-
TextField("Name", text: Binding(
104-
get: { user.name },
105-
set: { $user.set(User(name: $0)) }
106-
))
107-
.textFieldStyle(.roundedBorder)
108-
.padding(.horizontal)
102+
TextField("Name", text: $name)
103+
.textFieldStyle(.roundedBorder)
104+
.padding(.horizontal)
109105
}
110106
}
111107

@@ -122,10 +118,7 @@ struct MyApp: App {
122118
Text("User: \(user.name)")
123119
Text("Count: \(Int(counter))")
124120

125-
TextField("Name", text: Binding(
126-
get: { user.name },
127-
set: { $user.set(User(name: $0)) }
128-
))
121+
TextField("Name", text: $user.name)
129122
.textFieldStyle(.roundedBorder)
130123
.padding(.horizontal)
131124

apps/TesterIntegrated/swift/Generated/BrownfieldStore.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import Brownie
55
//
66
// let brownfieldStore = try? JSONDecoder().decode(BrownfieldStore.self, from: jsonData)
77

8+
//
9+
// Hashable or Equatable:
10+
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
11+
// for types that require the use of JSONAny, nor will the implementation of Hashable be
12+
// synthesized for types that have collections (such as arrays or dictionaries).
13+
814
import Foundation
915

1016
// MARK: - BrownfieldStore
@@ -13,6 +19,12 @@ struct BrownfieldStore: Codable, Equatable {
1319
var user: User
1420
}
1521

22+
//
23+
// Hashable or Equatable:
24+
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
25+
// for types that require the use of JSONAny, nor will the implementation of Hashable be
26+
// synthesized for types that have collections (such as arrays or dictionaries).
27+
1628
// MARK: - User
1729
struct User: Codable, Equatable {
1830
var name: String

apps/TesterIntegrated/swift/Generated/SettingsStore.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ import Brownie
55
//
66
// let settingsStore = try? JSONDecoder().decode(SettingsStore.self, from: jsonData)
77

8+
//
9+
// Hashable or Equatable:
10+
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
11+
// for types that require the use of JSONAny, nor will the implementation of Hashable be
12+
// synthesized for types that have collections (such as arrays or dictionaries).
13+
814
import Foundation
915

1016
// MARK: - SettingsStore
11-
struct SettingsStore: Codable {
17+
struct SettingsStore: Codable, Equatable {
1218
var notificationsEnabled, privacyMode: Bool
1319
var theme: Theme
1420
}
1521

16-
enum Theme: String, Codable {
22+
enum Theme: String, Codable, Equatable {
1723
case dark = "dark"
1824
case light = "light"
1925
}

docs/docs/brownie/swift-usage.mdx

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,19 @@ Selected values must conform to `Equatable` for change detection. Add `Equatable
8787

8888
### Updating State
8989

90-
Use the projected value (`$`) to access setter methods:
90+
The projected value (`$`) returns a standard SwiftUI `Binding<Value>`:
9191

9292
```swift
93-
// Set value directly
94-
$counter.set(10)
93+
// Use with any SwiftUI control that accepts Binding
94+
Stepper(value: $counter) { Text("Count: \(Int(counter))") }
95+
Slider(value: $counter, in: 0...100)
96+
Toggle("Enabled", isOn: $isEnabled)
9597

96-
// Set with closure (receives current value)
98+
// Set with closure (Brownie extension on Binding)
9799
$counter.set { $0 + 1 }
98100

99-
// For nested types, replace the whole object
100-
$user.set(User(name: "John"))
101+
// Access nested properties via Binding subscript
102+
TextField("Name", text: $user.name)
101103
```
102104

103105
### Multiple Selectors
@@ -124,18 +126,26 @@ struct MyView: View {
124126

125127
### TextField Binding
126128

127-
For two-way binding with TextField, create a `Binding`:
129+
Use the binding directly or select a nested property:
128130

129131
```swift
132+
// Option 1: Select the nested property directly
133+
struct UserView: View {
134+
@UseStore(\BrownfieldStore.user.name) var name
135+
136+
var body: some View {
137+
TextField("Name", text: $name)
138+
.textFieldStyle(.roundedBorder)
139+
}
140+
}
141+
142+
// Option 2: Select parent and access nested binding
130143
struct UserView: View {
131144
@UseStore(\BrownfieldStore.user) var user
132145

133146
var body: some View {
134-
TextField("Name", text: Binding(
135-
get: { user.name },
136-
set: { $user.set(User(name: $0)) }
137-
))
138-
.textFieldStyle(.roundedBorder)
147+
TextField("Name", text: $user.name)
148+
.textFieldStyle(.roundedBorder)
139149
}
140150
}
141151
```
@@ -245,19 +255,18 @@ Property wrapper for SwiftUI with required KeyPath selector:
245255
@UseStore(\BrownfieldStore.counter) var counter
246256
```
247257

248-
| Property | Type | Description |
249-
| ---------------- | -------------- | ---------------------------- |
250-
| `wrappedValue` | `Value` | Selected value (read-only) |
251-
| `projectedValue` | `StoreBinding` | Setter access via `$counter` |
258+
| Property | Type | Description |
259+
| ---------------- | ---------------- | ----------------------------------- |
260+
| `wrappedValue` | `Value` | Selected value (read-only) |
261+
| `projectedValue` | `Binding<Value>` | Standard SwiftUI binding via `$var` |
252262

253-
### StoreBinding
263+
### Binding Extension
254264

255-
Provides setter methods via projected value (`$counter`):
265+
Brownie adds a `set` method to `Binding` for closure-based updates:
256266

257-
| Method | Description |
258-
| --------- | ---------------------------------------------- |
259-
| `set(_:)` | Set value directly |
260-
| `set(_:)` | Set value with closure receiving current value |
267+
```swift
268+
$counter.set { $0 + 1 } // increment using current value
269+
```
261270

262271
### Store&lt;State&gt;
263272

packages/brownie/ArchitectureOverview.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -303,19 +303,18 @@ packages/brownie/
303303
```swift
304304
@UseStore(\BrownfieldStore.counter) var counter
305305
// counter -> Double (wrappedValue, read-only)
306-
// $counter.set(5) (projectedValue, direct value)
307-
// $counter.set { $0 + 1 } (projectedValue, closure receives current value)
306+
// $counter -> Binding<Double> (projectedValue, standard SwiftUI binding)
307+
// $counter.set { $0 + 1 } (Binding extension for closure updates)
308308
```
309309

310310
- Requires `WritableKeyPath` selector - forces explicit state selection
311311
- `Value` must conform to `Equatable` for change detection
312312
- Uses `removeDuplicates()` internally - only re-renders when selected value changes
313313
- `wrappedValue` - Selected value (read-only)
314-
- `projectedValue` - `StoreBinding` with `set` methods for updates
314+
- `projectedValue` - Standard `Binding<Value>` for SwiftUI controls
315315

316-
**StoreBinding** - Setter wrapper returned via `$projectedValue`:
316+
**Binding Extension** - Adds closure-based setter:
317317

318-
- `set(_:)` - Set value directly
319318
- `set(_:)` - Set value via closure that receives current value
320319

321320
## JS API

packages/brownie/ios/BrownieStore.swift

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,10 @@ public extension EnvironmentValues {
2020
}
2121
}
2222

23-
/// Provides setter methods for the selected store value via projectedValue ($counter).
24-
public struct StoreBinding<State: BrownieStoreProtocol, Value> {
25-
private let store: Store<State>
26-
private let keyPath: WritableKeyPath<State, Value>
27-
28-
init(store: Store<State>, keyPath: WritableKeyPath<State, Value>) {
29-
self.store = store
30-
self.keyPath = keyPath
31-
}
32-
33-
/// Set value directly
34-
public func set(_ value: Value) {
35-
store.set(keyPath, to: value)
36-
}
37-
23+
public extension Binding {
3824
/// Set value using closure that receives current value
39-
public func set(_ updater: (Value) -> Value) {
40-
let currentValue = store.get(keyPath)
41-
let newValue = updater(currentValue)
42-
store.set(keyPath, to: newValue)
25+
func set(_ updater: (Value) -> Value) {
26+
wrappedValue = updater(wrappedValue)
4327
}
4428
}
4529

@@ -62,8 +46,11 @@ public struct UseStore<State: BrownieStoreProtocol, Value: Equatable>: DynamicPr
6246
observer.value
6347
}
6448

65-
public var projectedValue: StoreBinding<State, Value> {
66-
StoreBinding(store: observer.store, keyPath: keyPath)
49+
public var projectedValue: Binding<Value> {
50+
Binding(
51+
get: { observer.store.get(keyPath) },
52+
set: { observer.store.set(keyPath, to: $0) }
53+
)
6754
}
6855
}
6956

packages/brownie/scripts/generators/swift.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ export async function generateSwift(
6363
rendererOptions: {
6464
'mutable-properties': 'true',
6565
initializers: 'false',
66+
'swift-5-support': 'true',
67+
protocol: 'equatable',
6668
},
6769
});
6870

0 commit comments

Comments
 (0)