[SWIFT] Create UITextView with code, add edit end button and prevent keyboard cover

Introduction

Since UITextView cannot close the keyboard by returning the keyboard, you have to close the keyboard from some other UI. (Add an input completion button to the keyboard, tap other than TextView, etc.) Also, in reality, I think that it is often necessary to take measures against the cover of the keyboard and TextView, so I wrote an article as a memorandum based on that.

environment

Xcode 12.3

Creating a UITextView

Create a TextView with code at the bottom of the screen so that it covers the keyboard on purpose. The background color is Cyan, but the border is similar to the TextField's roundRect.

スクリーンショット 2020-12-31 16.02.03.png

Since SafeArea is used for the placement of TextView, execute setupViews () at the timing of viewDidLayoutSubviews to place it.

code

ViewController.swift


import UIKit

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

    //textView settings
    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)
    
  }

  //Called when the layout of the subView is completed (view bounds are confirmed)
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    //Execution of view placement method
    setupViews()
    
  }
  
  
  func setupViews(){
    
    //Obtaining a safe area
    let safeArea = view.safeAreaInsets

    //Size of UI part placement area
    let partsArea_W = UIScreen.main.bounds.width - safeArea.left - safeArea.right
    let partsArea_H = UIScreen.main.bounds.height - safeArea.top - safeArea.bottom
    
    //Spacing between UI parts
    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)
    
  }
  

Add delegate method with extension

ViewController.swift


//MARK: - UITextViewDelegate

extension ViewController : UITextViewDelegate{
  
  //Called just before the start of editing
  func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
    //Allow editing to start by returning true
    return true
  }
  
  //Called immediately after editing starts
  func textViewDidBeginEditing(_ textView: UITextView) {
    
  }
  
  //Called just before the end of editing
  func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
    //Allow editing to end by returning true
    return true
  }
  
  //Called immediately after editing
  func textViewDidEndEditing(_ textView: UITextView) {
    //Extracting text from textView
    guard let text = textView.text else { return }
    print(text)
  }
}

Create an edit end button with UIToolbar and add it to your keyboard

Toolbars and buttons to add スクリーンショット 2020-12-31 16.21.13.png

Set the UIToolBar with the button in the InputAccessoryView of the TextView

ViewController.swift



  override func viewDidLoad() {
    super.viewDidLoad()

    //Create a toolbar to put the edit end button on the keyboard
    let kbToolbar = UIToolbar()
    kbToolbar.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 40)
    //Spacer to align the button to the right
    let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
    //Edit end button
    let kbDoneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(kbDoneTaped))
    //Add spacers and buttons to the toolbar
    kbToolbar.items = [spacer,kbDoneButton]
    
    
    //Set the created toolbar in the inputAccessoryView of the textView keyboard
    textView.inputAccessoryView = kbToolbar

    //~ ~ Omitted ~ ~
    
  }


  //Method executed when Done on keyboard is pressed
  @objc func kbDoneTaped(sender:UIButton){
    
    //Close keyboard
    view.endEditing(true)
    
  }

Screen shift (measures against keyboard cover)

Identify the activated TextView, shift the View on which the TextView is mounted by the amount that the keyboard overlaps, and return the screen when the keyboard closes.

スクリーンショット 2020-12-31 16.27.01.png

In viewDidAppear, set to receive notifications from Notification Center by opening and closing the keyboard.

ViewController.swift



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

    //Setting to receive notification and execute method according to the keyboard display
    notification.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
    //Settings to receive notifications and execute methods as the keyboard disappears
    notification.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    
  }

Method executed when notification is received

ViewController.swift



  //Keyboard screen shift When the keyboard appears, shift the entire view
  @objc func keyboardWillShow(notification: Notification?){
    
    //Identify and store the active View
    var activeView:UIView?
    
    for sub in view.subviews {
      if sub.isFirstResponder {
        activeView = sub
      }
    }
    
    //Check the height of the keyboard
    let userInfo = notification?.userInfo!
    let keyboardScreenEndFrame = (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
    
    //Find the position of the bottom edge of the active View
    guard let actView = activeView else { return }
    
    //Calculate the bottom edge of the active View
    let txtLimit = actView.frame.origin.y + actView.frame.height
    //Calculate the top edge of the keyboard
    let kbLimit = view.bounds.height - keyboardScreenEndFrame.size.height
    
    print("Bottom of view=",txtLimit,"Top of keyboard=",kbLimit)
    //Calculate the offset amount(Margin 10)
    let offset = kbLimit - (txtLimit + 10)
    
    //If the keyboard does not cover, exit the method and do not offset
    if offset > 0 {
      print("Does not cover the keyboard")
      return
      
    }
    
    print("Screen offset amount\(offset)")
    
    //Get time to animate
    let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
    UIView.animate(withDuration: duration!, animations: { () in
      
      //Set the amount of screen shift
      let transform = CGAffineTransform(translationX: 0, y: offset)
      //Animation execution that shifts the view
      self.view.transform = transform
    })
  }
  
  
  //Keyboard screen shift Return the screen when the keyboard disappears
  @objc func keyboardWillHide(notification: Notification?) {
    
    //Get time to animate
    let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Double
    UIView.animate(withDuration: duration!, animations: { () in
      //Run undo animation
      self.view.transform = CGAffineTransform.identity
    })
  }
}

