[Swift] How to dynamically change the height of the toolbar on the keyboard

Introduction

If you tap the button on the bar on the toolbar on the keyboard displayed by UITextField or UITextView, another bar will be displayed. As an image, the toolbar for selecting the reminder time and locale, which is implemented in the genuine iOS reminder app, will be the same. I often see the basic way to set a toolbar on the keyboard with textView.inputAccessoryView = toolbar, I couldn't find a way to change the height of the bar dynamically, so I hope it helps.

Complete image

When you tap the bell button on the toolbar, a button for selecting the notification time interval will appear one step higher. RocketSim Recording - iPhone 12 Pro Max - 2020-11-13 16.39.07.gif

Implementation method

At first, I tried to dynamically change the height of the toolbar set with textView.inputAccessoryView = toolbar, but I couldn't do it well. .. .. (Please let me know if you know how to implement it!) This time, for the toolbar set with textView.inputAccessoryView = toolbar, hide the child view with addSubview and sendSubviewToBack on the back side. The display is switched by moving up and down.

This time, I created a CustomTableViewCell in the UITableView and placed the UITextView on the cell. Makes the keyboard visible to the UITextView in the CustomTableViewCell to display the toolbar on the keyboard.

CustomTableViewCell.swift


let lowerToolbar = LowerToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
lowerToolbar.initToolbarButton(item: item!)
lowerToolbar.delegate = self
textView.inputAccessoryView = lowerToolbar

Here is the contents of the lowerToolbar set above

LowerToolbar.swift


protocol LowerToolbarDelegete: class {
    func onTouchToolbarButton(selectedTimeInterval: Int)
}

class LowerToolbar: UIView {
    @IBOutlet weak var toolbar: UIToolbar!
    var upperToolbar: UIView?
    var isHidenUpperToolbar: Bool = true
    var upperToolbarCenterY: CGFloat = 0
    weak var delegate: LowerToolbarDelegete! = nil
    let oneHourButton = UIButton(type: .system)
    let threeHourButton = UIButton(type: .system)
    let fiveHourButton = UIButton(type: .system)
    var item: ItemModel?
    var selectedTimeInterval: Int = 0 // Last tapped button.
    
