【Swift4】UINavigationControllerのNavigationBarをカスタマイズしてみた。【高さ変更】
どうも。Reoです。Swift記事はお久しぶりですね。
今回は、UINavigationController内のナビゲーションバーをカスタマイズしてみました。
ずっとUINavigationBarを単独で使っていたのですが、iPhoneXが出て、単独だとセーフエリアを考慮するのがとても面倒なことを実感しました。なので最近はUINavigationControllerを使う方向に移行しています。
以前「【Swift】Xcode9+iOS11でUINavigationBarが正しく表示できなくて困った話」という記事を書いたのですが、これも結局iPhoneX対応ではなく、高さをちゃんと表示するための対応策でしかないんですよね。
これだと結局UINavigationBarを単独で使うときは、iPhoneXかどうかの場合分けを書く必要が出てしまっていました。
この問題の一番簡単な解決策は、「ちゃんとUINavigationControllerを使おう!」なんですよね。
ただし、上記リンクの記事でお話しした「Xcode9での高さが反映されないバグ」は(おそらく)今回も発生していました。
今までUINavigationControllerをまともに使ってきたことがない最大の理由は、ナビゲーションバーのカスタマイズがわからない!ってことでした。ツールバーも同様です。
今回はUINavigationController内の
・ナビゲーションバーの色を変更 ・ナビゲーションバーの高さを変更
をメインに実装していこうと思います。
StoryBoardは使いません!
コード見りゃわかるって人はこちらからどうぞ。uruly/CustomNavigationController-CustomNavigationBar
UINavigationControllerを継承したクラスを用意しよう
まずはUINavigationControllerを継承したクラスを作ります。
File > New > File より Cocoa Touch Class を選択。 Subclass of をUINavigationControllerにして作成
rootViewControllerの設定と、ナビゲーションバーとツールバーのクラスの設定をまとめてしたかったので、簡易イニシャライザを用意します。ここは別に飛ばしてもk。
作ったクラス内(CustomNavigationController)にinit文を書いていきます。
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
    }
    
    override init(navigationBarClass: AnyClass?, toolbarClass: AnyClass?) {
        super.init(navigationBarClass: navigationBarClass, toolbarClass: toolbarClass)
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }ここは丸っとコピペでおk。
init(nibName:bundle:)のoverrideを書かなかったらエラーが出ました。
Fatal error: Use of unimplemented initializer 'init(nibName:bundle:)' for プロジェクト名.CustomNavigationViewController
簡易イニシャライザを作成。
    convenience init(rootVC:UIViewController , naviBarClass:AnyClass?, toolbarClass: AnyClass?){
        self.init(navigationBarClass: naviBarClass, toolbarClass: toolbarClass)
        self.viewControllers = [rootVC]
    }先ほど書いたinit(navigationBarClass:,toolbarClass)を使います。
一応init(rootViewController:)の部分は書かなくてもなんの問題もありません。
ただし、書いてないとinit(rootViewController:)での初期化ができなくなるので、ナビゲーションバーやツールバーをカスタムしないで使おうというときでも呼び出すことができません。
ここまでのまとめ
import UIKit
class CustomNavigationController: UINavigationController {
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
    }
    
    override init(navigationBarClass: AnyClass?, toolbarClass: AnyClass?) {
        super.init(navigationBarClass: navigationBarClass, toolbarClass: toolbarClass)
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
    
    convenience init(rootVC:UIViewController , naviBarClass:AnyClass?, toolbarClass: AnyClass?){
        self.init(navigationBarClass: naviBarClass, toolbarClass: toolbarClass)
        self.viewControllers = [rootVC]
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
}
初期表示画面にNavigationViewControllerを用いよう
先ほど作ったのをとりあえず初期表示させたいのでAppDelegate.swift内に以下のように書きます。
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        let vc = ViewController()
        let naviVC = CustomNavigationController(rootVC: vc, naviBarClass: nil, toolbarClass: nil)
        self.window!.rootViewController = naviVC
        
        return true
    }
    /***   ~~   ***/
}
簡易イニシャライザを作ってないときは
let vc = ViewController()
let naviVC = CustomNavigationController(navigationBarClass: nil, toolbarClass: nil)
naviVC.viewControllers = [vc]
self.window!.rootViewController = naviVCとしてあげればおkです。
ここまでで実行すれば、ナビゲーションバーがついたViewが表示されるはずです。
カスタムナビゲーションバーを作ろう
カスタムナビゲーションバーを作ります。
File > New > File より Cocoa Touch Class を選択。 Subclass of をUINavigationBarにして作成
この中にイニシャライザを書きます。
import UIKit
class CustomNavigationBar: UINavigationBar {
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
}
先ほどのCustomNavigationViewControllerを生成するところで、このCustomNavigationBarを使うように設定しましょう!
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        let vc = ViewController()
        let naviVC = CustomNavigationController(rootVC: vc, naviBarClass: CustomNavigationBar.self, toolbarClass: nil)
        self.window!.rootViewController = naviVC
        
        return true
    }簡易イニシャライザを用意してない人は
