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.
Xcode 12.3
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.
Since SafeArea is used for the placement of TextView, execute setupViews ()
at the timing of viewDidLayoutSubviews
to place it.
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)
}
}
Toolbars and buttons to add
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)
}
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.
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
})
}
}
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.
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)
}
}
Positive comments are welcome