🎹

iOS15からのキーボード回避[UIKit]

2021/12/04に公開
2

🗓この記事はClassi developers Advent Calendar 2021の4日目です🗓

まえがき

iPhoneアプリを開発している中で幾度となく実装する入力フォーム。
この入力フォームで必ずと言って良いほど要件に上がるのが、キーボードが表示された時に入力フォームが隠れないようにするというものです。

今まで幾多のiOSエンジニアがこれを実現するために、ScrollViewの中にTextFieldを配置したり、NotificationCenterを使ってキーボード表示イベントをキャッチしたりとあらゆる手を尽くしてきました。

それがついにiOS15からUIKeyboardLayoutGuideというものが用意され、従来のAutoLayoutの要領で実現可能となりました🎉
今回はそちらのやり方をご紹介しようと思います。

この記事でできる事

UIKeyboardLayoutGuideとは

アプリのレイアウトの中でキーボードが占めるスペースを表すレイアウトガイド

A layout guide that represents the space the keyboard occupies in your app’s layout.

ドキュメントより引用

要するにキーボードの矩形に合わせたレイアウトガイドという事ですね!
例えばview.keyboardLayoutGuide.topAnchorはキーボードが画面に表示されている時はその矩形の上辺、表示されていない時はSafeAreaのBottomと同等になるようです。

早速やってみる

まずはコードでTextFieldを画面下部に配置します。

import UIKit

class ViewController: UIViewController {
    private lazy var field: UITextField = {
        let field = UITextField()
        field.translatesAutoresizingMaskIntoConstraints = false
        field.borderStyle = .roundedRect
        field.placeholder = "Input here"
        return field
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpUI()
    }
    
    private func setUpUI() {
        view.backgroundColor = .systemBackground
        view.addSubview(field)
        NSLayoutConstraint.activate([
            field.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            field.widthAnchor.constraint(equalToConstant: 200),
            field.heightAnchor.constraint(equalToConstant: 40)
        ])
    }
}

これで幅200、高さ40のTextFieldが画面の下部に設置されました。

しかしこれだけでは当然フォームをタップするとキーボードに隠れてしまいます。
そこで、NSLayoutConstraint.activateの制約の中に以下を追加します。

view.keyboardLayoutGuide.topAnchor.constraint(equalTo: field.bottomAnchor, constant: 10)

すると、なんという事でしょう。
フォームが、ポップしてキーボードに追従するようになったではありませんか!

念の為、制約の設定部分は以下のようになっています。

NSLayoutConstraint.activate([
    field.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    field.widthAnchor.constraint(equalToConstant: 200),
    field.heightAnchor.constraint(equalToConstant: 40),
    view.keyboardLayoutGuide.topAnchor.constraint(equalTo: field.bottomAnchor, constant: 10)
])

最後に追加した

view.keyboardLayoutGuide.topAnchor.constraint(equalTo: field.bottomAnchor, constant: 10)

の部分は、iOS15からUIViewがプロパティとして持つようになったkeyboardLayoutGuideTopに対して、フォームのBottomの制約をつけているため上記のような動きが実現できたわけですね!
最後のconstant: 10の部分はスペースを10ポイントあけるという事なので、あっても無くても大丈夫です。

Storyboard上で設定できないの?

試してみたのですが、現状は難しそうです。。。
選択できるのは相変わらずSuperviewかSafeAreaでした。

それでもどうしてもStoryboard上でUIを組みたい時もあると思います。
その場合はStoryboard上では一旦SafeAreaに制約をつけて、Priorityを下げる事で対応できると思います。

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet private var field: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpUI()
    }
    
    private func setUpUI() {
        let keyboardConstraint = view.keyboardLayoutGuide.topAnchor.constraint(equalTo: field.bottomAnchor, constant: 10)
        keyboardConstraint.priority = .defaultHigh
        NSLayoutConstraint.activate([
            keyboardConstraint
        ])
    }
}

フォームが複数ある時は?

そうですよね。
むしろよく実装する入力フォームは項目が複数あることが普通。
逆に複数あるからこそ画面の下の方までフォームが配置される事になって、キーボードと被ってしまうんですよね。

ということで、複数の入力フォームがあって下の方のものだけがキーボードに被るケースを考えてみましょう。
こんな画面ですね。

UIの構成はUIStackViewの中にUITextFieldが並んでいるだけのシンプルなものとします。

まず一番簡易的な対応としては、一番下のフォームに対して制約をつけるという方法があります。

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet private var firstNameField: UITextField!
    @IBOutlet private var lastNameField: UITextField!
    @IBOutlet private var addressField: UITextField!
    @IBOutlet private var phoneNumberField: UITextField!
    @IBOutlet private var emailField: UITextField!
    @IBOutlet private var confirmEmailField: UITextField!
    @IBOutlet private var stackView: UIStackView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpUI()
    }
    
    private func setUpUI() {
        NSLayoutConstraint.activate([
            view.keyboardLayoutGuide.topAnchor.constraint(greaterThanOrEqualTo: confirmEmailField.bottomAnchor, constant: 10)
        ])
    }
}

これで以下のような動きが実現できます!

フォームによっては有効そうですね👏
ちなみに、この挙動をデバッガで確認してみたところ、StackView自体のサイズは変更されず中の要素だけが動いていました。

今回のようにただフォームが並んでいるだけで余白もちゃんとある画面であれば上記の対応で問題なさそうですが、実際はもっと画面要素が多い事が大半だと思います。
なのでその場合のケースも考えてみます。
たとえばこんな画面です。

この場合はこれまで通り画面のスクロールを取り入れた方が良さそうです。
具体的な構成としてはUIScrollViewUIStackViewを置いて、その中にUILabelUITextFieldを並べたカスタムビューを均等に並べるイメージです。
このようなスクロールを伴うフォームの場合、これまではキーボード表示の通知を受け取って、キーボードのフレームのy座標を取得して、フォーカスが当たっているフィールドの座標と比較して...とかなり面倒なことをしていたと思いますが、これからは以下の制約を付与するだけで良い感じの動作が実現できます。

NSLayoutConstraint.activate([
    view.keyboardLayoutGuide.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 10)
])

ただこれだけです。

これで以下のような動きになります。

アメージング🎉🎉🎉

あとがき

いかがでしたでしょうか。
これを使えるのはiOS15以降ではありますが、これまでの実装の手間を大幅に解消できるのではと思います。
色々な工夫で是非素敵なキーボド回避ライフを送ってください。

ちなみに、今回は触れませんでしたがiPadの場合はキーボードを切り離して操作できる機能が備わっています。
そのためKeyboardLayoutGuideを使う場合はひと工夫必要となります。
詳しくはこちらが参考になると思います。

また、今回ご紹介したサンプルコードはGitHubにて公開しております。
「すごい、こんなやり方があったのか!」とか、「手間が解消されて役に立った!」と感じた方は是非チャンネル登録(フォロー)、高評価(スター)をよろしくお願いします!

さて、明日のClassi developers Advent Calendar 2021らいむさんです✨
みなさまお楽しみに!!

Discussion

KengoKengo

貴重な情報ありがとうございます!参考になりました。
IBでもkeyboard layout guideとの制約を付けることができるようでした。下記の記事に記載がありました。ご参考までに。
https://useyourloaf.com/blog/interface-builder-keyboard-layout-guide/

SabSab

コメントありがとうございます!
本当ですね、全然知りませんでした💦
いつからできたのでしょう。。。
記事の方も更新させていただきました🙇