let naviVC = CustomNavigationController(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)とします。
これでカスタマイズの準備はおk!
ナビゲーションバーの色を変えてみよう
まずは簡単にナビゲーションバーの色を変えてみましょう。
先ほどのCustomNavigationBarクラス内に
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    func setup(){
        self.barTintColor = UIColor.blue    //バーの色
        self.tintColor = UIColor.white    //バー上のアイテムの色
        //タイトルテキストの色
        self.titleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white]
    }ハイライトしたところを付け足しました。
これで実行してみるとバーの色が変わったのがわかると思います。
rootViewController上での設定
ナビゲーションバーにタイトルをつけましょう。
タイトルはNavigationControllerのrootViewControllerに設定されているViewControllerにて設定します。 今回の場合だとlet vc = ViewController()としたこいつです。
ViewController内でタイトルを書きましょう。
import UIKit
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //ここでタイトルを設定
        self.title = "タイトル"
    }
    /*** ~~ ***/
}
ここまでを実行するとこうなりました。
このViewController内でキャンセルボタンを設置してみましょう。
ここに最初はめちゃくちゃハマりました。
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //ここでタイトルを設定
        self.title = "タイトル"
        self.view.backgroundColor = UIColor.white
        
        //キャンセルボタンを作成
        let cancelBtn = UIBarButtonItem(title: "キャンセル", style: .plain, target: self, action: #selector(self.cancelTapped))
        //ボタンを設置
        self.navigationItem.leftBarButtonItem = cancelBtn
    }
    
    @objc func cancelTapped(){
        print("キャンセル")
    }
これだけ見るとどこにハマる要素があるの?って思うかもしれないですが、例えばナビゲーションバーを隠したいときは以下のように書きます。
self.navigationController?.setNavigationBarHidden(true, animated: false)バーの色を変えたいときは
self.navigationController?.navigationBar.barTintColor = UIColor.greenうんうん、じゃあバーアイテムを置くときは
//これはダメ!!!!表示されない!!!
self.navigationController?.navigationItem.leftBarButtonItem = cancelBtnってナンデヤネンヽ(#゚Д゚)ノ┌┛)`Д゚)・;'
ということで、バーアイテムを置くときはself.navigationItemに設定 しないと反映されません。私のようにハマらないように気をつけてくださいね。
ナビゲーションバーの高さを変更しよう
今回一番やりたかったことはコレです。
CustomNavigationBar内でsizeThatFitsをoverrideします。
class CustomNavigationBar: UINavigationBar {
    /*** ~~ ***/
    override func sizeThatFits(_ size: CGSize) -> CGSize {
        //渡されるsizeは widthは決まっているがheightは決まっていない
        //super.sizeThatFits(size)でheightが決まる
        var newSize = super.sizeThatFits(size)
        let addHeight:CGFloat = 20.0    //通常よりどれだけ大きくするか
        newSize.height += addHeight
        
        return newSize
    }
    
}本来ならコレだけでバー自体の高さは決まるのでは?と思っているのですが、どうにも変化がない。
ここの部分が一番最初に話した「Xcode9+ios11でのバグ 」なんじゃないかなーと思ってます。
ここまでの状態でiOS9とiOS10で検証してみたところ、指定した高さにちゃんとなりました。
わかりやすいようにaddHeightを100にしてあります。
iOS11だと高さはデフォルトのまま表示されてしまいます。
ということで「【Swift】Xcode9+iOS11でUINavigationBarが正しく表示できなくて困った話」で用いた方法でiOS11に対応していきます。
CustomNavigationBarのlayoutSubviewsをoverrideします。
    override func layoutSubviews() {
        super.layoutSubviews()
        //iOS11以上でのみ
        if #available(iOS 11.0, *) {
            for subview in self.subviews {
                let stringFromClass = NSStringFromClass(subview.classForCoder)
                if stringFromClass.contains("BarBackground") {
                    //ステータスバー分あげないと余白ができる。
                    let statusBarHeight = UIApplication.shared.statusBarFrame.height
                    let point = CGPoint(x:0,y:-statusBarHeight)
                    //ここでバーの高さを調節 (sizeThatFitsを呼び出す)
                    subview.frame = CGRect(origin: point, size: sizeThatFits(self.bounds.size))
                }
            }
        }
    }書いている時に気付いたので修正したのですが、
