【Swift UI】TextFieldのキーボードを閉じる方法と@FocusStateの使い方

この記事からわかること

  • SwiftUITextFieldnumberPadを指定する場合
  • キーボード閉じることができない場合の解決法
  • @FocusState使い方閉じるボタンの自作方法
  • 子ViewにあるTextFieldの閉じるボタンを実装する方法
  • ビルドしたシミュレーターにキーボードを出現させる
  • 閉じるボタンが表示されない問題の解決法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

Swift UIでアプリを開発中にTextFieldの入力画面でキーボードをnumberPad(数字のみ)にしたところ閉じることができなくなってしまいました。

また親Viewと子Viewに分けて作成していたところ、子ViewのTextFieldに閉じるボタンを実装しようとしても表示されませんでした。

今回は「閉じるボタンの実装方法」と「子ViewにあるTextFieldへの閉じるボタンの実装方法」をまとめていきます。

TextFieldでnumberPadのみ発生する閉じれない問題

Swift UIではTextFieldを使うと簡単に以下のような入力可能な要素を作成できます。

TextField("km", text: $milage)
      .textFieldStyle(RoundedBorderTextFieldStyle())
      .keyboardType(.numberPad)
TextFieldでnumberPadのみ発生する閉じれない問題

入力欄にフォーカスを当てるとキーボードが表示されますが、数字のみの入力にしたい場合は.keyboardType(.numberPad)で数字キーのみに指定することができました。

しかし通常はEnterキーの押下でキーボードは閉じられますが、数字キーのみの場合はEnterキーがなく閉じることができなくなってしまいます

解決策として閉じるボタンを自作で実装することでキーボードを閉じることができるようにしていきます。

TextFieldでnumberPadのみ発生する閉じれない問題

閉じるボタンを自作方法

閉じるボタンを自作する際に重要なポイント

TextFieldのキーボードが出現するのは対象のTextFieldフォーカスが当たっている時です。TextFieldのフォーカスは.focused(変数)で取得、操作することが可能になっています。(Bool値)

TextField("km", text: $milage)
      .textFieldStyle(RoundedBorderTextFieldStyle())
      .keyboardType(.numberPad)
      .focused($isActive)  // このTextFieldのフォーカスをBool値で取得、操作

@FocusStateの使い方

@FocusState  var isActive:Bool 

.focused(変数)に渡す変数は@FocusStateが付与されていないといけません

@FocusStateとはiOS15以降(Swift5.1)から使えるようになったProperty Wrapper(プロパティラッパー)の1つです。プロパティラッパーはプロパティに対する操作や処理をカプセル化して定義することで特定のキーワードを付与するだけで任意の動作や挙動を行わせることができる便利な機能です。

その中でも@FocusStateはその名前の通り、TextFieldなどの要素のフォーカス制御を可能にしています。

@FocusStateを付与したコード

struct ContentView: View {
    @State  var milage:String = ""   
    @FocusState  var isActive:Bool
    
    var body: some View {
        TextField("km", text: $milage)
              .textFieldStyle(RoundedBorderTextFieldStyle())
              .keyboardType(.numberPad)
              .focused($isActive)  
    }
}

しかしこれではまだボタンの実装はできていません。実際のボタンは.toolbarで実装していきます。

toolbarとToolbarItemGroup

TextFieldでnumberPadのみ発生する閉じれない問題

ボタンを実装するにはtoolbarモディファイアのToolbarItemGroup内に実装していきます。

閉じるボタンを実装したコード

struct ContentView: View {
    @State  var milage:String = ""   
    @FocusState  var isActive:Bool
    
    var body: some View {
        TextField("km", text: $milage)
              .textFieldStyle(RoundedBorderTextFieldStyle())
              .keyboardType(.numberPad)
              .focused($isActive)
              .toolbar {
                  ToolbarItemGroup(placement: .keyboard) {
                      Spacer()         // 右寄せにする
                      Button("閉じる") {
                          isActive = false  //  フォーカスを外す
                      }
                  }
              }
    }
}

