UITextViewをコードで作成し、編集終了ボタンの追加とキーボード被り対策

##はじめに
UITextViewはキーボードのReturnによってキーボードを閉じる処理ができないため、何らかの別UIなどからキーボード閉じる操作をしなければなりません。（キーボードに入力完了ボタンを追加したり、TextView以外をタップするなど）
また、実際にはキーボードとTextViewの被りの対策も必要になることが多いと思いますので、その辺りも踏まえ忘備録として記事にしました。

##環境
Xcode 12.3

##UITextViewの作成

わざとキーボードと被る様に画面下の方にコードでTextViewを作成します。
背景色はCyanですが、枠線はTextFieldのroundRectに似せてみました。

![スクリーンショット 2020-12-31 16.02.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918084/6fc9a6e6-1803-6d45-3629-bfad3a0c35f3.png)


TextViewの配置にはSafeAreaを利用するため`viewDidLayoutSubviews`のタイミングで`setupViews()`を実行して配置


###コード
```ViewController.swift
import UIKit

class ViewController: UIViewController {
  
  let textView = UITextView()
  
  override func viewDidLoad() {
    super.viewDidLoad()

    //textViewの設定
    textView.delegate = self
    textView.keyboardType = .default
    textView.layer.cornerRadius = 5
    textView.layer.borderWidth = 1
    textView.layer.borderColor = UIColor(white: 0.9, alpha: 1).cgColor
    textView.backgroundColor = .cyan
    textView.text = "TextView"
    view.addSubview(textView)
    
  }

  //subViewのレイアウトが完了した際に呼ばれる（viewのboundsが確定される）
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    //viewの配置メソッドの実行
    setupViews()
    
  }
  
  
  func setupViews(){
    
    //safeAreaの取得
    let safeArea = view.safeAreaInsets

    //UIパーツの配置エリアの大きさ
    let partsArea_W = UIScreen.main.bounds.width - safeArea.left - safeArea.right
    let partsArea_H = UIScreen.main.bounds.height - safeArea.top - safeArea.bottom
    
    //UIパーツ間の間隔
    let margin_X = round(partsArea_W * 0.05)
    let margin_Y = round(partsArea_H * 0.05)
    
    let textView_W = partsArea_W - margin_X * 2
    let textView_H = round(partsArea_H * 0.5)
    let textView_X = margin_X // safeArea.left + margin_X
    let textView_Y = UIScreen.main.bounds.height - textView_H - safeArea.bottom - margin_Y
    textView.frame.size = CGSize(width: textView_W, height: textView_H)
    textView.frame.origin = CGPoint(x: textView_X, y: textView_Y)
    
  }
  
```

delegateメソッドをextensionで追加

```ViewController.swift
//MARK: - UITextViewDelegate

extension ViewController : UITextViewDelegate{
  
  //編集開始直前に呼ばれる
  func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
    //trueを返すことにより編集開始を許可する
    return true
  }
  
  //編集開始直後に呼ばれる
  func textViewDidBeginEditing(_ textView: UITextView) {
    
  }
  
  //編集終了直前に呼ばれる
  func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
    //trueを返すことにより編集終了を許可する
    return true
  }
  
  //編集終了直後に呼ばれる
  func textViewDidEndEditing(_ textView: UITextView) {
    //textViewからテキストの取り出し
    guard let text = textView.text else { return }
    print(text)
  }
}
```

##編集終了ボタンをUIToolbarで作成してキーボードに追加

追加するツールバーとボタン
![スクリーンショット 2020-12-31 16.21.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918084/bc6af9e7-7fce-7ad4-3386-c30c032315fc.png)


TextViewのinputAccessoryViewにボタンを配置したUIToolBarを設定します

```ViewController.swift

  override func viewDidLoad() {
    super.viewDidLoad()

    //キーボードに編集終了ボタンを乗せるツールバーを作成
    let kbToolbar = UIToolbar()
    kbToolbar.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 40)
    //ボタンを右寄せにするためのスペーサー
    let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
    //編集終了ボタン
    let kbDoneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(kbDoneTaped))
    //スペーサーとボタンをツールバーに追加
    kbToolbar.items = [spacer,kbDoneButton]
    
    
    //textViewのキーボードのinputAccessoryViewに作成したツールバーを設定
    textView.inputAccessoryView = kbToolbar

    //〜〜　省略　〜〜
    
  }


  //keyboardのDoneが押された時に実行されるメソッド
  @objc func kbDoneTaped(sender:UIButton){
    
    //キーボードを閉じる
    view.endEditing(true)
    
  }
```