//ここでバーの高さを調節 (sizeThatFitsを呼び出す)
subview.frame = CGRect(origin: self.bounds.origin, size: sizeThatFits(self.bounds.size))
のoriginがself.bounds.originだとステータスバー分隙間が空いてしまいます。
とりあえずその分だけマイナスしたところから表示するようにしました。
ステータスバーを表示しない設定の場合はself.bounds.originでおkです。(x:0,y:0)
ここまで動かすとiOS11のiPhone7だとちゃんと高さが反映されました。やったね。
と思ってiPhoneXで動かすと反映されてないことに気づきました。記事書くの一時中断して戦ってました。無事なんとかなりましたのでので。
iPhoneXに対応する!
上記コードで動かしてもiPhoneXでは高さが反映されません。というか、反映はされているんですが何か高さ足りてないんです。
そう!足りないのはsafeArea部分!
高さを変えなければ自動で考慮してくれるのですが、高さを変更してしまうとそこを自分で設定しないといけません。
CustomNavigationBar内のsizeThatFitsに以下のように書き足します。
    override func sizeThatFits(_ size: CGSize) -> CGSize {
        //渡されるsizeは widthは決まっているがheightは決まっていない
        //super.sizeThatFits(size)でheightが決まる
        var newSize = super.sizeThatFits(size)
        
        //iphoneX用
        var topInset:CGFloat = 0
        if #available(iOS 11.0, *) {
            topInset = superview?.safeAreaInsets.top ?? 0
        }
        let addHeight:CGFloat = 20.0 + topInset    //通常よりどれだけ大きくするか
        newSize.height += addHeight
        
        return newSize
    }
これでiPhoneXでも高さを変更することができました。
左からiOS10,iOS11,iPhoneXです。
あと少し気になるところがありますね。ナビゲーションバーの要素の高さです。
今回はiOS10での表示に合わせます。
先ほどのlayoutSubviewsのところに付け加えます。
    override func layoutSubviews() {
        super.layoutSubviews()
        if #available(iOS 11.0, *) {
            for subview in self.subviews {
                let stringFromClass = NSStringFromClass(subview.classForCoder)
                if stringFromClass.contains("BarBackground") {
                    //ステータスバー分あげないと余白ができる。
                    let statusBarHeight = UIApplication.shared.statusBarFrame.height
                    let point = CGPoint(x:0,y:-statusBarHeight)
                    //ここでバーの高さを調節 (sizeThatFitsを呼び出す)
                    subview.frame = CGRect(origin: point, size: sizeThatFits(self.bounds.size))
                }else if stringFromClass.contains("BarContentView") {
                    //ここでサブビューの位置を調整
                    subview.frame.origin.y = 20.0    //付け加えた高さ分でちょうど良い!
                    print(subview.frame.size.height)
                }
            }
        }
    }付け加えた高さ分 で大体iOS10での表示と同じになります。