.focusedをつけてフォーカスを制御できるようにしたTextFieldtoolbarモディファイアを追加します。中にはToolbarItemGroupを定義し、引数のplacement.keyboardを指定します。

ToolbarItemGroupの中にはButtonを定義し、そのアクション部分にフォーカスが外れる処理(falseを格納)します。

Buttonの上側にSpacer()を入れることで閉じるボタンを右寄せにすることができます。

これで数字のみのキーボードに閉じるボタンを実装することができました。

Stackの中に複数のTextFieldがある場合

TextFieldが複数あり、○Stackで囲んでいる場合はtoolbarモディファイアは○Stackに指定してもOKです。

struct ContentView: View {
    @State  var milage:String = ""
    @State  var refueling:String = ""
    @FocusState  var isActive:Bool
    
    var body: some View {
        VStack {
            HStack {
                Text("走行距離")
                TextField("km", text: $milage)
                      .textFieldStyle(RoundedBorderTextFieldStyle())
                      .keyboardType(.numberPad)
                      .focused($isActive)
            }
            HStack{
                Text("給油量")
                TextField("ℓ",text: $refueling)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .keyboardType(.numberPad)
                    .focused($isActive)
            }
        }.toolbar {  // VStackに指定
            ToolbarItemGroup(placement: .keyboard) {
                Spacer()         // 右寄せにする
                Button("閉じる") {
                    isActive = false  //  フォーカスを外す
                }
            }
        }
    }
}

フォーカスを制御している変数はTextFieldが複数あっても1つで問題ありませんでした。toolbarモディファイアを特定の1つのTextFieldに指定しても期待通りの動作をしましたが、可読性の観点から囲んでいるStackに当てるのがおすすめです。

子ViewにあるTextFieldへの閉じるボタンの実装方法

解決策

親Viewにfocusedとtoolbarを実装する

先ほどのように1つのビュー内で入れ子になっている場合はその一番上の要素の当てるだけでよかったですが、親ビューから子ビューを呼び出している場合は閉じるボタンの実装に注意が必要です。

子ビュー側@FocusStateと「閉じるボタン」を実装して親ビューから呼び出してみたところ数字のみのキーパッドに閉じるボタンどころかツールバー自体が表示されませんでした。

閉じるボタンが表示されなかったコード

子ビュー

struct TextFieldTest: View {
  @State    var milage:String = ""
  @FocusState  var isInputActive:Bool  // ナンバーパッドのフォーカス
  
  var body: some View {
      TextField("km", text: $milage)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .keyboardType(.numberPad)
            .focused($isInputActive)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    Spacer()  // 右寄せにする
                    Button("Done") {
                        isInputActive = false
                    }
                }
            }
  }
}

親ビュー

struct ContentView: View {
  @State  var selectedTag = 1

    
    var body: some View {
        
        TabView(selection: $selectedTag){
            TextFieldTest().tabItem{
                Text("Field1")
            }.tag(1)
            
            TextFieldTest().tabItem{
                Text("Field2")
            }.tag(2)
        }
        
        
    }
}

これを解決するためには親ビュー側にフォーカスの制御(@FocusState)とツールバーへの閉じるボタンを追加を記述しないといけないようです。

解決したコード

子ビュー

struct TextFieldTest: View {
  @State  var milage:String = ""

  var body: some View {
      TextField("km", text: $milage)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .keyboardType(.numberPad)
  }
}

親ビュー

struct ContentView: View {
  @State  var selectedTag = 1          // タブ切り替え
  @FocusState  var isInputActive:Bool  // ナンバーパッドのフォーカス
    
    var body: some View {
        
        TabView(selection: $selectedTag){
            TextFieldTest().tabItem{
                Text("Field1")
            }.tag(1)
            .focused($isInputActive) // フォーカスの制御

            TextFieldTest().tabItem{
                Text("Field2")
            }.tag(2)
            .focused($isInputActive) // フォーカスの制御
        }
        .toolbar {                   // ツールバーを親の一番上の要素に実装
            ToolbarItemGroup(placement: .keyboard) {
                Spacer()  // 右寄せにする
                Button("Done") {
                    isInputActive = false
                }
            }
        }
        
    }
}

