6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Swift】テキストとキーボードが被らないようにする処理

6
Last updated at Posted at 2019-01-04

Swiftエンジニアなら一度は実装したことがありそうな、
"テキストとキーボードが被らないようにする処理"を作りましたが、思いの外時間をかけてしまいました。
悔しいので一度丁寧に作り、まとめてみることにしました。

まずはStoryboardです。ScrollViewの上に全ての部品が乗っていますね。
swiftKeyboarLift.jpg

それではソースコードをどうぞ。

ProfileViewController.swift
//
//  ViewController.swift
//  KeyboardSampleParts
//

import UIKit

class ProfileViewController: UIViewController {
    @IBOutlet var nameTextField: UITextField!
    @IBOutlet var ageTextField: UITextField!
    @IBOutlet var remarksTextField: UITextView!
    @IBOutlet var baseScrollView: UIScrollView!
    
    // 選択中のテキストフィールドを保持
    var activeTextUIView: UIView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        nameTextField.delegate = self
        ageTextField.delegate = self
        remarksTextField.delegate = self
    }
    
    override func viewWillAppear(_ animated: Bool) {
        // キーボードの表示/非表示を監視する
        NotificationCenter.default.addObserver(self, selector: #selector(self.openKeyboard(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.closeKeyboard(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        // キーボードの表示/非表示を監視を解除する
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }
    
    @objc func openKeyboard(_ notification: Notification){
        // 選択したテキストビューの下辺Y座標を取得する
        guard let activeView = activeTextUIView else{
            return
        }
        let textMaxYpoint = activeView.convert(activeView.frame, to: self.view).maxY

        // キーボードの高さを取得する
        guard let userInfo = notification.userInfo as? [String: Any] else {
            return
        }
        guard let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else{
            return
        }

        // 画面の高さを取得する
        let myScreenHeight = UIScreen.main.bounds.size.height
        
        // テキストの下辺 + キーボードの高さ - スクリーンの高さ = キーボードに隠れてしまったYの領域
        let liftY = textMaxYpoint + keyboardScreenEndFrame.height - myScreenHeight
        
        // 0よりも大きい時、テキストがキーボードに隠れてしまう
        if liftY >= 0{
            // スクロールビューを上にあげる
            baseScrollView.setContentOffset(CGPoint.init(x: 0 , y: liftY), animated: true)
        }
    }
    
    @objc func closeKeyboard(_ notification: Notification){
        // キーボードを閉じた時、スクロールビューを元の高さに戻す
        baseScrollView.setContentOffset(CGPoint.init(x: 0 , y: 0), animated: true)
        activeTextUIView = nil
    }
}

extension ProfileViewController:UITextFieldDelegate{
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        // 選択中のテキストフィールドを設定
        activeTextUIView = textField
        return true
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        // 選択中のテキストフィールドを削除
        textField.resignFirstResponder()
        return true
    }
}

extension ProfileViewController:UITextViewDelegate{
    func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
        // 選択中のテキストフィールドを設定
        activeTextUIView = textView
        return true
    }
}

extension UIScrollView {
    override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.next?.touchesBegan(touches, with: event)
    }
}

実機テスト

縦画面
IMG_2320.jpg

横画面
IMG_2322.jpg

解説

処理の流れは以下の通りです。
1.画面初期表示時:キーボードの表示/非表示を監視する
2.テキスト編集前:選択中のテキストを保持する
3.キーボード表示時:テキストの下辺がキーボードに被るなら、スクロールビューを上にあげる

4.キーボード非表示時:スクロールビューを一番上に戻す
5.キーボード非表示時:選択中のテキストを破棄する
6.画面非表示時:キーボードの表示/非表示を監視を解除する

ポイント

・入れ子のテキストビューの下辺を取るのが大変だよ

入れ子になっているtextViewで

textView.frame.maxY

と書いても親ビューからのmaxY値しか取れません。
convertを使って下記のようにやると簡単に取得することが出来ます。

	let textMaxYpoint = activeView.convert(activeView.frame, to: self.view).maxY

・なんかtouchesBeganが効かないよ

スクロールビューはデフォルトではタッチできません。extensionが必要になります。
※本来 「UIScrollView+touchesBeganEvent.swift」といったクラス名に分けるべきでしょうが、サンプルのためまとめています。

extension UIScrollView {
    override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.next?.touchesBegan(touches, with: event)
    }
}

・テキストフィールド保存する部品がUIViewクラスだけど?

    // 選択中のテキストフィールドを保持
    var activeTextUIView: UIView?

UITextFieldとUITextViewを同じ変数に保存したかったので、このような実装にしてみました。

以上になります。

6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?