Swift LINEぽいメッセージアプリの入力UIをつくったのでメモ

はじめに

かなり前にこちらでメッセージを表示する部分の吹き出しの作り方をメモした。
http://kimagureneet.hatenablog.com/entry/2015/09/19/005407

今回はメッセージ入力部分のUIの作り込みの方法をメモ。こんな感じのものを作りたかった。
f:id:yoppy0066:20181101222535g:plain:w200

やりたいことは
・入力フォームを画面下に固定
・フォーカスを当てたらキーボードの上に入力フォームを移動
・複数行にも対応したいのでUITextFieldでなくUITextViewを使用

上記を満たすためにやらなくてはならないこと(上と重複するけど)
・キーボードを開いたとき
入力フォームの位置をキーボードの上に移動
UITextViewの高さを複数行見えるように変更

・キーボードを閉じたとき
入力フォームの位置を画面下に移動
UITextViewの高さを1行見えるように変更

ざっくりこんな感じ。あとはキーボードの上に完了ボタンを追加したり細々とした内容。

実装

import UIKit

class ViewController: UIViewController, UITextViewDelegate {

    let MARGIN_MSG: CGFloat = 1
    let HEIGHT_BOX: CGFloat = 50
    let WIDTH_SUBMIT: CGFloat = 70

    let MIN_HEIGHT_MSG: CGFloat = 50
    let MAX_HEIGHT_MSG: CGFloat = 100

    // メッセージを表示する領域
    var tableView: UITableView!

    // メッセージBOX(入力フォームと送信ボタン)
    var box: UIView!
    var msg: UITextView!
    var submit: UIButton!

    // テーブルビューのサイズ情報
    var sizeTable: CGSize!

    // メッセージBOXのフレーム情報(キーボードが閉じられた時の情報)
    var frBox: CGRect!

    // キーボードのフレーム情報
    var frKeyboard: CGRect!

    override func viewDidLoad() {
        super.viewDidLoad()
	setView()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
	let notificationCenter = NotificationCenter.default

        notificationCenter.addObserver(self, selector: #selector(ViewController.handleKeyboardWillShowNotification(_:)), name: .UIKeyboardWillShow, object: nil)
        notificationCenter.addObserver(self, selector: #selector(ViewController.handleKeyboardWillHideNotification(_:)), name: .UIKeyboardWillHide, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: self.view.window)
	NotificationCenter.default.removeObserver(self, name: .UIKeyboardDidHide, object: self.view.window)
    }

    // キーボードが表示されるときに行う処理
    @objc func handleKeyboardWillShowNotification(_ notification: Notification) {
        let userInfo = notification.userInfo!
        frKeyboard = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
        drawBox()
    }

    // キーボードが非表示になるときに行う処理
    @objc func handleKeyboardWillHideNotification(_ notification: Notification) {
        box.frame = frBox
        tableView.frame.size = sizeTable
    }

    // メッセージBOX描画処理
    func drawBox() {
        let statusBarHeight = UIApplication.shared.statusBarFrame.height
        let bound: CGSize = UIScreen.main.bounds.size
        var height = msg.sizeThatFits(CGSize(width: msg.frame.size.width, height: CGFloat.greatestFiniteMagnitude)).height
        if height < MIN_HEIGHT_MSG {
            height = MIN_HEIGHT_MSG
        } else if MAX_HEIGHT_MSG < height {
            height = MAX_HEIGHT_MSG
        }
        // 入力フォームの位置調整
        msg.frame = CGRect(x: msg.frame.origin.x, y: msg.frame.origin.y, width: msg.frame.width, height: height)
        box.frame = CGRect(x: box.frame.origin.x, y: bound.height - frKeyboard.size.height - box.frame.height, width: msg.frame.width, height: height)

        // テーブルビューの高さ調整
        tableView.frame.size = CGSize(width: tableView.frame.width, height: bound.height - (statusBarHeight + frKeyboard.height + height))
    }

    func textViewDidChange(_ textView: UITextView) {
	drawBox()
    }

    func setView() {

        let statusBarHeight = UIApplication.shared.statusBarFrame.height
        let widthMax = view.frame.width
        let heightMax = view.frame.height

        // メッセージを表示する領域
        tableView = UITableView(frame: CGRect(x:0, y:statusBarHeight, width:widthMax, height:heightMax - (statusBarHeight + HEIGHT_BOX)))
        tableView.separatorStyle = .none
        view.addSubview(tableView)

        // 入力フォームと送信ボタンを表示する領域
        box = UIView(frame: CGRect(x: 0, y: tableView.frame.origin.y + tableView.frame.height, width: widthMax, height: HEIGHT_BOX))
	view.addSubview(box)

        // 送信ボタン
        submit = UIButton(frame: CGRect(x: widthMax-(WIDTH_SUBMIT+MARGIN_MSG), y: MARGIN_MSG, width: WIDTH_SUBMIT, height: HEIGHT_BOX-MARGIN_MSG))
        submit.setTitle("送信", for: UIControlState.normal)
        submit.setTitleColor(UIColor.white, for: UIControlState.normal)
        submit.backgroundColor = UIColor.black
	submit.layer.cornerRadius = 2.0
        submit.layer.borderColor = UIColor.lightGray.cgColor
        submit.layer.borderWidth = 3
        box.addSubview(submit)

        // 入力フォーム
	msg = UITextView(frame: CGRect(x: MARGIN_MSG, y: MARGIN_MSG, width: widthMax-(MARGIN_MSG*3+WIDTH_SUBMIT), height: HEIGHT_BOX-MARGIN_MSG))
        msg.font = UIFont.systemFont(ofSize: 18)
        msg.layer.borderColor = UIColor.lightGray.cgColor
        msg.layer.borderWidth = 3
        msg.delegate = self
        box.addSubview(msg)

        // キーボード完了ボタン
        let keyboard = UIView(frame: CGRect(x: 0, y: 0, width: widthMax, height: 40))
        keyboard.backgroundColor = UIColor.lightGray
        let button = UIButton(frame: CGRect(x: widthMax - 50, y: 5, width: 50, height: 30))
        button.setTitle("完了", for: .normal)
        button.layer.cornerRadius = 2.0
        keyboard.addSubview(button)
	msg.inputAccessoryView = keyboard
        button.addTarget(self, action: #selector(onClose(sender:)), for: .touchUpInside)

        // メッセージBOXの位置を保持しておく
	frBox = box.frame

        // テーブルビューのサイズを保持しておく
        sizeTable = tableView.frame.size
    }

    // 完了ボタンでキーボードを閉じる
    @objc func onClose(sender: UIBarButtonItem){
        msg.endEditing(true)
    }

}

ずらずら書いたけど以上です。ちなみにSwift4で確認した。