【SwiftUI】キーボードを閉じる(フォーカスを外す)

SwiftUI

SwiftUIでソフトウェアキーボードを閉じる方法についてです。

iPhoneではソフトウェアキーボードを使う事が殆どだと思いますが、
実はiOSでソフトウェアキーボードを閉じる手段は少ないです。

キーボードを閉じるにはフォーカスを外せば良いのですが、
標準では「改行(return)」を押すしかありません。

Safariでは閉じるボタンが表示されるなど、
なんらかのCloseボタンが用意されている事が多く、
そのままでは閉じる事が出来ないユーザーが居る可能性もあります。
キーボードを閉じやすいUIを提供する様にしましょう。

フォーカスを外す

まずはフォーカスを外す方法です。
以下の2つがあります

  • @FocusStateを使う(iOS15以降)
  • resignFirstResponderを使う(UIKitから)

iOS15からはSwiftUIのみで行えます。
iOS14以前はSwiftUIの機能としては存在しないのでUIKitの頃から使われていた方法を取ります。
とは言ってもSwiftUIのViewをそのまま使用できるので安心して下さい。

@FocusState

iOS15からは@FocusStateと言うpropertyWrapperが追加されました。
その名の通り、入力フォームのフォーカスの状態を管理出来ます。

struct ContentView: View {
    
    @State var text = ""
    @FocusState var focus:Bool

    var body: some View {
        VStack {
            Button("keyboard close"){
                self.focus = false
            }
            TextEditor(text: self.$text)
                .focused(self.$focus)
        }
    }
}

@FocusState属性をつけてBoolの変数を宣言します。初期値は設定できません。
.focused Modifierに変数を渡せばOKです。

これでフォーカスの状態が変数に反映される様になります。
フォーカスを外したい時はfalseを代入すればOKです。

複数の入力フォームがある場合もひとつの変数で管理出来ます。
以下は公式ドキュメントを参考に改変しました。

FocusState | Apple Developer Documentation
A property wrapper type that can read and write a value that SwiftUI updates as the placement of focus within the scene changes.
struct LoginForm {
    enum Field: Hashable {
        case username
        case password
    }

    @State private var username = ""
    @State private var password = ""
    @FocusState private var focusedField: Field?

    var body: some View {
        Form {
            TextField("Username", text: $username)
                .focused($focusedField, equals: .username)

            SecureField("Password", text: $password)
                .focused($focusedField, equals: .password)

            Button("keyboard close"){
                self.focus = nil
            }
        }
    }
}

この場合は変数にnilを代入する事で閉じる事が出来ます。

resignFirstResponder

resignFirstResponderを使う方法です。
UIKitのViewの場合はUIResponderを継承しているので直接呼びますが、
SwiftUIのViewに対しても使う方法があります。
UIApplicationのsendActionに指定して使用します。

struct ContentView: View {
    
    @State var text = ""

    var body: some View {
        VStack {
            Button("keyboard close"){
                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
            }
            TextEditor(text: self.$text)
        }
    }
}

こちらは閉じるだけになります。
1行ですが長いのでUIApplicationにextensionでメソッドを作っておく例をよく見ます。

簡単に制御できる@FocusStateを使うのが理想ですが、
こちらのパターンでもUIViewRepresentableを使ってUIKitのViewを使う必要はなく、
SwiftUIのViewをそのまま使用できるので気軽に利用できると思います。

フォーカスを外すUI

フォーカスを外す為のコードは紹介しましたが、
UIから呼び出さなければなりません。

ありがちなパターンとして3つほど紹介します。

  • ToolBarにCloseボタンを設置する
  • NavigationBarにCloseボタンを設置する
  • 入力フォーム外をタップしたらフォーカスが外れる

ToolBarにCloseボタンを設置する

個人的にはこれが一番好みです。
一番わかりやすく邪魔にならないと思います。
ただしToolbarItemにkeyboardが指定できるのはiOS15以降です。

struct ToolbarCloseView: View {
    
    @State var text = ""
    @FocusState var focus:Bool

    var body: some View {
        Form {
            TextEditor(text: self.$text)
                .focused(self.$focus)
                .toolbar{
                    ToolbarItem(placement: .keyboard){
                        HStack{
                            Spacer()
                            Button("Close"){
                                self.focus = false
                            }
                        }
                    }
            }
        }
}

iOS14以前では無理矢理キーボードの上に配置するか、
入力フォームをUIKitで実装するかになるので割愛します。

NavigationBarにCloseボタンを設置する

NavigationBarに表示してしまいます。
常に表示すると格好悪いのでキーボードがある時だけ表示します。

struct NavigationBarCloseView: View {
    
    @State var text = ""
    @FocusState var focus:Bool
    
    var body: some View {
        NavigationView {
            Form {
                TextEditor(text: self.$text)
                    .focused(self.$focus)
                    .toolbar{
                        ToolbarItem(placement: .navigationBarTrailing){
                            if self.focus {
                                Button("Close"){
                                    self.focus = false
                                }
                            }
                        }
                    }
            }
        }
    }
}

iOS14以前では@FocusStateが使えないので別な方法でキーボードの開閉を検知します。
またiOS13ではtoolbarが使えないのでNvigationBarItemを使います。

struct NavigationBarCloseOldView: View {
    
    @State var text = ""
    @State var focus = false
    
    var body: some View {
        NavigationView {
            Form {
                TextEditor(text: self.$text)
                    .navigationBarItems(trailing: closeButton())
                    .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
                                self.focus = true
                    }
                    .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidHideNotification)) { _ in
                                self.focus = false
                    }
            }
        }
    }
    
    @ViewBuilder
    func closeButton() -> some View {
        if self.focus {
            Button("Close"){
                UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
            }
        }
    }
}

onReceiveでキーボードの表示切り替えの通知を受け取り、
ボタン表示切り替えを行なっています。
iOS15で如何に楽になったかが感じ取れますね・・・

入力フォーム外をタップしたらフォーカスが外れる

FormにonTapGestureを設定してしまいます。
割と見かけるパターンです。

struct OnTapOutsideView: View {
    
    @State var text = ""
    @FocusState var focus:Bool
    
    var body: some View {
        Form {
            TextEditor(text: self.$text)
                .focused(self.$focus)
        }
        .onTapGesture {
            self.focus = false
        }
    }
}

但し、このやり方を行うとForm内のボタン等が効かなくなります。
ButtonやPickerなどはForm外に設置する必要が出てきます。

またForm内のテキストなど操作しない表示のみの部分に個別で設定する手もありますが、
面倒ですし反応しない部分も出来てしまいます。

TapだとダメなのでDragにしてみます。

struct OnScrollView: View {
    
    @State var text = ""
    @FocusState var focus:Bool
    
    var gesture: some Gesture {
        DragGesture()
            .onChanged{ value in
                if value.translation.height != 0 {
                    self.focus = false
                }
            }
    }
    
    var body: some View {
        Form {
            TextEditor(text: self.$text)
                .focused(self.$focus)
        }.gesture(self.gesture)
    }
}

これならばButtonも反応します。
iOS標準の連絡先アプリもタップではフォーカスが外れませんが、
僅かにでもスクロールするとフォーカスが外れるので、
これに近い処理をしているのではないでしょうか?

最後に

ソフトウェアキーボードの閉じ方を色々と紹介しました。

実際に試してみるとわかりますが、
デフォルトのままだと閉じないパターンが多いです。

iOS15以降でないと、ひと工夫必要になりますが、
ユーザーライクなUIになる様しっかり作っていきましょう。

コメント

タイトルとURLをコピーしました