##画面ずらし（キーボード被り対策）

アクティブになったTextViewを特定してキーボードが重なる量だけTextViewが乗っているViewをずらし、キーボードが閉じたら画面を戻します。

![スクリーンショット 2020-12-31 16.27.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918084/298d4f43-0116-d681-72cb-f876627758ce.png)

viewDidAppearでNotificationCenterからキーボードの開閉で通知を受け取る設定をします。
※iOS9以前は画面から抜ける際に通知解除していましたが以降は不要になりました。

```ViewController.swift

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    let notification = NotificationCenter.default

    //キーボードの表示に合わせて通知を受け取りメソッドを実行する設定
    notification.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
    //キーボードが消えるのに合わせて通知を受け取りメソッドを実行する設定
    notification.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    
  }

```

通知を受け取った時に実行されるメソッド

```ViewController.swift

  //キーボード画面ずらし　キーボードが現れた時に、view全体をずらす
  @objc func keyboardWillShow(notification: Notification?){
    
    //アクティブになっているViewを特定して格納
    var activeView:UIView?
    
    for sub in view.subviews {
      if sub.isFirstResponder {
        activeView = sub
      }
    }
    
    //キーボードの高さを調べる
    let userInfo = notification?.userInfo!
    let keyboardScreenEndFrame = (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
    
    //アクティブなViewの下辺の位置を調べる
    guard let actView = activeView else { return }
    
    //アクティブなViewの下端を計算
    let txtLimit = actView.frame.origin.y + actView.frame.height
    //キーボードの上端を計算
    let kbLimit = view.bounds.height - keyboardScreenEndFrame.size.height
    
    print("viewの下端=",txtLimit,"キーボードの上端=",kbLimit)
    //オフセット量を計算(マージン 10)
    let offset = kbLimit - (txtLimit + 10)
    
    //キーボードが被らない場合はメソッドから抜けてオフセットしない
    if offset > 0 {
      print("キーボードは被らない")
      return
      
    }
    
    print("画面のオフセット量\(offset)")
    
    //アニメーションさせる時間を取得
    let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
    UIView.animate(withDuration: duration!, animations: { () in
      
      //画面のずらし量を設定
      let transform = CGAffineTransform(translationX: 0, y: offset)
      //viewをずらすアニメーション実行
      self.view.transform = transform
    })
  }
  
  
  //キーボード画面ずらし　キーボードが消えた時に、画面を戻す
  @objc func keyboardWillHide(notification: Notification?) {
    
    //アニメーションさせる時間を取得
    let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Double
    UIView.animate(withDuration: duration!, animations: { () in
      //元に戻すアニメーションの実行
      self.view.transform = CGAffineTransform.identity
    })
  }
}

```
###ここで不具合
TextViewが乗っているview自体をずらす様にしましたが、ずらした時点でSafeAreaがゼロになってしまう為、SafeAreaが加味されていない位置にずれてしまう不具合が発生しました。
そこで、強引な感じもしましたが、SafeAreaの取得を、初回起動時（変数がnilの時）のみ実行して、その後更新しない様にしてみました。

```swift

  var safeArea:UIEdgeInsets!

  func setupViews(){
    
    //safeAreaの取得(初回nilの時のみ取得※画面をずらすとSafeAreaがゼロになるため）
    if safeArea == nil{
      safeArea = view.safeAreaInsets
    }

    // 〜〜省略〜〜 

  }

```

ただ、この方法だと画面回転に対応できないなど不便な面も残っていると思われるため
ずらし用のUIViewに載せてからそのUIViewをずらすなどにした方が良いかもしれません。

##全コード

今回の全コードです、コピペで動くと思います。