    override init(frame: CGRect){
        super.init(frame: frame)
        loadNib()
    }
    
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }
    
    func loadNib(){
        let view = Bundle.main.loadNibNamed("LowerToolbar", owner: self, options: nil)?.first as! UIView
        view.frame = self.bounds
        self.addSubview(view)
        toolbar.clipsToBounds = true
        
        upperToolbar = UIView(frame: CGRect(x: 0, y: -1, width: self.frame.size.width, height: 61))
        upperToolbar?.backgroundColor = UIColor.toolbar
        self.addSubview(upperToolbar!)
        self.sendSubviewToBack(upperToolbar!)
        upperToolbarCenterY = upperToolbar!.center.y
    }
    
    func createToolbarButton(btn: UIButton, title: String, timeInterval: Int) {
        btn.setTitle(title, for: .normal)
        btn.tag = timeInterval
        btn.layer.borderWidth = 1
        btn.layer.borderColor = UIColor.toolbarBorder.cgColor
        btn.layer.cornerRadius = 20
        btn.contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
        btn.backgroundColor = UIColor.toolbarButton
        btn.addTarget(self, action: #selector(tapHoursButton), for: .touchUpInside)
        upperToolbar?.addSubview(btn)
        
        if self.item?.timeInterval == timeInterval {
            btn.layer.borderColor = UIColor.systemBlue.cgColor
            btn.backgroundColor = UIColor.systemBlue
            btn.tintColor = UIColor.white
        }
    }
    
    func initToolbarButton(item: ItemModel) {
        self.item = item

        createToolbarButton(btn: oneHourButton, title: "1hour", timeInterval: 60)
        oneHourButton.translatesAutoresizingMaskIntoConstraints = false
        oneHourButton.leadingAnchor.constraint(equalTo: upperToolbar!.leadingAnchor, constant: 20).isActive = true
        oneHourButton.topAnchor.constraint(equalTo: upperToolbar!.topAnchor, constant: 10).isActive = true
        
        createToolbarButton(btn: threeHourButton, title: "3hour", timeInterval: 180)
        threeHourButton.translatesAutoresizingMaskIntoConstraints = false
        threeHourButton.leadingAnchor.constraint(equalTo: oneHourButton.trailingAnchor, constant: 20).isActive = true
        threeHourButton.topAnchor.constraint(equalTo: upperToolbar!.topAnchor, constant: 10).isActive = true
        
        createToolbarButton(btn: fiveHourButton, title: "5hour", timeInterval: 400)
        fiveHourButton.translatesAutoresizingMaskIntoConstraints = false
        fiveHourButton.leadingAnchor.constraint(equalTo: threeHourButton.trailingAnchor, constant: 20).isActive = true
        fiveHourButton.topAnchor.constraint(equalTo: upperToolbar!.topAnchor, constant: 10).isActive = true
    }
    
    func initUpperToolbar() {
        upperToolbar?.center.y = upperToolbarCenterY
        isHidenUpperToolbar = true
    }
    
    func decorateTappedHourButton(btn: UIButton) {
        btn.layer.borderColor = UIColor.systemBlue.cgColor
        btn.backgroundColor = UIColor.systemBlue
        btn.tintColor = UIColor.white
    }
    
    func decorateNormalHourButton(btn: UIButton) {
        btn.layer.borderColor = UIColor.toolbarBorder.cgColor
        btn.backgroundColor = UIColor.toolbarButton
        btn.tintColor = .systemBlue
    }
    
    func decorateHourButton(btn1: UIButton, btn2: UIButton, btn3: UIButton, newTimeInterval: Int) {
        if selectedTimeInterval == newTimeInterval {
            selectedTimeInterval = 0
            decorateNormalHourButton(btn: btn1)
        } else {
            selectedTimeInterval = newTimeInterval
            decorateTappedHourButton(btn: btn1)
        }
        decorateNormalHourButton(btn: btn2)
        decorateNormalHourButton(btn: btn3)
    }
    
    @objc func tapHoursButton(btn: UIButton) {
        switch btn.tag {
        case 60:
            decorateHourButton(btn1: oneHourButton, btn2: threeHourButton, btn3: fiveHourButton, newTimeInterval: btn.tag)
        case 180:
            decorateHourButton(btn1: threeHourButton, btn2: oneHourButton, btn3: fiveHourButton, newTimeInterval: btn.tag)
        case 400:
            decorateHourButton(btn1: fiveHourButton, btn2: oneHourButton, btn3: threeHourButton, newTimeInterval: btn.tag)
        default:
            print("no item")
        }
        delegate?.onTouchToolbarButton(selectedTimeInterval: btn.tag)
    }
    
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if isHidenUpperToolbar {
            let rect = self.bounds
            return rect.contains(point)
        } else {
            var rect = self.bounds
            if rect.contains(point) {
                return rect.contains(point)
            }
            
            rect.origin.y -= 60
            return rect.contains(point)
        }
    }
    
    @IBAction func tapBellButton(_ sender: Any) {
        if isHidenUpperToolbar {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y -= 60
                self.isHidenUpperToolbar = false
                self.layoutIfNeeded()
            })
        } else {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y += 60
                self.isHidenUpperToolbar = true
                self.layoutIfNeeded()
            })
        }
    }
}

Commentary

LowerToolbar.swift


    func loadNib(){
        let view = Bundle.main.loadNibNamed("LowerToolbar", owner: self, options: nil)?.first as! UIView
        view.frame = self.bounds
        self.addSubview(view)
        toolbar.clipsToBounds = true
        
        upperToolbar = UIView(frame: CGRect(x: 0, y: -1, width: self.frame.size.width, height: 61))
        upperToolbar?.backgroundColor = UIColor.toolbar
        self.addSubview(upperToolbar!)
        self.sendSubviewToBack(upperToolbar!)
        upperToolbarCenterY = upperToolbar!.center.y
    }

When initializing the LowerToolbar, create an UpperToolbar and hide it by addSubview and sendSubviewToBack to move it to the back.

LowerToolbar.swift


    @IBAction func tapBellButton(_ sender: Any) {
        if isHidenUpperToolbar {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y -= 60
                self.isHidenUpperToolbar = false
                self.layoutIfNeeded()
            })
        } else {
            UIView.animate(withDuration: 0.1, animations: {
                self.upperToolbar!.center.y += 60
                self.isHidenUpperToolbar = true
                self.layoutIfNeeded()
            })
        }
    }

When you tap the bell button, the upper Toolbar is moved up and down by 60.

