[Swift] UINavigationBarController の NavigationBar 高さ変更

#はじめに

**UINavigationController** を使う時、**UINavigationBar** の高さを変更してみたいと思うことがありました。
**UINavigationController** と **UINavigationBar** をカスタマイズしてみて実用的なのかを解説していきたいと思います。

# 実践

## CustomNavigationController.swift を作成

簡易イニシャライザを作成しておくことで、CustomNavigationController を便利に使うことができます。

``` CustomNavigationController.swift
class CustomNavigationController: UINavigationController {
    
    // 1. イニシャライザ
    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
    }
    
    // 2. イニシャライザ
    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)
    }

    // 必須イニシャライザ
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // 簡易イニシャライザ (1. 2. をまとめ便利にしたもの)
    convenience init(rootViewController:UIViewController , navigationBarClass:AnyClass?, toolbarClass: AnyClass?){
        self.init(navigationBarClass: navigationBarClass, toolbarClass: toolbarClass)
        self.viewControllers = [rootViewController]
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 他にすることがある場合
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}
```

## CustomNavigationBar.swift の作成

**NavigationBar** の高さを変更するには、カスタムクラスを作成する必要があります。
`sizeThatFits(_ size: CGSize)`は、指定したサイズに最適なサイズを計算して返すようにビューに要求するメソッドなので、以下のように **NavigationBar** の高さを指定すると期待通り動いてくれます。
この時注意点がありiPhone X 以降の場合は SafeArea の計算が必要です。

また、NavigationBarContentView の位置がずれるため、**NavigationBar** の高さから NavigationBarContentView の高さを差し引いた位置にずらす必要があります。

```CustomNavigationBar.swift
class CustomNavigationBar: UINavigationBar {
    // NavigationBar の高さ
    private let barHeight: CGFloat = 100
    
    // イニシャライザ
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        self.commonInit()
    }
    
    // 必須イニシャライザ
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func commonInit() {
        // NavigationBar の色
        self.barTintColor = .white
    }
    
    // 1. 指定されたサイズに最適なサイズを計算して返すようにビューに要求
    override func sizeThatFits(_ size: CGSize) -> CGSize {
        var newSize = super.sizeThatFits(size)
        var topInset: CGFloat = 0.0
        
        // iPhone X 以降 (SafeArea の高さを取得)
        if #available(iOS 11.0, *) {
            topInset = superview?.safeAreaInsets.top ?? 0
        }
        
        // NavigationBar の高さを設定
        newSize.height = barHeight + topInset

        return newSize
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        if #available(iOS 11.0, *) {
            for subview in subviews {
                let stringFromClass = NSStringFromClass(subview.classForCoder)
                
                // NavigationBar の高さを調整
                if stringFromClass.contains("UIBarBackground") {
                    let topInset: CGFloat = superview?.safeAreaInsets.top ?? 0
                    subview.frame = CGRect(origin: CGPoint(x: 0, y: -topInset), size: sizeThatFits(self.bounds.size))
                }
                
                // UINavigationBarContentView の位置を調整
                if stringFromClass.contains("UINavigationBarContentView") {
                    let y = (barHeight - subview.frame.height)
                    subview.frame.origin.y = y
                }
            }
        }
    }
}
```

**NavigationBar** の高さを変更できました。
SafeArea も問題なく計算できているようです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091516/811d7622-5d0f-9e7a-8dd5-767706f82d27.png" width="320" "iPhone 8">　 <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091516/d507e404-a58b-de83-eff8-a3a8e62b3174.png" width="320" "iPhone 12">

しかし、一見期待通りに見えますがデバッグ画面を確認すると問題点がありました。
RootViewController の View が **NavigationBar** に重なってしまっています。

この問題を改善するには、SafeArea を拡張する必要がありそうです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091516/6cdc31d5-4fce-0567-620b-50a0fa56c752.png" width="500">

# 結果

SafeAreaを拡張してからデバッグ画面を確認してみても View の重なりは改善されていませんでした。
そのため、View を追加する際に上側の制約を SafeArea に指定すると期待通り動いてくれました。
おそらく SafeArea の拡張はできているがデバッグ画面では確認できなかった。

```RootViewController.swift
class RootViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 背景色
        self.view.backgroundColor = .systemGreen
        // NavigationBar のタイトル
        self.title = "Test"
        
        // SafeArea の拡張
        if #available(iOS 11.0, *) {
            // NavigationBar の高さ - 元の NavigationBar の高さ
            additionalSafeAreaInsets.top = 100 - 44
        }
        
        // 赤い正方形 を追加
        self.view.addSubview(topView)
        
        // 制約を設定
        topView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
        topView.widthAnchor.constraint(equalToConstant: 100).isActive = true
        topView.heightAnchor.constraint(equalToConstant: 100).isActive = true
        topView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
    }
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091516/2aeced0b-0e81-4a5f-d734-5f1ea7c756a8.png" width="320pc">


#さいごに

かなり複雑な内容になったため、わかりにくい箇所もあるかもしれませんが、**NavigationBar** の高さを変更することは出来ました。
けれど、問題点も多くあまり実用的ではないような気がします。

以下に問題点を過剰書きしときます。これらを改善したりしてでも使いたいと思うならば実装してみてもいいかもしれません。

- 画面遷移の時、**NavigationBar** の高さが一時的に元に戻る
- NavigationBarItem の titleView に UIButton などを追加した時、タップできる範囲に限りがある
 - これは公式でも対応していないらしく、改善することはハック的要素が強い
 - 将来的に使えなくなる可能性がある

ここまで見ていただきありがとうございます、皆様の学びの助けになれば幸いです。

#参考文献

* [sizeThatFits（_ :) | Apple Developer Documentation](https://developer.apple.com/documentation/uikit/uiview/1622625-sizethatfits)