CustomNavigationBarの全文コードはこうなりました。
import UIKit
class CustomNavigationBar: UINavigationBar {
    //デフォルトよりどれだけ高くするか
    let addHeight:CGFloat = 40.0
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    func setup(){
        self.barTintColor = UIColor.blue
        self.tintColor = UIColor.white
        self.titleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white]
    }
    
    override func sizeThatFits(_ size: CGSize) -> CGSize {
        //渡されるsizeは widthは決まっているがheightは決まっていない
        //super.sizeThatFits(size)でheightが決まる
        var newSize = super.sizeThatFits(size)
        
        //iphoneX用
        var topInset:CGFloat = 0
        if #available(iOS 11.0, *) {
            topInset = superview?.safeAreaInsets.top ?? 0
        }
        newSize.height += addHeight + topInset  //通常よりどれだけ大きくするか
        
        return newSize
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        if #available(iOS 11.0, *) {
            for subview in self.subviews {
                let stringFromClass = NSStringFromClass(subview.classForCoder)
                if stringFromClass.contains("BarBackground") {
                    //ステータスバー分あげないと余白ができる。
                    let statusBarHeight = UIApplication.shared.statusBarFrame.height
                    let point = CGPoint(x:0,y:-statusBarHeight)
                    //ここでバーの高さを調節 (sizeThatFitsを呼び出す)
                    subview.frame = CGRect(origin: point, size: sizeThatFits(self.bounds.size))
                }else if stringFromClass.contains("BarContentView") {
                    //ここでサブビューの位置を調整
                    subview.frame.origin.y = addHeight
                }
            }
        }
    }
}
これでおkのはず!決め打ちしていた高さを定数にしておいたよ。
以上です!!!!!!!
ついでにブログを書くのと同時に作ったサンプルをgithubにあげました。
uruly/CustomNavigationController-CustomNavigationBar
記事の通りのことしかしてないですが、もしかするとカスタムツールバーを付け加える可能性も。そのときはまた記事書くかもしれないですけどね〜。
結局iPhoneXでの場合分け書いてるじゃんね・・・
iPhoneXにも関わらずsafeAreaInsets.topが0ならば、CustomNavigationControllerで
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        if #available(iOS 11.0, *) {
            print(self.view.safeAreaInsets)
        }
        //更新
        self.navigationBar.setNeedsLayout()
    }としてみるとうまくいくかもしれない。
今回はなくてもちゃんとsafeAreaInsets取れたので、書いてないですが、もし0の場合は設定される前に呼ばれてるかもしれませぬ。
以上でっす。何かの役に立てば〜。
ではでは。
追記(2018/02/03)
ステータスバーを表示しないときに、iPhoneXだとナビゲーションバーの上に空白ができてしまいます。
そんなときはsafeAreaInsets.top分をマイナスしてあげてください。
     override func layoutSubviews() {
        super.layoutSubviews()
        if #available(iOS 11.0, *) {
            for subview in self.subviews {
                let stringFromClass = NSStringFromClass(subview.classForCoder)
                if stringFromClass.contains("BarBackground") {
                    let statusBarHeight = UIApplication.shared.statusBarFrame.height
                    var point = CGPoint(x:0,y:-statusBarHeight)
                    //iPhoneXならステータスバー表示しない時に0.0になるので、確認しないといけない
                    if let top = superview?.safeAreaInsets.top,
                        top != 0 && statusBarHeight == 0{
                        point = CGPoint(x:0,y:-top)
                    }
                    //ここでバーの高さを調節 (sizeThatFitsを呼び出す)
                    subview.frame = CGRect(origin: point, size: sizeThatFits(self.bounds.size))
                }else if stringFromClass.contains("BarContentView") {
                    //ここでサブビューの位置を調整
                    subview.frame.origin.y = addHeight
                }
            }
        }githubの方にあげたやつも修正しました!

 
   
   
   
   
  