はじめに
UITextFieldやUITextViewで表示されるキーボード上のツールバーに、バー上のボタンをタップするともう1段バーが表示される方法になります。
イメージとしてはiOS純正のリマインダーアプリで実装されている、リマインド時間やロケールを選択するツールバーを同じものになります。
textView.inputAccessoryView = toolbar
でキーボード上にツールバーを設定する基本的な方法はよく見かけますが、
動的にバーの高さを変更する方法が見当たらなかったので参考になれば幸いです。
完成イメージ
ツールバー上のベルのボタンをタップすると、もう1段上に通知時間のインターバルを選択するボタンが表示されます。
実装方法
はじめはtextView.inputAccessoryView = toolbar
で設定したtoolbarの高さを動的に変更しようとしたのですが、なかなかうまくできず。。。
(実装方法わかる方いれば教えてください!)
今回はtextView.inputAccessoryView = toolbar
で設定したtoolbarに対して子viewをaddSubview
、sendSubviewToBack
で裏側に隠しておいて、
上下に移動することで表示を切り替えています。
今回はUITableViewにCustomTableViewCellを作成し、セル上にUITextViewを配置しています。
キーボード上にツールバーを表示するためにCustomTableViewCell内でUITextViewに対してキーボードが表示されるようにします。
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
上記で設定しているlowerToolbarの中身がこちらになります
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()
})
}
}
}
解説
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
}
LowerToolbarを初期化する際に、UpperToolbarを作成し、addSubview
してsendSubviewToBack
で背面に移動することで非表示にします。
@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()
})
}
}
ベルボタンをタップした際に、upperToolbarを上下に60移動しています。
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if isHidenUpperToolbar {
// upperToolbarが非表示で
// lowerToolbarがタップされた場合
let rect = self.bounds
return rect.contains(point)
} else {
// upperToolbarが表示されているが
// upperToolbarがタップされた場合
var rect = self.bounds
if rect.contains(point) {
return rect.contains(point)
}
// lowerToolbarがタップされた場合
rect.origin.y -= 60
return rect.contains(point)
}
}
ポイントなのがこの部分で、upperToolbarを60上に移動した場合、親viewのlowerToolbarのframe領域からはみ出してしまうため、
upperToolbarの上に乗っているボタンのイベントを受け取れなくなってしまいます。
そこで、override func point(inside point: CGPoint, with event: UIEvent?) -> Bool
をオーバーライドして
upperToolbarがタップされた場合にイベントが受け取れるようにしています。
その他気づき
今回はlowerToolbarの中でupperToolbarを生成してコードで作成しました。
これは、upperToolbarをstoryboardで個別に作成したものを使用したところ、
delegateを使用してタップされたイベントをupperToolbar->lowerToolbar->CustomTableViewCellへ移譲することができなかったためです。
(lowerToolbarでupperToolbarのdelegate設定できなかったです。)
まとめ
ツールバーの高さを動的に変更できれば一番シンプルな実装にはなるかと思いますが、
高さの変更が思ったようにできず、違う方法でトライしてみました。
アップルの純正リマインダーアプリではどうやって実装してるんですかね。気になります。
何か参考になれば嬉しいです。