iOS15で追加されたFocusStateは、TextFieldなどのフォーカス制御が簡単になる便利なプロパティラッパーです。
今回はFocusStateをTCAで正しく扱う方法について解説していきたいと思います。
実装
Viewに以下のようなextensionを追加します。
public extension View {
func synchronize<Value: Equatable>(
_ first: Binding<Value>,
_ second: FocusState<Value>.Binding
) -> some View {
onChange(of: first.wrappedValue, perform: { second.wrappedValue = $0 })
.onChange(of: second.wrappedValue, perform: { first.wrappedValue = $0 })
}
}
onChangeを使ってfirst
のwrappedValueが変化したらsecond
のwrappedValueにfirstの値を入れ、
同じようにsecond
が変化したらfirst
に代入しています。このようにして、BindingとFocusStateをリンクすることができます。
使用方法
今回はemailとpasswordの入力を例にします。EmailのTextFieldでエンターを押すとPasswordのTextFieldにfocusされる処理を書いていきます。
HogeReducer.swift
struct HogeReducer: ReducerProtocol {
struct State: Equatable {
@BindingState var focused: Field?
@BindingState var email = ""
@BindingState var password = ""
}
enum Action: Equatable, BindableAction {
case binding(BindingAction<State>)
}
var body: some ReducerProtocolOf<Self> {
BindingReducer()
}
}
extension HogeReducer {
enum Field {
case email, password
}
}
HogeView.swift
struct HogeView: View {
@FocusState private var focused: HogeReducer.Field?
private let store: StoreOf<HogeReducer>
init(store: StoreOf<HogeReducer>) {
self.store = store
}
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
HStack {
TextField(
"email",
text: viewStore.binding(\.$email),
onCommit: {
// Enterを押したらPasswordのFieldがFocusされるように
viewStore.send(.set(\.$focused, .password))
}
)
.focused($focused, equals: .email)
SecureField("password", text: viewStore.binding(\.$password))
.focused($focused, equals: .password)
}
// HogeReducer.StateのfocusedとHogeViewのfocusedをシンクロさせる。
.synchronize(viewStore.binding(\.$focused), $focused)
}
}
}
TCAは単方向のデータフローなのでViewで状態を保ち、Viewで状態を変更するのは好ましくないです。そのため、HogeReducer
のほうでfocusedの状態を保持、変更されるようにしています。
まとめ
TCAでFocusStateを正しく利用することができました。
参考文献