ビルドしたシミュレーターにキーボードを出現させる

プレビューではキーボードが表示されないのでシミュレーターを起動させて確認する必要があります。

Xcodeの新規プロジェクト編集画面

ツールバーにある ボタンをクリックしてシミュレーターを起動させてTextFieldフォーカスを当ててもキーボードが出ない場合もあります

解決するには「Simulator」>「I/O」>「Keyboard」>「Toggle Software Keyboard」をクリックするとフォーカスが当たった時にキーボードが出現するようになります。

TextFieldを使ったアプリのビルドしたシミュレーションにキーボードを出現させる

TextFieldの使い方

理解を深めるためにTextFieldの使い方と注意点をまとめておきます。

TextFieldを使用するポイント

構文

TextField(titleKey: LocalizedStringKey, text: Binding<String>)

使用例

TextField("km", text: $milage)
    .textFieldStyle(RoundedBorderTextFieldStyle()) // 角に丸みを帯びた入力要素
    .keyboardType(.numberPad) // 数字のみのキーボードを表示

カスタマイズした枠線をつける方法

TextFieldに枠線を付けるには先ほど紹介したtextFieldStyle(RoundedBorderTextFieldStyle())を指定することで「角に丸みを帯びた枠線」を付与することができます。

SwiftUIのTextfieldでtextFieldStyle(RoundedBorderTextFieldStyle())を使って枠線をつけた様子

しかし色をつけた枠線を付ける方法は今のところないので要素の上に要素を重ねることのできるoverlayモディファイアを使用して枠線を作成していきます。

TextField("住所", text: $address)
.overlay(
    RoundedRectangle(cornerRadius: 2)
        .stroke(.cyan,lineWidth: 3)
)
SwiftUIのTextfieldでカスタマイズした枠線をつける方法

閉じるボタンが表示されない問題の解決法

以下のようにsheetモディファイアを使用して表示させたビューではなぜか「閉じるボタン」が表示されませんでした。それだけでなくナビゲーションタイトルも表示されませんでした。sheetモディファイアを使用したビューではナビゲーションバー(ツールバー)が表示されないようです。

struct TestNavigationView: View {
    
    @FocusState  var isInputActive: Bool
    @State  var text: String = ""
    @State  var isModal: Bool = false
    
    var body: some View {
        NavigationStack {
            Button {
                isModal = true
            } label: {
                Text("isModal")
            }
            .sheet(isPresented: $isModal, content: {
                    VStack {
                        TextField("", text: $text)
                            .frame(width: 120)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            .focused($isInputActive)
                    }.navigationBarTitle("Test")
                    .toolbar {
                        ToolbarItem(placement: .keyboard) {
                            Button("閉じる") {
                                isModal = false
                            }
                        }
                    }
            })
        }
    }
}

これを解決するには以下のようにsheetモディファイアで渡すビューもNavigationStackで囲むことで正常に表示されるようになりました。(この方法が正しいのかは分かりませんが...)

解決策(NavigationStackで囲むだけ)

struct TestNavigationView: View {
    
    @FocusState  var isInputActive: Bool
    @State  var text: String = ""
    @State  var isModal: Bool = false
    
    var body: some View {
        NavigationStack {
            Button {
                isModal = true
            } label: {
                Text("isModal")
            }
            .sheet(isPresented: $isModal, content: {
                NavigationStack {
                    VStack {
                        TextField("", text: $text)
                            .frame(width: 120)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            .focused($isInputActive)
                    }.navigationBarTitle("Test")
                    .toolbar {
                        ToolbarItem(placement: .keyboard) {
                            Button("閉じる") {
                                isModal = false
                            }
                        }
                    }
                }
            })
        }
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index