[Swift4]複数のTextFieldがキーボードに隠れて入力できない時の対策

iOSアプリで、複数のTextFieldを持ったフォームを作る場面、よくありますよね。そんな時に、何も考えずにTextFieldを組んだらこうなっちゃったこと、ありませんか。

TextFieldがキーボードで隠れる

そう、TextFieldがキーボードで隠れるんすよ。Androidだと、入力部分を上にスライドさせてくれるんですが、iOSはそういう親切なあれはありません。なので、自分で実装して、やり方をまとめてみました。

Swift: 4.0.3
Xcode: 9.2
ソース:
https://github.com/yheihei/ios/tree/master/TextFieldScrolleAndClose

こちらの記事は当初運営者メンバーの執筆でしたが現在は離れており、寄稿記事となっています。そのため当記事の情報アップデートにつきましては2020年1月をもって終了・サポート・内容についての問合せなどは受けておりません。また、記事内容を利用された際に生じた内容にも責任を負えませんのでご了承ください。

完成図

TextFieldを選択すると、画面がスクロールし、キーボードで隠れなくなる。どのTextFieldが選択されているかを見て、スクロール量を決めてスクロールしている。

TextFieldが選択されたら画面をスライドさせる

 

やり方

ルートをScrollViewにする

一番下はScrollView

AutoLayoutの設定は下記。SafeAreaいっぱいに。

ScrollViewのConstraint

 

ScrollViewの子にViewを置く

ViewをScrollViewの子に置いて、AutoLayoutの設定は、ScrollViewいっぱいに。

ScrollViewの下にView

ViewのConstraint

必ずEqual Widthと Equal HeightをSafeAreaと同じにしておく。(ここ重要

なぜかViewのEqual WidthとHeighをいじる図

 

TextFieldを下の方に配置

適当に下の方に配置する。

TextFieldを画面下部に配置

TextFieldのReturn Keyを「Done」にしておく。後ほど、コードの中でキーボードの「Done」が押されたらtextFieldShouldReturnイベントを受けてキーボードを閉じるため。

TextFieldのreturn keyを設定しておく

 

ソース全文と解説

TextFieldScrolleAndCloseViewController.swift

//
//  TextFieldScrollAndCloseViewController.swift
//  TextFieldScrolleAndClose
//
//  Created by 小久保洋平 on 2018/01/29.
//  Copyright © 2018年 Yohei Kokubo. All rights reserved.
//

import UIKit

class TextFieldScrollAndCloseViewController: UIViewController {
    
    // テキストフィールドが選択されたらキーボード表示とともにスクロールさせるView
    @IBOutlet weak var scrollView: UIScrollView!
    
    // 現在選択されているTextField
    var selectedTextField:UITextField?

    @IBOutlet weak var textField1: UITextField!
    @IBOutlet weak var textField2: UITextField!
    @IBOutlet weak var textField3: UITextField!
    
    
    override func viewDidLoad() {
        super.viewDidLoad()

        self.textFieldInit() // TextFieldのセットアップ
        // Do any additional setup after loading the view.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // キーボードイベントの監視開始
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(keyboardWillBeShown(notification:)),
                                               name: NSNotification.Name.UIKeyboardWillShow,
                                               object: nil)
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(keyboardWillBeHidden(notification:)),
                                               name: NSNotification.Name.UIKeyboardWillHide,
                                               object: nil)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        // キーボードイベントの監視解除
        NotificationCenter.default.removeObserver(self,
                                                  name: NSNotification.Name.UIKeyboardWillShow,
                                                  object: nil)
        NotificationCenter.default.removeObserver(self,
                                                  name: NSNotification.Name.UIKeyboardWillHide,
                                                  object: nil)
    }
    

    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destinationViewController.
        // Pass the selected object to the new view controller.
    }
    */

}

extension TextFieldScrollAndCloseViewController: UITextFieldDelegate {
    func textFieldInit() {
        // 最初に選択されているTextFieldをセット
        self.selectedTextField = self.textField1
        
        // 各TextFieldのdelegate 色んなイベントが飛んでくるようになる
        self.textField1.delegate = self
        self.textField2.delegate = self
        self.textField3.delegate = self

    }
    
    // キーボードが表示された時に呼ばれる
    @objc func keyboardWillBeShown(notification: NSNotification) {
        if let userInfo = notification.userInfo {
            if let keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as AnyObject).cgRectValue, let animationDuration = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as AnyObject).doubleValue {
                restoreScrollViewSize()
                
                let convertedKeyboardFrame = scrollView.convert(keyboardFrame, from: nil)
                // 現在選択中のTextFieldの下部Y座標とキーボードの高さから、スクロール量を決定
                let offsetY: CGFloat = self.selectedTextField!.frame.maxY - convertedKeyboardFrame.minY
                if offsetY < 0 { return }
                updateScrollViewSize(moveSize: offsetY, duration: animationDuration)
            }
        }
    }
    
    // キーボードが閉じられた時に呼ばれる
    @objc func keyboardWillBeHidden(notification: NSNotification) {
        restoreScrollViewSize()
    }
    
    // TextFieldが選択された時
    func textFieldDidBeginEditing(_ textField: UITextField) {
        // 選択されているTextFieldを更新
        self.selectedTextField = textField
    }
    
    // リターンが押された時
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        // キーボードを閉じる
        textField.resignFirstResponder()
        return true
    }
    
    // moveSize分Y方向にスクロールさせる
    func updateScrollViewSize(moveSize: CGFloat, duration: TimeInterval) {
        UIView.beginAnimations("ResizeForKeyboard", context: nil)
        UIView.setAnimationDuration(duration)
        
        let contentInsets = UIEdgeInsetsMake(0, 0, moveSize, 0)
        self.scrollView.contentInset = contentInsets
        self.scrollView.scrollIndicatorInsets = contentInsets
        self.scrollView.contentOffset = CGPoint(x: 0, y: moveSize)
        
        UIView.commitAnimations()
    }
    
    func restoreScrollViewSize() {
        // キーボードが閉じられた時に、スクロールした分を戻す
        self.scrollView.contentInset = UIEdgeInsets.zero
        self.scrollView.scrollIndicatorInsets = UIEdgeInsets.zero
    }
}

 

 

ポイント

  1. レイアウトはルートをScrollViewに、その下にViewを入れ、その下にようやくTextField。Constraintが直感的でなく、ハックっぽいので注意(なぜEqual WidthとかEqual Heightが必要なのか謎)
  2. UITextFieldDelegateを用いて、textFieldDidBeginEditingでどのTextFieldが選択されたかのイベントを受ける
  3. キーボードが開いた時に、keyboardWillBeShownが呼ばれる。2で取得したTextFieldのY座標を取得し、scrollViewをどのぐらいスクロールさせるか決める
  4. textFieldShouldReturnで「Done」が押されたのを検知し、resignFirstResponderでキーボードを閉じる
  5. キーボードを閉じたら、keyboardWillBeHiddenが呼ばれ、3でスクロールした量を元に戻す

やってみると簡単だけど、大変めんどくさいすね。Androidはこんなこと考えなくても良いのに。。。

ソース:
https://github.com/yheihei/ios/tree/master/TextFieldScrolleAndClose