Skip to content

Commit 9d5b90b

Browse files
committed
feat: implement iOS selectors
1 parent 79e8101 commit 9d5b90b

5 files changed

Lines changed: 199 additions & 80 deletions

File tree

apps/TesterIntegrated/swift/App.swift

Lines changed: 40 additions & 23 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 = false
15+
let isSideBySideMode = true
1616

1717
@main
1818
struct MyApp: App {
@@ -55,8 +55,6 @@ struct MyApp: App {
5555
}
5656

5757
struct FullScreenView: View {
58-
@UseStore<BrownfieldStore> var store
59-
6058
var body: some View {
6159
NavigationView {
6260
VStack {
@@ -66,19 +64,8 @@ struct MyApp: App {
6664
.padding()
6765
.multilineTextAlignment(.center)
6866

69-
Text("Count: \(Int(store.state.counter))")
70-
71-
TextField("Name", text: Binding(get: { store.state.user.name }, set: { data in
72-
store.set { $0.user.name = data }
73-
}))
74-
.textFieldStyle(.roundedBorder)
75-
.padding(.horizontal)
76-
77-
Button("Increment") {
78-
store.set { $0.counter += 1 }
79-
}
80-
.buttonStyle(.borderedProminent)
81-
.padding(.bottom)
67+
CounterView()
68+
UserView()
8269

8370
NavigationLink("Push React Native Screen") {
8471
ReactNativeView(moduleName: "ReactNative")
@@ -94,26 +81,56 @@ struct MyApp: App {
9481
}
9582
}
9683

84+
struct CounterView: View {
85+
@UseStore(\BrownfieldStore.counter) var counter
86+
87+
var body: some View {
88+
VStack {
89+
Text("Count: \(Int(counter))")
90+
Button("Increment") {
91+
$counter.set { $0 + 1 }
92+
}
93+
.buttonStyle(.borderedProminent)
94+
.padding(.bottom)
95+
}
96+
}
97+
}
98+
99+
struct UserView: View {
100+
@UseStore(\BrownfieldStore.user) var user
101+
102+
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)
109+
}
110+
}
111+
97112
struct NativeView: View {
98-
@UseStore<BrownfieldStore> var store
113+
@UseStore(\BrownfieldStore.counter) var counter
114+
@UseStore(\BrownfieldStore.user) var user
99115

100116
var body: some View {
101117
VStack {
102118
Text("Native Side")
103119
.font(.headline)
104120
.padding(.top)
105121

106-
Text("User: \(store.state.user.name)")
107-
Text("Count: \(Int(store.state.counter))")
122+
Text("User: \(user.name)")
123+
Text("Count: \(Int(counter))")
108124

109-
TextField("Name", text: Binding(get: { store.state.user.name }, set: { data in
110-
store.set { $0.user.name = data }
111-
}))
125+
TextField("Name", text: Binding(
126+
get: { user.name },
127+
set: { $user.set(User(name: $0)) }
128+
))
112129
.textFieldStyle(.roundedBorder)
113130
.padding(.horizontal)
114131

115132
Button("Increment") {
116-
store.set { $0.counter += 1 }
133+
$counter.set { $0 + 1 }
117134
}
118135
.buttonStyle(.borderedProminent)
119136

apps/TesterIntegrated/swift/Generated/BrownfieldStore.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import Brownie
88
import Foundation
99

1010
// MARK: - BrownfieldStore
11-
struct BrownfieldStore: Codable {
11+
struct BrownfieldStore: Codable, Equatable {
1212
var counter: Double
1313
var user: User
1414
}
1515

1616
// MARK: - User
17-
struct User: Codable {
17+
struct User: Codable, Equatable {
1818
var name: String
1919
}
2020

docs/docs/brownie/swift-usage.mdx

Lines changed: 79 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -44,77 +44,100 @@ struct MyApp: App {
4444

4545
### @UseStore Property Wrapper
4646

47-
The `@UseStore` property wrapper provides reactive access to the store:
47+
The `@UseStore` property wrapper provides reactive access to a selected slice of state using KeyPath selectors. This ensures your view only re-renders when the selected value changes.
4848

4949
```swift
5050
import Brownie
5151
import SwiftUI
5252

53-
struct ContentView: View {
54-
@UseStore<BrownfieldStore> var store
53+
struct CounterView: View {
54+
@UseStore(\BrownfieldStore.counter) var counter
5555

5656
var body: some View {
5757
VStack {
58-
Text("Count: \(Int(store.state.counter))")
58+
Text("Count: \(Int(counter))")
5959

6060
Button("Increment") {
61-
store.set { $0.counter += 1 }
61+
$counter.set { $0 + 1 }
6262
}
6363
}
6464
}
6565
}
6666
```
6767

68+
### Selectors
69+
70+
Every `@UseStore` requires a KeyPath selector. This:
71+
72+
- Forces explicit state selection
73+
- Prevents unnecessary re-renders (only updates when selected value changes)
74+
- Provides type-safe access to state
75+
76+
```swift
77+
// Select primitive
78+
@UseStore(\BrownfieldStore.counter) var counter // counter is Double
79+
80+
// Select nested object
81+
@UseStore(\BrownfieldStore.user) var user // user is User
82+
```
83+
84+
:::info Equatable Requirement
85+
Selected values must conform to `Equatable` for change detection. Add `Equatable` to your generated types.
86+
:::
87+
6888
### Updating State
6989

70-
Use the `set` method with a closure to mutate state:
90+
Use the projected value (`$`) to access setter methods:
7191

7292
```swift
73-
// Update single property
74-
store.set { $0.counter += 1 }
93+
// Set value directly
94+
$counter.set(10)
7595

76-
// Update nested property
77-
store.set { $0.user.name = "John" }
96+
// Set with closure (receives current value)
97+
$counter.set { $0 + 1 }
7898

79-
// Update multiple properties
80-
store.set {
81-
$0.counter = 0
82-
$0.user.name = "Reset"
83-
}
99+
// For nested types, replace the whole object
100+
$user.set(User(name: "John"))
84101
```
85102

86-
### TextField Binding
103+
### Multiple Selectors
87104

88-
For two-way binding with TextField, create a `Binding`:
105+
Use multiple `@UseStore` declarations for different state slices. Each only triggers re-renders when its selected value changes:
89106

90107
```swift
91-
struct ContentView: View {
92-
@UseStore<BrownfieldStore> var store
108+
struct MyView: View {
109+
@UseStore(\BrownfieldStore.counter) var counter
110+
@UseStore(\BrownfieldStore.user) var user
93111

94112
var body: some View {
95-
TextField("Name", text: Binding(
96-
get: { store.state.user.name },
97-
set: { store.set { $0.user.name = $0 } }
98-
))
99-
.textFieldStyle(.roundedBorder)
113+
VStack {
114+
Text("Count: \(Int(counter))")
115+
Text("User: \(user.name)")
116+
117+
Button("Increment") {
118+
$counter.set { $0 + 1 }
119+
}
120+
}
100121
}
101122
}
102123
```
103124

104-
### Reading State
125+
### TextField Binding
105126

106-
Access state via the `state` property or keypaths:
127+
For two-way binding with TextField, create a `Binding`:
107128

108129
```swift
109-
// Via state property
110-
let counter = store.state.counter
111-
let name = store.state.user.name
112-
113-
// Via keypath subscript
114-
let counter = store[\.counter]
130+
struct UserView: View {
131+
@UseStore(\BrownfieldStore.user) var user
115132

116-
// Via get method
117-
let counter = store.get(\.counter)
133+
var body: some View {
134+
TextField("Name", text: Binding(
135+
get: { user.name },
136+
set: { $user.set(User(name: $0)) }
137+
))
138+
.textFieldStyle(.roundedBorder)
139+
}
140+
}
118141
```
119142

120143
## UIKit
@@ -214,6 +237,28 @@ cancelSubscription = store.subscribe(\.counter) { [weak self] counter in
214237

215238
## API Reference
216239

240+
### @UseStore
241+
242+
Property wrapper for SwiftUI with required KeyPath selector:
243+
244+
```swift
245+
@UseStore(\BrownfieldStore.counter) var counter
246+
```
247+
248+
| Property | Type | Description |
249+
| ---------------- | -------------- | ---------------------------- |
250+
| `wrappedValue` | `Value` | Selected value (read-only) |
251+
| `projectedValue` | `StoreBinding` | Setter access via `$counter` |
252+
253+
### StoreBinding
254+
255+
Provides setter methods via projected value (`$counter`):
256+
257+
| Method | Description |
258+
| --------- | ---------------------------------------------- |
259+
| `set(_:)` | Set value directly |
260+
| `set(_:)` | Set value with closure receiving current value |
261+
217262
### Store&lt;State&gt;
218263

219264
| Method | Description |
@@ -233,13 +278,3 @@ cancelSubscription = store.subscribe(\.counter) { [weak self] counter in
233278
| `StoreManager.get(key:as:)` | Retrieve typed store by key |
234279
| `shared.snapshot(key:)` | Get raw snapshot dictionary |
235280
| `shared.removeStore(key:)` | Remove and cleanup store |
236-
237-
### @UseStore
238-
239-
Property wrapper for SwiftUI that auto-discovers store by type's `storeName`.
240-
241-
```swift
242-
@UseStore<BrownfieldStore> var store
243-
```
244-
245-
Requires generated type to conform to `BrownieStoreProtocol`.

packages/brownie/ArchitectureOverview.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,25 @@ packages/brownie/
298298
- `store(key:as:)` - Retrieve typed store
299299
- `snapshot(key:)` - Get snapshot via C++ bridge
300300

301-
**@UseStore** - SwiftUI property wrapper:
301+
**@UseStore** - SwiftUI property wrapper with selector support:
302302

303-
- Uses `BrownieStoreProtocol` to automatically derive store key via `storeName`
304-
- `wrappedValue` - Access to full `Store<State>`
303+
```swift
304+
@UseStore(\BrownfieldStore.counter) var counter
305+
// counter -> Double (wrappedValue, read-only)
306+
// $counter.set(5) (projectedValue, direct value)
307+
// $counter.set { $0 + 1 } (projectedValue, closure receives current value)
308+
```
309+
310+
- Requires `WritableKeyPath` selector - forces explicit state selection
311+
- `Value` must conform to `Equatable` for change detection
312+
- Uses `removeDuplicates()` internally - only re-renders when selected value changes
313+
- `wrappedValue` - Selected value (read-only)
314+
- `projectedValue` - `StoreBinding` with `set` methods for updates
315+
316+
**StoreBinding** - Setter wrapper returned via `$projectedValue`:
317+
318+
- `set(_:)` - Set value directly
319+
- `set(_:)` - Set value via closure that receives current value
305320

306321
## JS API
307322

0 commit comments

Comments
 (0)