Bug here

I tried to shift the view itself on which the TextView is mounted, but since the SafeArea becomes zero at the time of shifting, there was a problem that the SafeArea was shifted to the position where it was not added. So, I felt it was forcible, but I tried to acquire SafeArea only at the first startup (when the variable is nil) and not update it after that.


  var safeArea:UIEdgeInsets!

  func setupViews(){
    
    //Obtaining a safe area(Obtained only when nil for the first time * Because Safe Area becomes zero when the screen is shifted)
    if safeArea == nil{
      safeArea = view.safeAreaInsets
    }

    //~~abridgement~~

  }

However, this method seems to have some inconveniences such as not being able to handle screen rotation. It may be better to put it on the UIView for shifting and then shift the UIView.

All chords

This is all the code, I think it works by copying.

ViewController.swift



import UIKit

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

    //Create a toolbar to put the edit end button on the keyboard
    let kbToolbar = UIToolbar()
    kbToolbar.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 40)
    //Spacer to align the button to the right
    let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
    //Edit end button
    let kbDoneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(kbDoneTaped))
    //Add spacers and buttons to the toolbar
    kbToolbar.items = [spacer,kbDoneButton]
    
    
    //textView settings
    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)
    
    
  }
  

  //Called when the layout of the subView is completed (view bounds are confirmed)
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    //Execution of view placement method
    setupViews()
    
  }
  
  
  //Called immediately after view is displayed on the screen(Called multiple times, such as when returning to the background or switching tabs)
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    
    let notification = NotificationCenter.default
    //Setting to receive notification and execute method according to the keyboard display
    notification.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
    //Settings to receive notifications and execute methods as the keyboard disappears
    notification.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    
  }
  
  
  
  
  func setupViews(){
    
    //Obtaining a safe area(Obtained only when nil for the first time * Because Safe Area becomes zero when the screen is shifted)
    if safeArea == nil{
      safeArea = view.safeAreaInsets
    }

    //Size of UI part placement area
    let partsArea_W = UIScreen.main.bounds.width - safeArea.left - safeArea.right
    let partsArea_H = UIScreen.main.bounds.height - safeArea.top - safeArea.bottom
    
    //Spacing between UI parts
    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)
    
  }
  
  //Method executed when Done on keyboard is pressed
  @objc func kbDoneTaped(sender:UIButton){
    
    //Close keyboard
    view.endEditing(true)
    
  }
  
  
  //Keyboard screen shift When the keyboard appears, shift the entire view
  @objc func keyboardWillShow(notification: Notification?){
    
    //Identify and store the active View
    var activeView:UIView?
    
    for sub in view.subviews {
      if sub.isFirstResponder {
        activeView = sub
      }
    }
    
    //Check the height of the keyboard
    let userInfo = notification?.userInfo!
    let keyboardScreenEndFrame = (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
    
    //Find the position of the bottom edge of the active View
    guard let actView = activeView else { return }
    
    //Calculate the bottom edge of the active View
    let txtLimit = actView.frame.origin.y + actView.frame.height
    //Calculate the top edge of the keyboard
    let kbLimit = view.bounds.height - keyboardScreenEndFrame.size.height
    
    print("Bottom of view=",txtLimit,"Top of keyboard=",kbLimit)
    //Calculate the offset amount(Margin 10)
    let offset = kbLimit - (txtLimit + 10)
    
    //If the keyboard does not cover, exit the method and do not offset
    if offset > 0 {
      print("Does not cover the keyboard")
      return
      
    }
    
    print("Screen offset amount\(offset)")
    
    //Get time to animate
    let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
    UIView.animate(withDuration: duration!, animations: { () in
      
      //Set the amount of screen shift
      let transform = CGAffineTransform(translationX: 0, y: offset)
      //Animation execution that shifts the view
      self.view.transform = transform
    })
  }
  
  
  //Keyboard screen shift Return the screen when the keyboard disappears
  @objc func keyboardWillHide(notification: Notification?) {
    
    //Get time to animate
    let duration: TimeInterval? = notification?.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Double
    UIView.animate(withDuration: duration!, animations: { () in
      //Run undo animation
      self.view.transform = CGAffineTransform.identity
    })
  }
}




//MARK: - UITextViewDelegate

extension ViewController : UITextViewDelegate{
  
  //Called just before the start of editing
  func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
    //Allow editing to start by returning true
    return true
  }
  
  //Called immediately after editing starts
  func textViewDidBeginEditing(_ textView: UITextView) {
    
  }
  
  //Called just before the end of editing
  func textViewShouldEndEditing(_ textView: UITextView) -> Bool {
    //Allow editing to end by returning true
    return true
  }
  
  //Called immediately after editing
  func textViewDidEndEditing(_ textView: UITextView) {
    //Extracting text from textView
    guard let text = textView.text else { return }
    print(text)
  }
}

Finally

Positive comments are welcome

Recommended Posts

Create UITextView with code, add edit end button and prevent keyboard cover
Create table and add columns