Skip to content

Commit 1f0092a

Browse files
committed
Introduce @State property wrapper based state management (breaking)
Replaces the old protocol requirement based state handling. I've tried my best to make migration as painless as possible with descriptive warnings and fix-its when SwiftCrossUI detects outdated state properties.
1 parent c1b964f commit 1f0092a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+462
-379
lines changed

Examples/Sources/ControlsExample/ControlsApp.swift

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ import SwiftCrossUI
66
#endif
77

88
class ControlsState: Observable {
9-
@Observed var count = 0
10-
@Observed var exampleButtonState = false
11-
@Observed var exampleSwitchState = false
12-
@Observed var sliderValue = 5.0
139
}
1410

1511
@main
1612
@HotReloadable
1713
struct ControlsApp: App {
18-
let state = ControlsState()
14+
@State var count = 0
15+
@State var exampleButtonState = false
16+
@State var exampleSwitchState = false
17+
@State var sliderValue = 5.0
1918

2019
var body: some Scene {
2120
WindowGroup("ControlsApp") {
@@ -24,32 +23,32 @@ struct ControlsApp: App {
2423
VStack {
2524
Text("Button")
2625
Button("Click me!") {
27-
state.count += 1
26+
count += 1
2827
}
29-
Text("Count: \(state.count)")
28+
Text("Count: \(count)")
3029
}
3130
.padding(.bottom, 20)
3231

3332
VStack {
3433
Text("Toggle button")
35-
Toggle("Toggle me!", active: state.$exampleButtonState)
34+
Toggle("Toggle me!", active: $exampleButtonState)
3635
.toggleStyle(.button)
37-
Text("Currently enabled: \(state.exampleButtonState)")
36+
Text("Currently enabled: \(exampleButtonState)")
3837
}
3938
.padding(.bottom, 20)
4039

4140
VStack {
4241
Text("Toggle switch")
43-
Toggle("Toggle me:", active: state.$exampleSwitchState)
42+
Toggle("Toggle me:", active: $exampleSwitchState)
4443
.toggleStyle(.switch)
45-
Text("Currently enabled: \(state.exampleSwitchState)")
44+
Text("Currently enabled: \(exampleSwitchState)")
4645
}
4746

4847
VStack {
4948
Text("Slider")
50-
Slider(state.$sliderValue, minimum: 0, maximum: 10)
49+
Slider($sliderValue, minimum: 0, maximum: 10)
5150
.frame(maxWidth: 200)
52-
Text("Value: \(String(format: "%.02f", state.sliderValue))")
51+
Text("Value: \(String(format: "%.02f", sliderValue))")
5352
}
5453
}
5554
}

Examples/Sources/CounterExample/CounterApp.swift

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,21 @@ import SwiftCrossUI
55
import SwiftBundlerRuntime
66
#endif
77

8-
class CounterState: Observable {
9-
@Observed var count = 0
10-
}
11-
128
@main
139
@HotReloadable
1410
struct CounterApp: App {
15-
let state = CounterState()
11+
@State var count = 0
1612

1713
var body: some Scene {
18-
WindowGroup("CounterExample: \(state.count)") {
14+
WindowGroup("CounterExample: \(count)") {
1915
#hotReloadable {
2016
HStack(spacing: 20) {
2117
Button("-") {
22-
state.count -= 1
18+
count -= 1
2319
}
24-
Text("Count: \(state.count)")
20+
Text("Count: \(count)")
2521
Button("+") {
26-
state.count += 1
22+
count += 1
2723
}
2824
}
2925
.padding()

Examples/Sources/GreetingGeneratorExample/GreetingGeneratorApp.swift

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,37 @@ import SwiftCrossUI
55
import SwiftBundlerRuntime
66
#endif
77

8-
class GreetingGeneratorState: Observable {
9-
@Observed var name = ""
10-
@Observed var greetings: [String] = []
11-
}
12-
138
@main
149
@HotReloadable
1510
struct GreetingGeneratorApp: App {
16-
let state = GreetingGeneratorState()
11+
@State var name = ""
12+
@State var greetings: [String] = []
1713

1814
var body: some Scene {
1915
WindowGroup("Greeting Generator") {
2016
#hotReloadable {
2117
VStack {
22-
TextField("Name", state.$name)
18+
TextField("Name", $name)
2319
HStack {
2420
Button("Generate") {
25-
state.greetings.append("Hello, \(state.name)!")
21+
greetings.append("Hello, \(name)!")
2622
}
2723
Button("Reset") {
28-
state.greetings = []
29-
state.name = ""
24+
greetings = []
25+
name = ""
3026
}
3127
}
3228

33-
if let latest = state.greetings.last {
29+
if let latest = greetings.last {
3430
Text(latest)
3531
.padding(.top, 5)
3632

37-
if state.greetings.count > 1 {
33+
if greetings.count > 1 {
3834
Text("History:")
3935
.padding(.top, 20)
4036

4137
ScrollView {
42-
ForEach(state.greetings.reversed()[1...]) { greeting in
38+
ForEach(greetings.reversed()[1...]) { greeting in
4339
Text(greeting)
4440
}
4541
}

Examples/Sources/NavigationExample/NavigationApp.swift

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,20 @@ enum HumanitiesSubject: Codable {
2121
case history
2222
}
2323

24-
class NavigationAppState: Observable {
25-
@Observed var path = NavigationPath()
26-
}
27-
2824
@main
2925
@HotReloadable
3026
struct NavigationApp: App {
31-
let state = NavigationAppState()
27+
@State var path = NavigationPath()
3228

3329
var body: some Scene {
3430
WindowGroup("Navigation") {
3531
#hotReloadable {
36-
NavigationStack(path: state.$path) {
32+
NavigationStack(path: $path) {
3733
Text("Learn about subject areas")
3834
.padding(.bottom, 10)
3935

40-
NavigationLink("Science", value: SubjectArea.science, path: state.$path)
41-
NavigationLink("Humanities", value: SubjectArea.humanities, path: state.$path)
36+
NavigationLink("Science", value: SubjectArea.science, path: $path)
37+
NavigationLink("Humanities", value: SubjectArea.humanities, path: $path)
4238
}
4339
.navigationDestination(for: SubjectArea.self) { area in
4440
switch area {
@@ -47,17 +43,17 @@ struct NavigationApp: App {
4743
.padding(.bottom, 10)
4844

4945
NavigationLink(
50-
"Physics", value: ScienceSubject.physics, path: state.$path)
46+
"Physics", value: ScienceSubject.physics, path: $path)
5147
NavigationLink(
52-
"Chemistry", value: ScienceSubject.chemistry, path: state.$path)
48+
"Chemistry", value: ScienceSubject.chemistry, path: $path)
5349
case .humanities:
5450
Text("Choose a humanities subject")
5551
.padding(.bottom, 10)
5652

5753
NavigationLink(
58-
"English", value: HumanitiesSubject.english, path: state.$path)
54+
"English", value: HumanitiesSubject.english, path: $path)
5955
NavigationLink(
60-
"History", value: HumanitiesSubject.history, path: state.$path)
56+
"History", value: HumanitiesSubject.history, path: $path)
6157
}
6258

6359
backButton
@@ -91,7 +87,7 @@ struct NavigationApp: App {
9187
@ViewBuilder
9288
var backButton: some View {
9389
Button("Back") {
94-
state.path.removeLast()
90+
path.removeLast()
9591
}
9692
.padding(.top, 10)
9793
}

Examples/Sources/NotesExample/ContentView.swift

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ struct Note: Codable, Equatable {
77
var content: String
88
}
99

10-
class NotesState: Observable, Codable {
11-
@Observed
12-
var notes: [Note] = [
10+
struct ContentView: View {
11+
let notesFile = URL(fileURLWithPath: "notes.json")
12+
13+
@State var notes: [Note] = [
1314
Note(title: "Hello, world!", content: "Welcome SwiftCrossNotes!"),
1415
Note(
1516
title: "Shopping list",
@@ -21,25 +22,17 @@ class NotesState: Observable, Codable {
2122
),
2223
]
2324

24-
@Observed
25-
var selectedNoteId: UUID?
26-
27-
@Observed
28-
var error: String?
29-
}
30-
31-
struct ContentView: View {
32-
let notesFile = URL(fileURLWithPath: "notes.json")
25+
@State var selectedNoteId: UUID?
3326

34-
var state = NotesState()
27+
@State var error: String?
3528

3629
var selectedNote: Binding<Note>? {
37-
guard let id = state.selectedNoteId else {
30+
guard let id = selectedNoteId else {
3831
return nil
3932
}
4033

4134
guard
42-
let index = state.notes.firstIndex(where: { note in
35+
let index = notes.firstIndex(where: { note in
4336
note.id == id
4437
})
4538
else {
@@ -49,40 +42,40 @@ struct ContentView: View {
4942
// TODO: This is unsafe, index could change/not exist anymore
5043
return Binding(
5144
get: {
52-
state.notes[index]
45+
notes[index]
5346
},
5447
set: { newValue in
55-
state.notes[index] = newValue
48+
notes[index] = newValue
5649
}
5750
)
5851
}
5952

6053
var body: some View {
6154
NavigationSplitView {
6255
VStack {
63-
ForEach(state.notes) { note in
56+
ForEach(notes) { note in
6457
Button(note.title) {
65-
state.selectedNoteId = note.id
58+
selectedNoteId = note.id
6659
}
6760
}
6861
Spacer()
69-
if let error = state.error {
62+
if let error = error {
7063
Text(error)
7164
.foregroundColor(.red)
7265
}
7366
Button("New note") {
7467
let note = Note(title: "Untitled", content: "")
75-
state.notes.append(note)
76-
state.selectedNoteId = note.id
68+
notes.append(note)
69+
selectedNoteId = note.id
7770
}
7871
}
79-
.onChange(of: state.notes) {
72+
.onChange(of: notes) {
8073
do {
81-
let data = try JSONEncoder().encode(state.notes)
74+
let data = try JSONEncoder().encode(notes)
8275
try data.write(to: notesFile)
8376
} catch {
8477
print("Error: \(error)")
85-
state.error = "Failed to save notes"
78+
self.error = "Failed to save notes"
8679
}
8780
}
8881
.onAppear {
@@ -92,10 +85,10 @@ struct ContentView: View {
9285

9386
do {
9487
let data = try Data(contentsOf: notesFile)
95-
state.notes = try JSONDecoder().decode([Note].self, from: data)
88+
notes = try JSONDecoder().decode([Note].self, from: data)
9689
} catch {
9790
print("Error: \(error)")
98-
state.error = "Failed to load notes"
91+
self.error = "Failed to load notes"
9992
}
10093
}
10194
.padding(10)

Examples/Sources/RandomNumberGeneratorExample/RandomNumberGeneratorApp.swift

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@ import SwiftCrossUI
55
import SwiftBundlerRuntime
66
#endif
77

8-
class RandomNumberGeneratorState: Observable {
9-
@Observed var minNum = 0
10-
@Observed var maxNum = 100
11-
@Observed var randomNumber = 0
12-
@Observed var colorOption: ColorOption? = ColorOption.red
13-
}
14-
158
enum ColorOption: String, CaseIterable {
169
case red
1710
case green
@@ -32,22 +25,25 @@ enum ColorOption: String, CaseIterable {
3225
@main
3326
@HotReloadable
3427
struct RandomNumberGeneratorApp: App {
35-
let state = RandomNumberGeneratorState()
28+
@State var minNum = 0
29+
@State var maxNum = 100
30+
@State var randomNumber = 0
31+
@State var colorOption: ColorOption? = ColorOption.red
3632

3733
var body: some Scene {
3834
WindowGroup("Random Number Generator") {
3935
#hotReloadable {
4036
VStack {
41-
Text("Random Number: \(state.randomNumber)")
37+
Text("Random Number: \(randomNumber)")
4238
Button("Generate") {
43-
state.randomNumber = Int.random(in: Int(state.minNum)...Int(state.maxNum))
39+
randomNumber = Int.random(in: Int(minNum)...Int(maxNum))
4440
}
4541

4642
Text("Minimum:")
4743
Slider(
48-
state.$minNum.onChange { newValue in
49-
if newValue > state.maxNum {
50-
state.minNum = state.maxNum
44+
$minNum.onChange { newValue in
45+
if newValue > maxNum {
46+
minNum = maxNum
5147
}
5248
},
5349
minimum: 0,
@@ -56,9 +52,9 @@ struct RandomNumberGeneratorApp: App {
5652

5753
Text("Maximum:")
5854
Slider(
59-
state.$maxNum.onChange { newValue in
60-
if newValue < state.minNum {
61-
state.maxNum = state.minNum
55+
$maxNum.onChange { newValue in
56+
if newValue < minNum {
57+
maxNum = minNum
6258
}
6359
},
6460
minimum: 0,
@@ -67,11 +63,11 @@ struct RandomNumberGeneratorApp: App {
6763

6864
HStack {
6965
Text("Choose a color:")
70-
Picker(of: ColorOption.allCases, selection: state.$colorOption)
66+
Picker(of: ColorOption.allCases, selection: $colorOption)
7167
}
7268
}
7369
.padding(10)
74-
.foregroundColor(state.colorOption?.color ?? .red)
70+
.foregroundColor(colorOption?.color ?? .red)
7571
}
7672
}
7773
.defaultSize(width: 500, height: 0)

0 commit comments

Comments
 (0)