##はじめに
UITextViewはキーボードのReturnによってキーボードを閉じる処理ができないため、何らかの別UIなどからキーボード閉じる操作をしなければなりません。(キーボードに入力完了ボタンを追加したり、TextView以外をタップするなど)
また、実際にはキーボードとTextViewの被りの対策も必要になることが多いと思いますので、その辺りも踏まえ忘備録として記事にしました。
##環境
Xcode 12.3
##UITextViewの作成
わざとキーボードと被る様に画面下の方にコードでTextViewを作成します。
背景色はCyanですが、枠線はTextFieldのroundRectに似せてみました。
TextViewの配置にはSafeAreaを利用するためviewDidLayoutSubviews
のタイミングでsetupViews()
を実行して配置
###コード
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で追加
//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で作成してキーボードに追加
TextViewのinputAccessoryViewにボタンを配置したUIToolBarを設定します
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をずらし、キーボードが閉じたら画面を戻します。
viewDidAppearでNotificationCenterからキーボードの開閉で通知を受け取る設定をします。
※iOS9以前は画面から抜ける際に通知解除していましたが以降は不要になりました。
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)
}
通知を受け取った時に実行されるメソッド
//キーボード画面ずらし キーボードが現れた時に、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の時)のみ実行して、その後更新しない様にしてみました。
var safeArea:UIEdgeInsets!
func setupViews(){
//safeAreaの取得(初回nilの時のみ取得※画面をずらすとSafeAreaがゼロになるため)
if safeArea == nil{
safeArea = view.safeAreaInsets
}
// 〜〜省略〜〜
}
ただ、この方法だと画面回転に対応できないなど不便な面も残っていると思われるため
ずらし用のUIViewに載せてからそのUIViewをずらすなどにした方が良いかもしれません。
##全コード
今回の全コードです、コピペで動くと思います。
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)
}
}
##最後に
前向きなコメント大歓迎です