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

そう、TextFieldがキーボードで隠れるんすよ。Androidだと、入力部分を上にスライドさせてくれるんですが、iOSはそういう親切なあれはありません。なので、自分で実装して、やり方をまとめてみました。
Swift: 4.0.3
Xcode: 9.2
ソース:
https://github.com/yheihei/ios/tree/master/TextFieldScrolleAndClose
こちらの記事は当初運営者メンバーの執筆でしたが現在は離れており、寄稿記事となっています。そのため当記事の情報アップデートにつきましては2020年1月をもって終了・サポート・内容についての問合せなどは受けておりません。また、記事内容を利用された際に生じた内容にも責任を負えませんのでご了承ください。
完成図
TextFieldを選択すると、画面がスクロールし、キーボードで隠れなくなる。どのTextFieldが選択されているかを見て、スクロール量を決めてスクロールしている。

やり方
ルートをScrollViewにする

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

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


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

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

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

ソース全文と解説
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
}
}
ポイント
- レイアウトはルートをScrollViewに、その下にViewを入れ、その下にようやくTextField。Constraintが直感的でなく、ハックっぽいので注意(なぜEqual WidthとかEqual Heightが必要なのか謎)
- UITextFieldDelegateを用いて、textFieldDidBeginEditingでどのTextFieldが選択されたかのイベントを受ける
- キーボードが開いた時に、keyboardWillBeShownが呼ばれる。2で取得したTextFieldのY座標を取得し、scrollViewをどのぐらいスクロールさせるか決める
- textFieldShouldReturnで「Done」が押されたのを検知し、resignFirstResponderでキーボードを閉じる
- キーボードを閉じたら、keyboardWillBeHiddenが呼ばれ、3でスクロールした量を元に戻す
やってみると簡単だけど、大変めんどくさいすね。Androidはこんなこと考えなくても良いのに。。。
ソース:
https://github.com/yheihei/ios/tree/master/TextFieldScrolleAndClose