Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 32 additions & 22 deletions apps/TesterIntegrated/swift/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ struct MyApp: App {
}

struct FullScreenView: View {
@UseStore<BrownfieldStore> var store

var body: some View {
NavigationView {
VStack {
Expand All @@ -66,19 +64,8 @@ struct MyApp: App {
.padding()
.multilineTextAlignment(.center)

Text("Count: \(Int(store.state.counter))")

TextField("Name", text: Binding(get: { store.state.user.name }, set: { data in
store.set { $0.user.name = data }
}))
.textFieldStyle(.roundedBorder)
.padding(.horizontal)

Button("Increment") {
store.set { $0.counter += 1 }
}
.buttonStyle(.borderedProminent)
.padding(.bottom)
CounterView()
UserView()

NavigationLink("Push React Native Screen") {
ReactNativeView(moduleName: "ReactNative")
Expand All @@ -94,26 +81,49 @@ struct MyApp: App {
}
}

struct CounterView: View {
@UseStore(\BrownfieldStore.counter) var counter

var body: some View {
VStack {
Text("Count: \(Int(counter))")
Stepper(value: $counter, label: { Text("Increment") })

.buttonStyle(.borderedProminent)
.padding(.bottom)
}
}
}

struct UserView: View {
@UseStore(\BrownfieldStore.user.name) var name

var body: some View {
TextField("Name", text: $name)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
}
}

struct NativeView: View {
@UseStore<BrownfieldStore> var store
@UseStore(\BrownfieldStore.counter) var counter
@UseStore(\BrownfieldStore.user) var user

var body: some View {
VStack {
Text("Native Side")
.font(.headline)
.padding(.top)

Text("User: \(store.state.user.name)")
Text("Count: \(Int(store.state.counter))")
Text("User: \(user.name)")
Text("Count: \(Int(counter))")

TextField("Name", text: Binding(get: { store.state.user.name }, set: { data in
store.set { $0.user.name = data }
}))
TextField("Name", text: $user.name)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)

Button("Increment") {
store.set { $0.counter += 1 }
$counter.set { $0 + 1 }
}
.buttonStyle(.borderedProminent)

Expand Down
16 changes: 14 additions & 2 deletions apps/TesterIntegrated/swift/Generated/BrownfieldStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,28 @@ import Brownie
//
// let brownfieldStore = try? JSONDecoder().decode(BrownfieldStore.self, from: jsonData)

//
// Hashable or Equatable:
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
// for types that require the use of JSONAny, nor will the implementation of Hashable be
// synthesized for types that have collections (such as arrays or dictionaries).

import Foundation

// MARK: - BrownfieldStore
struct BrownfieldStore: Codable {
struct BrownfieldStore: Codable, Equatable {
var counter: Double
var user: User
}

//
// Hashable or Equatable:
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
// for types that require the use of JSONAny, nor will the implementation of Hashable be
// synthesized for types that have collections (such as arrays or dictionaries).

// MARK: - User
struct User: Codable {
struct User: Codable, Equatable {
var name: String
}

Expand Down
10 changes: 8 additions & 2 deletions apps/TesterIntegrated/swift/Generated/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ import Brownie
//
// let settingsStore = try? JSONDecoder().decode(SettingsStore.self, from: jsonData)

//
// Hashable or Equatable:
// The compiler will not be able to synthesize the implementation of Hashable or Equatable
// for types that require the use of JSONAny, nor will the implementation of Hashable be
// synthesized for types that have collections (such as arrays or dictionaries).

import Foundation

// MARK: - SettingsStore
struct SettingsStore: Codable {
struct SettingsStore: Codable, Equatable {
var notificationsEnabled, privacyMode: Bool
var theme: Theme
}

enum Theme: String, Codable {
enum Theme: String, Codable, Equatable {
case dark = "dark"
case light = "light"
}
Expand Down
130 changes: 87 additions & 43 deletions docs/docs/brownie/swift-usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,77 +44,110 @@ struct MyApp: App {

### @UseStore Property Wrapper

The `@UseStore` property wrapper provides reactive access to the store:
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.

```swift
import Brownie
import SwiftUI

struct ContentView: View {
@UseStore<BrownfieldStore> var store
struct CounterView: View {
@UseStore(\BrownfieldStore.counter) var counter

var body: some View {
VStack {
Text("Count: \(Int(store.state.counter))")
Text("Count: \(Int(counter))")

Button("Increment") {
store.set { $0.counter += 1 }
$counter.set { $0 + 1 }
}
}
}
}
```

### Selectors

Every `@UseStore` requires a KeyPath selector. This:

- Forces explicit state selection
- Prevents unnecessary re-renders (only updates when selected value changes)
- Provides type-safe access to state

```swift
// Select primitive
@UseStore(\BrownfieldStore.counter) var counter // counter is Double

// Select nested object
@UseStore(\BrownfieldStore.user) var user // user is User
```

:::info Equatable Requirement
Selected values must conform to `Equatable` for change detection.
:::

### Updating State

Use the `set` method with a closure to mutate state:
The projected value (`$`) returns a standard SwiftUI `Binding<Value>`:

```swift
// Update single property
store.set { $0.counter += 1 }
// Use with any SwiftUI control that accepts Binding
Stepper(value: $counter) { Text("Count: \(Int(counter))") }
Slider(value: $counter, in: 0...100)
Toggle("Enabled", isOn: $isEnabled)

// Update nested property
store.set { $0.user.name = "John" }
// Set with closure (Brownie extension on Binding)
$counter.set { $0 + 1 }

// Update multiple properties
store.set {
$0.counter = 0
$0.user.name = "Reset"
}
// Access nested properties via Binding subscript
TextField("Name", text: $user.name)
```

### TextField Binding
### Multiple Selectors

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

```swift
struct ContentView: View {
@UseStore<BrownfieldStore> var store
struct MyView: View {
@UseStore(\BrownfieldStore.counter) var counter
@UseStore(\BrownfieldStore.user) var user

var body: some View {
TextField("Name", text: Binding(
get: { store.state.user.name },
set: { store.set { $0.user.name = $0 } }
))
.textFieldStyle(.roundedBorder)
VStack {
Text("Count: \(Int(counter))")
Text("User: \(user.name)")

Button("Increment") {
$counter.set { $0 + 1 }
}
}
}
}
```

### Reading State
### TextField Binding

Access state via the `state` property or keypaths:
Use the binding directly or select a nested property:

```swift
// Via state property
let counter = store.state.counter
let name = store.state.user.name
// Option 1: Select the nested property directly
struct UserView: View {
@UseStore(\BrownfieldStore.user.name) var name

// Via keypath subscript
let counter = store[\.counter]
var body: some View {
TextField("Name", text: $name)
.textFieldStyle(.roundedBorder)
}
}

// Via get method
let counter = store.get(\.counter)
// Option 2: Select parent and access nested binding
struct UserView: View {
@UseStore(\BrownfieldStore.user) var user

var body: some View {
TextField("Name", text: $user.name)
.textFieldStyle(.roundedBorder)
}
}
```

## UIKit
Expand Down Expand Up @@ -214,6 +247,27 @@ cancelSubscription = store.subscribe(\.counter) { [weak self] counter in

## API Reference

### @UseStore

Property wrapper for SwiftUI with required KeyPath selector:

```swift
@UseStore(\BrownfieldStore.counter) var counter
```

| Property | Type | Description |
| ---------------- | ---------------- | ----------------------------------- |
| `wrappedValue` | `Value` | Selected value (read-only) |
| `projectedValue` | `Binding<Value>` | Standard SwiftUI binding via `$var` |

### Binding Extension

Brownie adds a `set` method to `Binding` for closure-based updates:

```swift
$counter.set { $0 + 1 } // increment using current value
```

### Store&lt;State&gt;

| Method | Description |
Expand All @@ -233,13 +287,3 @@ cancelSubscription = store.subscribe(\.counter) { [weak self] counter in
| `StoreManager.get(key:as:)` | Retrieve typed store by key |
| `shared.snapshot(key:)` | Get raw snapshot dictionary |
| `shared.removeStore(key:)` | Remove and cleanup store |

### @UseStore

Property wrapper for SwiftUI that auto-discovers store by type's `storeName`.

```swift
@UseStore<BrownfieldStore> var store
```

Requires generated type to conform to `BrownieStoreProtocol`.
20 changes: 17 additions & 3 deletions packages/brownie/ArchitectureOverview.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,24 @@ packages/brownie/
- `store(key:as:)` - Retrieve typed store
- `snapshot(key:)` - Get snapshot via C++ bridge

**@UseStore** - SwiftUI property wrapper:
**@UseStore** - SwiftUI property wrapper with selector support:

- Uses `BrownieStoreProtocol` to automatically derive store key via `storeName`
- `wrappedValue` - Access to full `Store<State>`
```swift
@UseStore(\BrownfieldStore.counter) var counter
// counter -> Double (wrappedValue, read-only)
// $counter -> Binding<Double> (projectedValue, standard SwiftUI binding)
// $counter.set { $0 + 1 } (Binding extension for closure updates)
```

- Requires `WritableKeyPath` selector - forces explicit state selection
- `Value` must conform to `Equatable` for change detection
- Uses `removeDuplicates()` internally - only re-renders when selected value changes
- `wrappedValue` - Selected value (read-only)
- `projectedValue` - Standard `Binding<Value>` for SwiftUI controls

**Binding Extension** - Adds closure-based setter:

- `set(_:)` - Set value via closure that receives current value

## JS API

Expand Down
Loading