ナビゲーションバーの高さをデフォルトから変更したかっただけなのですが、iOS10とiOS11でナビゲーションバーの仕様が違うために色々と苦労したので、ここに備忘録としてまとめておきます。
やりたいこと
- ナビゲーションバーの高さをデフォルトから変更したい
- iOS10とiOS11に対応したい
環境
- XCode 9.2
- Swift 4.0.3
サンプルアプリ
画面構成
NavigationControllerを使って2つのViewController間で画面遷移をするアプリを考えます。
以下で説明するCustomNavigationBar
クラスを、NavigationController配下のNavigationBarのCustom Classとして指定してください。
カスタムナビゲーションバークラスを作成する
ナビゲーションバーの高さを変更するには、UINavigationBarクラスを継承したカスタムクラスを作成する必要があります。
iOS10では、sizeThatFits()
メソッドで高さを指定します。
以下のように指定の高さを返すようにしてあげるだけで、ナビゲーションバーの高さは変更されます。
import UIKit
class CustomNavigationBar: UINavigationBar {
let barHeight: CGFloat = 100
override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: barHeight)
}
}
しかし、これだとナビゲーションバー上のタイトルなどの位置が以下のようになってしまいます。
遷移先の画面。
タイトルや戻るボタンの位置を調整する
タイトルや戻るボタン(UIBarButtonItem)のY軸方向の位置を調整するには、setTitleVerticalPositionAdjustment()
、setBackgroundVerticalPositionAdjustment()
というメソッドを利用します。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.setTitleVerticalPositionAdjustment(-20.0, for: .default)
}
}
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "ic_arrow_back"), style: .plain, target: self, action: #selector(back))
navigationController?.navigationBar.setTitleVerticalPositionAdjustment(-20.0, for: .default)
navigationItem.leftBarButtonItem?.setBackgroundVerticalPositionAdjustment(-20.0, for: .default)
}
@objc func back() {
navigationController?.popViewController(animated: true)
}
}
遷移先の画面。
1点注意なのが、UIBarButtonItemに関しては画像が指定されている必要があることです。
以下のように文字列を指定していると位置の変更がされません。
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// navigationItem.leftBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "ic_arrow_back"), style: .plain, target: self, action: #selector(back))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(back))
navigationController?.navigationBar.setTitleVerticalPositionAdjustment(-20.0, for: .default)
navigationItem.leftBarButtonItem?.setBackgroundVerticalPositionAdjustment(-20.0, for: .default)
}
@objc func back() {
navigationController?.popViewController(animated: true)
}
}
カスタムナビゲーションバークラスをiOS11に対応させる
iOS11では、上述した方法ではナビゲーションバーの高さは変更されません。
代わりに、layoutSubviews()
をオーバーライドした実装をしてあげる必要があります。
ここはStackOverflowで紹介されていた方法を利用しました。
import UIKit
class CustomNavigationBar: UINavigationBar {
let barHeight: CGFloat = 100
override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: barHeight)
}
override func layoutSubviews() {
super.layoutSubviews()
if #available(iOS 11.0, *) {
for subview in subviews {
let stringFromClass = NSStringFromClass(subview.classForCoder)
if stringFromClass.contains("UIBarBackground") {
subview.frame = CGRect(x: 0, y: 0, width: frame.width, height: barHeight)
subview.sizeToFit()
}
if stringFromClass.contains("UINavigationBarContentView") {
let centerY = (barHeight - subview.frame.height) / 2.0
subview.frame = CGRect(x: 0, y: centerY, width: frame.width, height: subview.frame.height)
subview.sizeToFit()
}
}
}
}
}
この実装が何をやっているかは、画面をデバッグしてみるとよくわかります。
UINavigationBarは、配下にUIBarBackground
とUINavigationBarContentView
というビューを持っています。
緑色の部分がUIBarBackground
ビューで、この部分を指定したい高さに設定しています。
黄色の部分はUINavigationBarContentView
ビューで、配下にUILabelを持っています。
タイトルの位置をナビゲーションバーの真ん中の高さに来るように見せたいために、このビューの位置をUIBarBackground
の真ん中に来るように設定しています。
UINavigationBarContentView
にもUIBarBackground
と同じ設定をすれば良いように思いますが、UINavigationBarContentView
は高さ変更がどうやらできないらしく、このような方法を取っています。
SafeAreaを広げる
デバッグ画面からわかるように、このままだとViewController配下のView(青色部分)がナビゲーションバーにかぶってしまいます。
上記のカスタムナビゲーションバークラスの実装では、ナビゲーションバーの高さ自体は変更されていないため、SafeAreaのサイズも変わりません。
そこで、ViewController側で自身のViewのSafeAreaサイズを拡張して上げる必要があります。
以下のように、ナビゲーションバーのデフォルトの高さからの増分を、additionalSafeAreaInsets.top
に与えてあげます。
こうすることでSafeAreaのTop側が拡張され、Viewがかぶらないようになります。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 11.0, *) {
additionalSafeAreaInsets.top = 100 - 44
} else {
navigationController?.navigationBar.setTitleVerticalPositionAdjustment(-20.0, for: .default)
}
}
}
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "ic_arrow_back"), style: .plain, target: self, action: #selector(back))
if #available(iOS 11.0, *) {
additionalSafeAreaInsets.top = 100 - 44
} else {
navigationController?.navigationBar.setTitleVerticalPositionAdjustment(-20.0, for: .default)
navigationItem.leftBarButtonItem?.setBackgroundVerticalPositionAdjustment(-20.0, for: .default)
}
}
@objc func back() {
navigationController?.popViewController(animated: true)
}
}
仕上げ
これで終わりと思いきや、上記の実装だとiOS11で画面遷移の際にナビゲーションバーが一瞬縮むような動きが出てしまいます。
解決策として、ViewControllerのviewWillAppear()
メソッドでナビゲーションバーのsizeToFit()
メソッドを呼ぶようにしています。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 11.0, *) {
print(additionalSafeAreaInsets)
additionalSafeAreaInsets.top = 100 - 44
} else {
navigationController?.navigationBar.setTitleVerticalPositionAdjustment(-20.0, for: .default)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.sizeToFit()
}
}
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "ic_arrow_back"), style: .plain, target: self, action: #selector(back))
if #available(iOS 11.0, *) {
additionalSafeAreaInsets.top = 100 - 44
} else {
navigationController?.navigationBar.setTitleVerticalPositionAdjustment(-20.0, for: .default)
navigationItem.leftBarButtonItem?.setBackgroundVerticalPositionAdjustment(-20.0, for: .default)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.sizeToFit()
}
@objc func back() {
navigationController?.popViewController(animated: true)
}
}
なぜこれで解決できるのかはいまいち理解できていないので、わかる方いたらコメントいただけると嬉しいです。
わかっていることとしては、viewWillAppear()
でsizeToFit()
を呼ぶと、カスタムナビゲーションバークラスのsizeThatFits()
が呼ばれ、ナビゲーションバーの高さが一時的に指定の値(ここでは100)になります。
最終的にナビゲーションバーの高さは元の44に戻ってしまうのですが、この一時的に指定の高さに変更するというのがポイントなのでしょうか。
まとめ
だいぶ長くなりましたが、ナビゲーションバーの高さをデフォルトから変更する方法について説明しました。
やりたいことは至って単純なのに、結構手間がかかりますね。
もっとシンプルで良いやり方があったら、是非教えてください
参考