Skip to content

Commit fdc8773

Browse files
mbrandonwmluisbrown
authored andcommitted
@focusstate case study. (#690)
* @focusstate case study. * compiler version
1 parent c5ba0e3 commit fdc8773

File tree

4 files changed

+134
-0
lines changed

4 files changed

+134
-0
lines changed

Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
CA25E5D224463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */; };
1414
CA27C0B7245780CE00CB1E59 /* 03-Effects-SystemEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA27C0B6245780CE00CB1E59 /* 03-Effects-SystemEnvironment.swift */; };
1515
CA34170824A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */; };
16+
CA3E421F26B8337500581ABC /* 01-GettingStarted-FocusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA3E421E26B8337500581ABC /* 01-GettingStarted-FocusState.swift */; };
1617
CA3E4C5B24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA3E4C5A24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift */; };
1718
CA410EE0247A15FE00E41798 /* 02-Effects-WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */; };
1819
CA410EE2247C73B400E41798 /* 02-Effects-WebSocketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */; };
@@ -156,6 +157,7 @@
156157
CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-Bindings-Basics.swift"; sourceTree = "<group>"; };
157158
CA27C0B6245780CE00CB1E59 /* 03-Effects-SystemEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Effects-SystemEnvironment.swift"; sourceTree = "<group>"; };
158159
CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AnimationsTests.swift"; sourceTree = "<group>"; };
160+
CA3E421E26B8337500581ABC /* 01-GettingStarted-FocusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-FocusState.swift"; sourceTree = "<group>"; };
159161
CA3E4C5A24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-Lifecycle.swift"; sourceTree = "<group>"; };
160162
CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocket.swift"; sourceTree = "<group>"; };
161163
CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocketTests.swift"; sourceTree = "<group>"; };
@@ -399,6 +401,7 @@
399401
DC5B505025C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift */,
400402
DCC68EE02447C4630037F998 /* 01-GettingStarted-Composition-TwoCounters.swift */,
401403
DC89C4432446111B006900B9 /* 01-GettingStarted-Counter.swift */,
404+
CA3E421E26B8337500581ABC /* 01-GettingStarted-FocusState.swift */,
402405
DCC68EDC2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift */,
403406
CA7BC8ED245CCFE4001FB69F /* 01-GettingStarted-SharedState.swift */,
404407
CAA9ADC12446587C0003A984 /* 02-Effects-Basics.swift */,
@@ -744,6 +747,7 @@
744747
DC89C45324465452006900B9 /* 03-Navigation-Lists-NavigateAndLoad.swift in Sources */,
745748
CAF069D024ACC5AF00A1AAEF /* 00-Core.swift in Sources */,
746749
DCC68EE32447C8540037F998 /* 04-HigherOrderReducers-ReusableFavoriting.swift in Sources */,
750+
CA3E421F26B8337500581ABC /* 01-GettingStarted-FocusState.swift in Sources */,
747751
DCC68EDF2447BC810037F998 /* TemplateText.swift in Sources */,
748752
DCAC2A4F2452352E0094DEF5 /* 04-HigherOrderReducers-ElmLikeSubscriptions.swift in Sources */,
749753
CA6AC2672451135C00C71CB3 /* DownloadClient.swift in Sources */,

Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct RootState {
1515
var effectsCancellation = EffectsCancellationState()
1616
var effectsTimers = TimersState()
1717
var episodes = EpisodesState(episodes: .mocks)
18+
var focusDemo = FocusDemoState()
1819
var lifecycle = LifecycleDemoState()
1920
var loadThenNavigate = LoadThenNavigateState()
2021
var loadThenNavigateList = LoadThenNavigateListState()
@@ -45,6 +46,7 @@ enum RootAction {
4546
case effectsBasics(EffectsBasicsAction)
4647
case effectsCancellation(EffectsCancellationAction)
4748
case episodes(EpisodesAction)
49+
case focusDemo(FocusDemoAction)
4850
case lifecycle(LifecycleDemoAction)
4951
case loadThenNavigate(LoadThenNavigateAction)
5052
case loadThenNavigateList(LoadThenNavigateListAction)
@@ -160,6 +162,12 @@ let rootReducer = Reducer<RootState, RootAction, RootEnvironment>.combine(
160162
action: /RootAction.episodes,
161163
environment: { .init(favorite: $0.favorite, mainQueue: $0.mainQueue) }
162164
),
165+
focusDemoReducer
166+
.pullback(
167+
state: \.focusDemo,
168+
action: /RootAction.focusDemo,
169+
environment: { _ in .init() }
170+
),
163171
lifecycleDemoReducer
164172
.pullback(
165173
state: \.lifecycle,

Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ struct RootView: View {
8181
)
8282
)
8383

84+
#if compiler(>=5.5)
85+
NavigationLink(
86+
"Focus State",
87+
destination: FocusDemoView(
88+
store: self.store.scope(
89+
state: \.focusDemo,
90+
action: RootAction.focusDemo
91+
)
92+
)
93+
)
94+
#endif
95+
8496
NavigationLink(
8597
"Animations",
8698
destination: AnimationsView(
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
private let readMe = """
5+
This demonstrates how to make use of SwiftUI's `@FocusState` in the Composable Architecture. \
6+
If you tap the "Sign in" button while a field is empty, the focus will be changed to that field.
7+
"""
8+
9+
struct FocusDemoState: Equatable {
10+
var focusedField: Field? = nil
11+
var password: String = ""
12+
var username: String = ""
13+
14+
enum Field: String, Hashable {
15+
case username, password
16+
}
17+
}
18+
19+
enum FocusDemoAction: Equatable {
20+
case binding(BindingAction<FocusDemoState>)
21+
case signInButtonTapped
22+
}
23+
24+
struct FocusDemoEnvironment {}
25+
26+
let focusDemoReducer = Reducer<
27+
FocusDemoState,
28+
FocusDemoAction,
29+
FocusDemoEnvironment
30+
> { state, action, _ in
31+
switch action {
32+
case .binding:
33+
return .none
34+
35+
case .signInButtonTapped:
36+
if state.username.isEmpty {
37+
state.focusedField = .username
38+
} else if state.password.isEmpty {
39+
state.focusedField = .password
40+
}
41+
return .none
42+
}
43+
}
44+
.binding(action: /FocusDemoAction.binding)
45+
46+
#if compiler(>=5.5)
47+
struct FocusDemoView: View {
48+
let store: Store<FocusDemoState, FocusDemoAction>
49+
@FocusState var focusedField: FocusDemoState.Field?
50+
51+
var body: some View {
52+
WithViewStore(self.store) { viewStore in
53+
VStack(alignment: .leading, spacing: 32) {
54+
Text(template: readMe, .caption)
55+
56+
VStack {
57+
TextField(
58+
"Username",
59+
text: viewStore.binding(keyPath: \.username, send: FocusDemoAction.binding)
60+
)
61+
.focused($focusedField, equals: .username)
62+
63+
SecureField(
64+
"Password",
65+
text: viewStore.binding(keyPath: \.password, send: FocusDemoAction.binding)
66+
)
67+
.focused($focusedField, equals: .password)
68+
69+
Button("Sign In") {
70+
viewStore.send(.signInButtonTapped)
71+
}
72+
}
73+
74+
Spacer()
75+
}
76+
.padding()
77+
.synchronize(
78+
viewStore.binding(keyPath: \.focusedField, send: FocusDemoAction.binding),
79+
self.$focusedField
80+
)
81+
}
82+
.navigationBarTitle("Focus demo")
83+
}
84+
}
85+
86+
extension View {
87+
func synchronize<Value: Equatable>(
88+
_ first: Binding<Value>,
89+
_ second: FocusState<Value>.Binding
90+
) -> some View {
91+
self
92+
.onChange(of: first.wrappedValue) { second.wrappedValue = $0 }
93+
.onChange(of: second.wrappedValue) { first.wrappedValue = $0 }
94+
}
95+
}
96+
97+
struct FocusDemo_Previews: PreviewProvider {
98+
static var previews: some View {
99+
NavigationView {
100+
FocusDemoView(
101+
store: Store(
102+
initialState: .init(),
103+
reducer: focusDemoReducer,
104+
environment: .init()
105+
)
106+
)
107+
}
108+
}
109+
}
110+
#endif

0 commit comments

Comments
 (0)