```ViewController.swift

import UIKit

class ViewController: UIViewController {
  
  let textView = UITextView()
  
  var safeArea:UIEdgeInsets!
  
  override func viewDidLoad() {
    super.viewDidLoad()

    //キーボードに編集終了ボタンを乗せるツールバーを作成
    let kbToolbar = UIToolbar()
    kbToolbar.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 40)
    //ボタンを右寄せにするためのスペーサー
    let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
    //編集終了ボタン
    let kbDoneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(kbDoneTaped))
    //スペーサーとボタンをツールバーに追加
    kbToolbar.items = [spacer,kbDoneButton]
    
    
    //textViewの設定
    textView.delegate = self
    textView.keyboardType = .default
    textView.inputAccessoryView = kbToolbar
    textView.layer.cornerRadius = 5
    textView.layer.borderWidth = 1
    textView.layer.borderColor = UIColor(white: 0.9, alpha: 1).cgColor
    textView.backgroundColor = .cyan
    textView.text = "TextView"
    view.addSubview(textView)
    
    
  }
  

  //subViewのレイアウトが完了した際に呼ばれる（viewのboundsが確定される）
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    //viewの配置メソッドの実行
    setupViews()
    
  }
  
  
  //viewが画面に表示された直後に呼ばれる(バックグラウンド復帰時やタブ切り替え時など複数回呼ばれる)
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    let notification = NotificationCenter.default
    //キーボードの表示に合わせて通知を受け取りメソッドを実行する設定
    notification.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
    //キーボードが消えるのに合わせて通知を受け取りメソッドを実行する設定
    notification.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    
  }
  
  
  
  
  func setupViews(){
    
    //safeAreaの取得(初回nilの時のみ取得※画面をずらすとSafeAreaがゼロになるため）
    if safeArea == nil{
      safeArea = view.safeAreaInsets
    }

    //UIパーツの配置エリアの大きさ
    let partsArea_W = UIScreen.main.bounds.width - safeArea.left - safeArea.right
    let partsArea_H = UIScreen.main.bounds.height - safeArea.top - safeArea.bottom
    
    //UIパーツ間の間隔
    let margin_X = round(partsArea_W * 0.05)
    let margin_Y = round(partsArea_H * 0.05)
    
    let textView_W = partsArea_W - margin_X * 2
    let textView_H = round(partsArea_H * 0.5)
    let textView_X = margin_X // safeArea.left + margin_X
    let textView_Y = UIScreen.main.bounds.height - textView_H - safeArea.bottom - margin_Y
    textView.frame.size = CGSize(width: textView_W, height: textView_H)
    textView.frame.origin = CGPoint(x: textView_X, y: textView_Y)
    
  }
  
  //keyboardのDoneが押された時に実行されるメソッド
  @objc func kbDoneTaped(sender:UIButton){
    
    //キーボードを閉じる
    view.endEditing(true)
    
  }
  
  
  //キーボード画面ずらし　キーボードが現れた時に、view全体をずらす
  @objc func keyboardWillShow(notification: Notification?){
    
    //アクティブになっているViewを特定して格納
    var activeView:UIView?
    
    for sub in view.subviews {
      if sub.isFirstResponder {
        activeView = sub
      }
    }
    
    //キーボードの高さを調べる
    let userInfo = notification?.userInfo!
    let keyboardScreenEndFrame = (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
    
    //アクティブなViewの下辺の位置を調べる
    guard let actView = activeView else { return }
    
    //アクティブなViewの下端を計算
    let txtLimit = actView.frame.origin.y + actView.frame.height
    //キーボードの上端を計算
    let kbLimit = view.bounds.height - keyboardScreenEndFrame.size.height
    
    print("viewの下端=",txtLimit,"キーボードの上端=",kbLimit)
    //オフセット量を計算(マージン 10)
    let offset = kbLimit - (txtLimit + 10)
    
    //キーボードが被らない場合はメソッドから抜けてオフセットしない
    if offset > 0 {
      print("キーボードは被らない")
      return
      
    }
    
    print("画面のオフセット量\(offset)")
    
    //アニメーションさせる時間を取得
    let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
    UIView.animate(withDuration: duration!, animations: { () in
      
      //画面のずらし量を設定
      let transform = CGAffineTransform(translationX: 0, y: offset)
      //viewをずらすアニメーション実行
      self.view.transform = transform
    })
  }
  
  
  //キーボード画面ずらし　キーボードが消えた時に、画面を戻す
  @objc func keyboardWillHide(notification: Notification?) {
    
    //アニメーションさせる時間を取得
    let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Double
    UIView.animate(withDuration: duration!, animations: { () in
      //元に戻すアニメーションの実行
      self.view.transform = CGAffineTransform.identity
    })
  }
}




//MARK: - UITextViewDelegate

extension ViewController : UITextViewDelegate{
  
  //編集開始直前に呼ばれる
  func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
    //trueを返すことにより編集開始を許可する
    return true
  }
  
  //編集開始直後に呼ばれる
  func textViewDidBeginEditing(_ textView: UITextView) {
    
  }
  
  //編集終了直前に呼ばれる
  func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
    //trueを返すことにより編集終了を許可する
    return true
  }
  
  //編集終了直後に呼ばれる
  func textViewDidEndEditing(_ textView: UITextView) {
    //textViewからテキストの取り出し
    guard let text = textView.text else { return }
    print(text)
  }
}
```

##最後に

前向きなコメント大歓迎です