おはようございます,モノトーンです。
今日は,検索してもあまり出てこなかった,inputAccessoryView に入れた UIToolbar 自体に Safearea を考慮させる hack です。
1. UIToolbar の内部構造
ざっとこんな感じです。
これを見る限り constraint を更新すれば,問題なさそうですね。
2. 大まかな流れ
- Safe area の変化を捉えられるように,
insetsLayoutMarginsFromSafeArea
を有効化。 safeAreaInsetsDidChange
を捉え,invalidateIntrinsicContentSize
で UIToolbar のサイズ,setNeedsUpdateConstraints
で制約を更新が必要と通知。updateConstraints
で,UIStackView (厳密には内部クラス)
.bottom
==_UIToolbarContentView
.bottom
となる制約を見つけ,それをUIStackView
.bottom
==_UIToolbarContentView
.bottom
- SafeAreaBottom とする。
といった感じ。難しくはないですね。
3. コード
// MIT license でご自由にご利用ください import class Foundation.NSCoder import struct UIKit.CGFloat import struct UIKit.CGRect import struct UIKit.CGSize import class UIKit.NSLayoutConstraint import class UIKit.UIToolbar import class UIKit.UIView @available(iOS 11.0, *) public class UISafeToolbar: UIToolbar { private static let toolbarHeight: CGFloat = 44 private weak var toolbarContentView: UIView! public convenience init() { self.init(frame: .zero) } public override init(frame: CGRect) { super.init(frame: .zero) self.insetsLayoutMarginsFromSafeArea = true } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.insetsLayoutMarginsFromSafeArea = true } public override func didAddSubview(_ subview: UIView) { super.didAddSubview(subview) let viewClassString = String(describing: type(of: subview)) if viewClassString == "_UIToolbarContentView" { toolbarContentView = subview } } public override func safeAreaInsetsDidChange() { super.safeAreaInsetsDidChange() invalidateIntrinsicContentSize() setNeedsUpdateConstraints() } public override func updateConstraints() { super.updateConstraints() for constraint in toolbarContentView.constraints { if isBottomConstraint(constraint) { constraint.constant = -safeAreaInsets.bottom break } } } public override var intrinsicContentSize: CGSize { let height = UISafeToolbar.toolbarHeight + safeAreaInsets.bottom return CGSize(width: bounds.width, height: height) } private func isBottomConstraint(_ constraint: NSLayoutConstraint) -> Bool { return constraint.firstAttribute == .bottom && constraint.relation == .equal && constraint.secondItem === toolbarContentView && constraint.secondAttribute == .bottom } }
3.1 利用方法
OS で分岐してやればいいです
let toolbar: UIToolbar if #available(iOS 11.0, *) { toolbar = UISafeToolbar() } else { toolbar = UIToolbar(frame: .zero) } toolbar.autoresizingMask = [.flexibleHeight] toolbar.items = items yourTextView.inputAccessoryView = toolbar
実行結果は次の通り。
まとめ
そんなに難しくないのに,検索に乗ってこないのは意外でした。
iOS 12 しか動作確認していませんが,iOS 11 と内部構造は変わってないと思うので,多分大丈夫なはず。
現実問題として,UIToolbar そのままで使いたいケースもあると思うので,そのままの使用感で使える hack として実装してみました。