Skip to content

Instantly share code, notes, and snippets.

@gohanlon
Last active April 4, 2022 20:16
Show Gist options
  • Save gohanlon/8a01a736101481abe4c4d140fa7c503b to your computer and use it in GitHub Desktop.
Save gohanlon/8a01a736101481abe4c4d140fa7c503b to your computer and use it in GitHub Desktop.
TCA and new SwiftUI focus APIs from WWDC21
import Combine
import ComposableArchitecture
import SwiftUI
struct LoginFormState: Equatable {
var username: String = ""
var password: String = ""
var focusedField: Field?
enum Field: Hashable {
case username
case password
}
}
enum LoginFormAction: Equatable {
case usernameChanged(String)
case passwordChanged(String)
case focusedFieldChanged(LoginFormState.Field?)
case signInTapped
}
let reducer = Reducer<LoginFormState, LoginFormAction, Void> { state, action, _ in
switch action {
case .usernameChanged(let username):
state.username = username
return .none
case .passwordChanged(let password):
state.password = password
return .none
case .focusedFieldChanged(let focusedField):
state.focusedField = focusedField
return .none
case .signInTapped:
if state.username.isEmpty {
state.focusedField = .username
} else if state.password.isEmpty {
state.focusedField = .password
} else {
// handleLogin(username, password)
}
return .none
}
}
.debug()
struct FocusTestView: View {
let store: Store<LoginFormState, LoginFormAction>
@FocusState private var focusedField: LoginFormState.Field?
var body: some View {
// iOS 15 Beta 1 bug: Placing FocusState-focusable views within a Form view, as in Apple's doc example,
// completely breaks FocusState. Assigning to the @FocusState property has no effect--it will always
// be nil. Wrapping in a VStack and/or TCA's WithViewStore works, tho.
WithViewStore(self.store) { viewStore in
VStack {
TextField(
"Username",
text: viewStore.binding(get: { $0.username }, send: LoginFormAction.usernameChanged)
)
.focused(self.$focusedField, equals: .username)
SecureField(
"Password",
text: viewStore.binding(get: { $0.password }, send: LoginFormAction.passwordChanged)
)
.focused(self.$focusedField, equals: .password)
Button("Sign In") {
viewStore.send(.signInTapped)
}
}
.onChange(of: self.focusedField) { viewStore.send(.focusedFieldChanged($0)) }
.onChange(of: viewStore.focusedField) { self.focusedField = $0 }
}
}
}
@main
struct TestFocusApp: App {
var body: some Scene {
WindowGroup {
FocusTestView(
store: .init(
initialState: LoginFormState(),
reducer: reducer,
environment: ()
)
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment