Open5
1つの画面内で複数のTextFieldがあり、TextFieldごとにカスタムバーに表示させるボタンを分岐させたいスレ
詳細
実現したいこと
- パターン1の3つのいずれかのTextFieldにカーソルを当てている場合、キーボードのカスタムバーにはTextField間を移動できるボタンとキーボードを閉じるボタンを追加したい。(ただしパターン2のTextFieldには移動できないようにしたい)
- パターン2のキーボードのカスタムバーには移動させるボタンは表示させず、閉じるボタンのみを表示させたい。
現状
- パターン1の3つのいずれかのTextFieldにカーソルを当てている場合でもパターン2のTextField2用の閉じるボタンが表示されてしまう
- 同じくパターン2のTextFieldにカーソルを当てている場合でもパターン1のTextField用のボタンが表示されてしまう
対象画面
パターン1にフォーカス | パターン2にフォーカス |
---|---|
ソースコード:
HogeScreen.swift
import SwiftUI
struct HogeScreen: View {
@State private var viewModel: HogeViewModel = .init()
@FocusState private var focusedField: Field?
@FocusState private var isPattern2TextFieldFocused: Bool
private enum Field {
case field1, field2, field3
}
var body: some View {
VStack(spacing: 32) {
VStack(alignment: .leading, spacing: 4) {
Text("パターン1")
.fontWeight(.black)
textFields
}
VStack(alignment: .leading, spacing: 4) {
Text("パターン2")
.fontWeight(.black)
TextField("", text: $viewModel.value1)
.textFieldStyle(.roundedBorder)
.focused($isPattern2TextFieldFocused)
.toolbar {
pattern2TextFieldKeyboardCustomBar
}
}
Spacer()
}
.padding(16)
}
var textFields: some View {
HStack(spacing: 4) {
TextField("値1", text: $viewModel.value1)
.focused($focusedField, equals: .field1)
TextField("値2", text: $viewModel.value2)
.focused($focusedField, equals: .field2)
TextField("値3", text: $viewModel.value3)
.focused($focusedField, equals: .field3)
}
.textFieldStyle(.roundedBorder)
.toolbar {
pattern1TextFieldsKeyboardCustomBar
}
}
var pattern1TextFieldsKeyboardCustomBar: some ToolbarContent {
ToolbarItemGroup(placement: .keyboard) {
HStack(spacing: 4) {
Button(action: {
moveToPreviousField()
}) {
Image(systemName: "chevron.left")
}
Button(action: {
moveToNextField()
}) {
Image(systemName: "chevron.right")
}
Spacer()
Button(action: {
focusedField = nil // キーボードを閉じる
}) {
Text("閉じる")
}
}
}
}
var pattern2TextFieldKeyboardCustomBar: some ToolbarContent {
ToolbarItemGroup(placement: .keyboard) {
Button(action: {
isPattern2TextFieldFocused = false
}) {
Text("閉じる")
}
}
}
}
private extension HogeScreen {
func moveToPreviousField() {
switch focusedField {
case .field1:
focusedField = .field3
case .field2:
focusedField = .field1
case .field3:
focusedField = .field2
default:
focusedField = nil
}
}
func moveToNextField() {
switch focusedField {
case .field1:
focusedField = .field2
case .field2:
focusedField = .field3
case .field3:
focusedField = .field1
default:
focusedField = nil
}
}
}
#Preview {
HogeScreen()
}
HogeViewModel.swift
@Observable
final class HogeViewModel {
var value1: String = ""
var value2: String = ""
var value3: String = ""
}
否めないク⭕️コード感...パターン1のTextFieldをenumで管理してるのもこれでいいのかと思うし、 @FocusStateが2つあるのも...🤔
そしてprivate extension HogeScreenはViewModelに移行させたすぎる
改善の余地はありそうだが、一旦望んでいる挙動は実現できた。
挙動:
パターン1にフォーカス | パターン2にフォーカス |
---|---|
ソースコード:
HogeScreen.swift
import SwiftUI
struct HogeScreen: View {
@State private var viewModel: HogeViewModel = .init()
@FocusState private var focusedField: Field?
private enum Field {
case field1, field2, field3, memoField
}
var body: some View {
VStack(spacing: 32) {
pattern1View
pattern2View
Spacer()
}
.padding(16)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
if focusedField == .memoField {
pattern2TextFieldKeyboardCustomBar
} else {
pattern1TextFieldsKeyboardCustomBar
}
}
}
}
var pattern1View: some View {
VStack(alignment: .leading, spacing: 4) {
Text("パターン1")
.fontWeight(.black)
textFields
}
}
var textFields: some View {
HStack(spacing: 4) {
TextField("値1", text: $viewModel.value1)
.focused($focusedField, equals: .field1)
TextField("値2", text: $viewModel.value2)
.focused($focusedField, equals: .field2)
TextField("値3", text: $viewModel.value3)
.focused($focusedField, equals: .field3)
}
.textFieldStyle(.roundedBorder)
}
var pattern2View: some View {
VStack(alignment: .leading, spacing: 4) {
Text("パターン2")
.fontWeight(.black)
TextField("", text: $viewModel.value1)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .memoField)
}
}
var pattern1TextFieldsKeyboardCustomBar: some View {
HStack(spacing: 4) {
Button(action: {
moveToPreviousField()
}) {
Image(systemName: "chevron.left")
}
Button(action: {
moveToNextField()
}) {
Image(systemName: "chevron.right")
}
Spacer()
Button(action: {
focusedField = nil // キーボードを閉じる
}) {
Text("閉じる")
}
}
}
var pattern2TextFieldKeyboardCustomBar: some View {
HStack(spacing: 4) {
Spacer()
Button(action: {
focusedField = nil // キーボードを閉じる
}) {
Text("閉じる")
}
}
}
}
private extension HogeScreen {
func moveToPreviousField() {
switch focusedField {
case .field1:
focusedField = .field3
case .field2:
focusedField = .field1
case .field3:
focusedField = .field2
default:
focusedField = nil
}
}
func moveToNextField() {
switch focusedField {
case .field1:
focusedField = .field2
case .field2:
focusedField = .field3
case .field3:
focusedField = .field1
default:
focusedField = nil
}
}
}
#Preview {
HogeScreen()
}
にしてもprivate extensionで定義しているこいつはViewModelに移行したいものだ。なんかできそう。
今日時間ないので来週いっぱいで考えて実装してみる。
なんかこの解決策に若干懐疑的ではあるが一応メモ
同じように困っていて、こんな感じでtoolbar modifier内で表示を出し分けるようにしたコンポーネント経由で使うようにしたら同一画面の複数のTextFieldやTextEditorでもうまいこと制御できました!(これも微妙な実装ではあるのですが...)
引数にToolbarItemに出したいViewを取るようにすれば、上の実装も簡素化できるかもしれません。
struct TextEditorWithToolbar: View {
@Binding var text: String
@FocusState private var focus: Bool
var body: some View {
TextEditor(text: $text)
.focused($focus)
.toolbar {
if focus {
ToolbarItem(placement: .keyboard) {
HStack {
Spacer()
Button("閉じる") {
self.focus = false
}
}
}
}
}
}
}