LowerToolbar.swift


    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if isHidenUpperToolbar {
            //upperToolbar is hidden
            //When lowerToolbar is tapped
            let rect = self.bounds
            return rect.contains(point)
        } else {
            //Although the upper Toolbar is displayed
            //When upperToolbar is tapped
            var rect = self.bounds
            if rect.contains(point) {
                return rect.contains(point)
            }
            
            //When lowerToolbar is tapped
            rect.origin.y -= 60
            return rect.contains(point)
        }
    }

The point is this part, because if you move the upperToolbar up to 60, it will extend beyond the frame area of the lowerToolbar of the parent view. I will not be able to receive the event of the button on the upperToolbar. So, override override func point (inside point: CGPoint, with event: UIEvent?)-> Bool I am trying to receive an event when the upperToolbar is tapped.

Other notices

This time, I generated an upperToolbar in the lowerToolbar and created it with code. This is when I used the upperToolbar created individually in the storyboard, This is because the event tapped using delegate could not be delegated to upperToolbar-> lowerToolbar-> CustomTableViewCell. (I couldn't set delegate for upperToolbar with lowerToolbar.)

Summary

I think it would be the simplest implementation if the height of the toolbar could be changed dynamically. I couldn't change the height as I expected, so I tried a different method. How do you implement it in Apple's genuine reminder app? I'm curious. I hope you find it helpful.

Recommended Posts

[Swift] How to dynamically change the height of the toolbar on the keyboard
How to change the timezone on Ubuntu
[Swift 5] Processing to close the keyboard on UITableView
[swift5] How to change the color of TabBar or the color of item of TabBar with code
[Ruby on Rails] How to change the column name
[Swift] How to change the order of Bar Items in Tab Bar Controller [Beginner]
[Rails] How to change the column name of the table
[Swift] How to get the document ID of Firebase
How to dynamically change the column name acquired by MyBatis
How to change the setting value of Springboot Hikari CP
How to change the contents of the jar file without decompressing
How to change BackgroundColor etc. of NavigationBar in Swift UI
How to check the database of apps deployed on Heroku
[Swift] Change the textColor of UIDatePicker
[Swift5] How to get an array and the complement of arrays
How to display 0 on the left side of the standard input value
[Rails / Heroku / MySQL] How to reset the DB of Rails application on Heroku
[Swift UI] How to get the startup status of the application [iOS]
[Rails] How to change the page title of the browser for each page
[chown] How to change the owner of a file or directory
How to determine the number of parallels
[Swift] How to implement the countdown function
[Swift] Get the height of Safe Area
Ransack sort_link How to change the color!
How to sort the List of SelectItem
[Swift] Change the color of SCN Node
How to change the maximum and maximum number of POST data in Spark
How to solve the local environment construction of Ruby on Rails (MAC)!
How to change the value of a variable at a breakpoint in intelliJ
[Ruby On Rails] How to search the contents of params using include?
Customize how to divide the contents of Recyclerview
[Swift] How to implement the LINE login function
[swift5] How to implement the Twitter share function
How to add sound in the app (swift)
[Swift] How to link the app with Firebase
How to get today's day of the week
Change the timezone of the https-portal container to JST
Output of how to use the slice method
[Swift UI] How to disable ScrollsToTop of ScrollView
[Swift] How to implement the fade-in / out function
How Microservices Change the Way of Developing Applications
How to display the result of form input
http: // localhost: How to change the port number
[Java] Memo on how to write the source
[Java] How to get the authority of the folder
[Swift] How to get the number of elements in an array (super basic)
[swift] How to control the behavior when the back button of NavigationBar is pressed
How to get the ID of a user authenticated with Firebase in Swift
[Ruby on Rails] Rails tutorial Chapter 14 Summary of how to implement the status feed
[Ruby on Rails] How to Japaneseize the error message of Form object (ActiveModel)
[Java] How to get the URL of the transition source
How to delete / update the list field of OneToMany
How to write Scala from the perspective of Java
[Ruby] How to find the sum of each digit
How to install the root certificate of Centos7 (Cybertrust)
[Java] How to get the maximum value of HashMap
[SwiftUI] How to specify the abbreviated position of Text
How to change the process depending on the list pressed when there are multiple ListViews
As of April 2018 How to get Java 8 on Mac
[Android] How to get the setting language of the terminal
[Rails] How to get the contents of strong parameters