【SwiftUI】iOS 15からの@FocusStateを使用して画面タップでキーボードを閉じる方法

2022.02.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

iOS 15から使用できるプロパティラッパー@FocusStateを使用してキーボードを閉じる方法を調べました。今回、画面をタップした時に@FocusStateを付与した値を更新してキーボードを閉じる方法を記載しています。

環境

  • Xcode 13.2.1

準備

TextFieldを二つ配置したViewを準備しました。

import SwiftUI

struct ContentView: View {

    @State private var titleText = ""
    @State private var messageText = ""

    var body: some View {

        VStack {

            TextField("タイトル", text: $titleText)
                .padding()
                .border(.black)
                .padding()

            TextField("メッセージ", text: $messageText)
                .padding()
                .border(.black)
                .padding()
        }
    }
}

まだこの時点では、TextFieldにフォーカスが当たった時に出てくるキーボードは改行ボタンを押した時にしか閉じることが出来ません。

@FocusStateを使用してキーボードを閉じる

@FocusState

@FocusStateとは、フォーカスが変更があった時にSwiftUIが更新する値を読み書きできるプロパティラッパーです。

@frozen @propertyWrapper struct FocusState<Value> where Value : Hashable

@FocusStateを使用する

フォーカスが当たるTextFieldを判断するためのenumを作成します。@FocusStateの定義にもある通り、ValueHashableである必要がある為、準拠しています。

enum Field: Hashable {
    case title
    case message
}

@FocusStateを付与した値をnilにするとキーボードが閉じてくれるのでオプショナルにしています。

@FocusState private var focusedField: Field?

TextFieldにフォーカスが当たった時に@FocusStateを更新

TextFieldfocusedモディファイアを追加します。

第一引数には@FocusStateの値を渡し、第二引数には今回はどのfocusedFieldを指しているのかを渡しています。

var body: some View {

    VStack {

        TextField("タイトル", text: $titleText)
            .padding()
            .border(.black)
            .padding()
            .focused($focusedField, equals: .title)

        TextField("メッセージ", text: $messageText)
            .padding()
            .border(.black)
            .padding()
            .focused($focusedField, equals: .message)
    }
}

画面タップでキーボードを閉じる

onTapGuesturefocuedFieldの値をnilにすると画面をタップ時にキーボードを閉じる事が出来ます。

このままだとタップ領域が狭い為、VStackframecontentShapeを追加してタップ領域を広げています。

VStack {
// 省略
}
.frame(width: UIScreen.main.bounds.width,
       height: UIScreen.main.bounds.height)
.contentShape(RoundedRectangle(cornerRadius: 10))
.onTapGesture {
    focusedField = nil
}

これで画面タップでキーボードが閉じれるようになりました。

before

キーボードが一度閉じてしまう問題

画面タップでキーボードを閉じれるようになったのですが、一度キーボードを出した状態で別のTextFieldをタップした時に一度キーボードを閉じなければならない状態になってしまいました。

これだとユーザー体験的には気持ち良くはないと思います。別のTextFieldに移動した際はキーボードを出した状態にする為に各TextFieldにもonTapGUestureを追加して、focusedFieldに値を入れるようにします。

TextField("タイトル", text: $titleText)
    .padding()
    .border(.black)
    .padding()
    .focused($focusedField, equals: .title)
    .onTapGesture {
        focusedField = .title
    }

TextField("メッセージ", text: $messageText)
    .padding()
    .border(.black)
    .padding()
    .focused($focusedField, equals: .message)
    .onTapGesture {
        focusedField = .message
    }

これで別のTextFieldに移った際にはキーボードが出た状態を保持することができるようになりました。

after

コード全体

今回のContentViewのコード全体になります。

import SwiftUI

struct ContentView: View {

    enum Field: Hashable {
        case title
        case message
    }

    @State private var titleText = ""
    @State private var messageText = ""
    @FocusState private var focusedField: Field?

    var body: some View {

        VStack {

            TextField("タイトル", text: $titleText)
                .padding()
                .border(.black)
                .padding()
                .focused($focusedField, equals: .title)
                .onTapGesture {
                    focusedField = .title
                }

            TextField("メッセージ", text: $messageText)
                .padding()
                .border(.black)
                .padding()
                .focused($focusedField, equals: .message)
                .onTapGesture {
                    focusedField = .message
                }
        }
        .frame(width: UIScreen.main.bounds.width,
               height: UIScreen.main.bounds.height)
        .contentShape(RoundedRectangle(cornerRadius: 10))
        .onTapGesture {
            focusedField = nil
        }
    }
}

おわりに

@FocusStateを使った良い画面タップでのキーボードの閉じ方、別TextFieldに移行した時にキーボードを表示したままにするやり方をご存知の方いましたら教えていただけますと嬉しいです。

参考