NavigationBarのカスタマイズ法をサンプルアプリから学ぶ

## はじめに
NavigationBarをカスタマイズする方法がAppleのDeveloperサイトに載っています。
https://developer.apple.com/documentation/uikit/uinavigationcontroller/customizing_your_app_s_navigation_bar

そこにはサンプルアプリもダウンロードできるようになっており、記事とサンプルアプリの両方を見ながらNavigationBarのカスタマイズ方法を学びます。

NavigationBarをカスタマイズする方法として以下の２種類があると記載されています。

- ViewControllerのUINavigationItemを変更することで間接的にNavigationBarを変更
- Appearanceプロキシを使用して直接NavigationControllerを変更

概要の意味の深掘りはここではしません。
実際にサイトの説明やサンプルアプリを順に追って見ていきましょう。

## BarStyleの変更

NavigationBarの外観を変更する方法です。
サンプルコードはご覧のようになっています。

```swift
// NavigationBarのスタイルを変更するbarStyleを.blackとすることで背景を黒くする
self.navigationController!.navigationBar.barStyle = .black

// isTranslucentでNavigationBarを半透明にする
self.navigationController!.navigationBar.isTranslucent = true

// NavigationBarのタイトルの色を変更
self.navigationController!.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white]

// NavigationBarのItemの色を変更
self.navigationController!.navigationBar.tintColor = #colorLiteral(red: 1, green: 0.99997437, blue: 0.9999912977, alpha: 1)
```
サンプルコードにコメントを入れてみました。
NavigationBarのスタイルを変更するbarStyleには実は`.blackTranslucent`というstyleが存在していたのですが、iOS13.0でdeprecatedになったので、stlyeを.blackに、isTranslucentをtrueにという方法を取っています。

こちらのサンプルコードが実行されると以下のような見た目になります。
![Simulator Screen Shot - iPhone 12 - 2020-12-17 at 14.30.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/329f1b23-b5f6-b141-4154-fa4d2fcddcef.png)

## RightViewをカスタマイズする
RightViewというのはNavigationBar右側の領域でカスタムUIViewの適用やUIBarButtonItemを使用することができます。

サンプルでは、３種類のUIBarButtonItemをNavigationBarの右側に配置しています。
サンプルコードです。

```swift
// 上下の画像を設定したUISegmentedControlを生成
let segmentedControl = UISegmentedControl(items: [
    UIImage(systemName: "arrow.up")!,
    UIImage(systemName: "arrow.down")!
])
// セグメントが選択された状態を表示しない（デフォルトはfalse）
segmentedControl.isMomentary = true

// BarButtonItemのカスタムUIViewとして、segmentedControlを使用
let segmentBarItem = UIBarButtonItem(customView: segmentedControl)
// 右側にItemを設定
navigationItem.rightBarButtonItem = segmentBarItem
```
サイト上だと末尾２行しか掲載されてなかったので、サンプルアプリからソースコードを一部追加しました。
こちらの例ではUISegmentedControlをNavigationBar右側のバーボタン項目として設定しています。

見た目はこうなります。
NavigationBar右側に上下の画像が設定されたUISegmentedControlが表示されていると思います。
![Simulator Screen Shot - iPhone 12 - 2020-12-17 at 14.46.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/7c07fafa-06d7-813b-6ee8-c7d72e798c6d.png)

## TitleViewをカスタマイズする
`navigationItem.titleView`にUIViewを設定して、NavigationBarのTitleViewをカスタマイズすることができます。
サンプルコードではUISegmentedControlを中央のカスタムTitleViewとして設定しています。

```swift
let segmentTextContent = [
    NSLocalizedString("Image", comment: ""),
    NSLocalizedString("Text", comment: ""),
    NSLocalizedString("Video", comment: "")
]
let segmentedControl = UISegmentedControl(items: segmentTextContent)
self.navigationItem.titleView = segmentedControl
```

![Simulator Screen Shot - iPhone 12 - 2020-12-18 at 18.46.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/7cf6283c-dd4f-4748-c4b5-26a150f0b9cb.png)


## NavigationPromptの変更
PromptとはNavigationBar上部に表示される1行のテキストのことです。
UINavigationItemのpromptプロパティを使用します。

```swift
navigationItem.prompt = NSLocalizedString("Navigation prompts appear at the top.", comment: "")
```
promptはきちんと１行までに限定されていて、改行コードを設定しても無効でした。
改行コード以降の文字は無視されて表示されません。
また、大量の文字を設定しましたが、端末幅に収まりきらない文字は表示できるフォントサイズまで小さくされます。
adjustsFontSizeToFitWidthをtrueにしたのと同じような動きですね。
場合によっては認識できないくらいのフォントサイズに縮小されてしまうので、文字数は意識しましょう。（あまり長い文字を設定するのはオススメしません）

![Simulator Screen Shot - iPhone 12 - 2020-12-18 at 19.15.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/eb859c3b-e6b4-8169-0ab8-fcdef4d7cfa2.png)

## NavigationBarの外観をカスタマイズする
barTintColorや背景画像を追加してNavigationBarの背景をカスタマイズします。
サンプルコードでは画像を設定する方法を紹介しています。

```swift
guard let bounds = navigationController?.navigationBar.bounds else { return }

// 色を指定して、グラデーション画像を生成  
var backImageForDefaultBarMetrics =
    UIImage.gradientImage(bounds: bounds,
                          colors: [UIColor.systemBlue.cgColor, UIColor.systemFill.cgColor])
var backImageForLandscapePhoneBarMetrics =
    UIImage.gradientImage(bounds: bounds,
                          colors: [UIColor.systemTeal.cgColor, UIColor.systemFill.cgColor])

let navigationBarAppearance = self.navigationController!.navigationBar
// デフォルト（より適切な背景画像が見つからない時に使用される）
navigationBarAppearance.setBackgroundImage(backImageForDefaultBarMetrics, for: .default)
// iPhoneで横向きのときに使用される
navigationBarAppearance.setBackgroundImage(backImageForLandscapePhoneBarMetrics, for: .compact)
```

![Simulator Screen Shot - iPhone 11 Pro - 2021-01-10 at 00.59.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/239e7d08-91ad-1e3f-b5fe-3993d92d09aa.png)

## 戻るボタンのタイトルをカスタマイズ

ユーザーは戻るボタンをタップ・ホールドすることで異なるスタックレベルを素早く切り替えることができると説明されています。
サンプルコードはありませんが、サンプルアプリではスタック内の各ViewControlerのレベルに合わせてバックボタンのタイトルをカスタマイズしています。
文章だと少し伝わりづらいかもしれないので、サンプルアプリで実装されているコードを抜粋します。

```swift
func makeViewController(_ level: Int) -> UIViewController {
    let viewController = UIViewController()
    viewController.navigationItem.backButtonTitle = "\(level)"
    return viewController
}

for level in 1..<10 {
    self.navigationController?.pushViewController(
        makeViewController(level), animated: false
    )
}

self.navigationController?.pushViewController(makeViewController(10), animated: true)
```

各スタックのレベルに応じて戻るボタンがカスタマイズされていることがわかると思います。
１０個のViewControllerを作成し、それをPUSHすることでスタック上に戻るボタンがカスタマイズされたViewControllerが積み上がります。

## 戻るボタンを画像でカスタマイズ

NavigationBarの戻るボタンはデフォルトのままだと、テキストと戻る矢印がセットで表示されます。
カスタマイズすることでそれを表示せず、ユーザー側で用意した画像を表示する方法です。
UINavigationBarAppearanceを使用しています。
ここでようやくAppearanceプロキシが登場してきましたね。

```swift
let backButtonBackgroundImage = UIImage(systemName: "list.bullet")

// CustomBackButtonNavControllerにあるすべてのUINavigationBarの設定を変更するためにUINavigationBarAppearanceを生成
let barAppearance =
    UINavigationBar.appearance(whenContainedInInstancesOf: [CustomBackButtonNavController.self])

// 戻るボタンを変更する（backIndicatorTransitionMaskImageも一緒に設定する必要がある）
barAppearance.backIndicatorImage = backButtonBackgroundImage
// PushやPopのトランジション時にコンテンツのマスクとして使用される画像
barAppearance.backIndicatorTransitionMaskImage = backButtonBackgroundImage

// CustomBackButtonNavControllerにあるすべてのUIBarButtonItemの設定を変更するためにUIBarButtonAppearanceを生成
let barButtonAppearance =
    UIBarButtonItem.appearance(whenContainedInInstancesOf: [CustomBackButtonNavController.self])
// 戻るボタンのテキストの位置を調整
barButtonAppearance.setBackButtonTitlePositionAdjustment(UIOffset(horizontal: 0, vertical: -5), for: .default)
```

戻るボタンのタイトルを取り除くためのコードも必要です。
空文字を設定してデフォルトの設定を変更していることが分かります。

```swift 
let backBarButtton = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
navigationItem.backBarButtonItem = backBarButtton
```

これを実装すると戻るボタンは以下のような見た目になります。

![Simulator Screen Shot - iPhone 11 Pro - 2021-01-10 at 18.35.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/071f7388-ebe3-514f-655f-2c57558a22ea.png)

## NavigationBarのタイトル文字を大きくする

こちらは非常にシンプルに実装することができます。
ただしLargeタイトルはScrollTopの時に限定され、スクロールすると通常の表示に自動的に切り替わります。

```swift
self.navigationController?.navigationBar.prefersLargeTitles = true
```

### ScrollTopのとき
![Simulator Screen Shot - iPhone 11 Pro - 2021-01-10 at 18.48.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/512527ba-f7d4-7cdb-001a-5993e97985c8.png)

### Scrollしたとき
![Simulator Screen Shot - iPhone 11 Pro - 2021-01-10 at 19.23.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/e859663b-131f-2c7f-d80b-e9e486590e96.png)

## NavigationBarの外観を変更
少し前にも利用されていましたが、
UINavigationBarAppearanceとUIBarButtonItemAppearanceを使用する方法です。

```swift
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
// 背景色を変更
appearance.backgroundColor = UIColor.systemRed
// NavigationBarのタイトル色を変更
appearance.titleTextAttributes = [.foregroundColor: UIColor.lightText]
// 通常のNavigationBarの表示
navigationItem.standardAppearance = appearance
// LargeタイトルのScrollTopの時の表示
navigationItem.scrollEdgeAppearance = appearance
// 表示モードが横向きの時の表示
navigationItem.compactAppearance = appearance

let buttonAppearance = UIBarButtonItemAppearance()
// UIBarButtonのタイトルの色を変更
buttonAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.systemGreen]
navigationItem.standardAppearance?.buttonAppearance = buttonAppearance
navigationItem.compactAppearance?.buttonAppearance = buttonAppearance

let doneButtonAppearance = UIBarButtonItemAppearance()
// Doneボタンのタイトルの色を変更
doneButtonAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor.systemYellow]
navigationItem.standardAppearance?.doneButtonAppearance = doneButtonAppearance
navigationItem.compactAppearance?.doneButtonAppearance = doneButtonAppearance
```

ここでのポイントとしてはAppearanceが`standardAppearance`、`scrollEdgeAppearance`、`compactAppearance`、の３種類あることです。
大まかな意味合いはソースコードのコメントに記載しています。
NavigationBarは表示モードが若干違いますので、それぞれで設定が適用されるようにしています。

### standardAppearance
![Simulator Screen Shot - iPhone 11 Pro - 2021-01-10 at 19.10.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/f16b1724-8b58-27a5-fe92-e2f241b52875.png)

### scrollEdgeAppearance
サンプルアプリではNavigationBarの表示はstandardなので、
scrollEdgeAppearanceの表示をご紹介するためにソースコードを追加しています。

```swift
// 追加
appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.lightText]
navigationController?.navigationBar.prefersLargeTitles = true
```

![Simulator Screen Shot - iPhone 11 Pro - 2021-01-10 at 19.21.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/f97525a5-c4c7-c0ce-dfa3-96ad4368ae9b.png)

### compactAppearance
![Simulator Screen Shot - iPhone 11 Pro - 2021-01-10 at 19.22.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/40beea6e-dc42-1124-65db-70a612b1fdb1.png)


## BarButtonItemにメニューを追加
メニューを追加すると、１箇所でアプリケーションの機能に簡単にアクセスすることができます。
UIBarButtonItemの右側にUIMenuを追加します。

```swift
@IBOutlet var optionsBarItem: UIBarButtonItem!

func menuHandler(action: UIAction) {
    Swift.debugPrint("Menu handler: \(action.title)")
}

override func viewDidLoad() {
    let barButtonMenu = UIMenu(title: "", children: [
        UIAction(title: NSLocalizedString("Copy", comment: ""), image: UIImage(systemName: "doc.on.doc"), handler: menuHandler),
        UIAction(title: NSLocalizedString("Rename", comment: ""), image: UIImage(systemName: "pencil"), handler: menuHandler),
        UIAction(title: NSLocalizedString("Duplicate", comment: ""), image: UIImage(systemName: "plus.square.on.square"), handler: menuHandler),
        UIAction(title: NSLocalizedString("Move", comment: ""), image: UIImage(systemName: "folder"), handler: menuHandler)
    ])
    optionsBarItem.menu = barButtonMenu
}
```

UIBarButtonItemにはmenuプロパティがあるので、そこにUIMenuを設定しています。
ただし、iOS14から追加されたプロパティのようなのでOSバージョンには注意してください。
サンプルではボタンを押してもログを出力しているだけなので、アプリの要件に合わせて変更すると良いと思います。

### メニュー閉
![Simulator Screen Shot - iPhone 11 Pro - 2021-01-10 at 19.39.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/19987f3f-51a9-e651-bf06-aa701fd418fb.png)

### メニュー開
![Simulator Screen Shot - iPhone 11 Pro - 2021-01-10 at 19.39.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596715/592c85c4-fe90-800f-451a-6a63e7d1ab06.png)


### 最後に

UINavigationBarのカスタマイズ方法をご紹介しました。
私もやったことがないものもあったり、よくわかってなかったところが整理されたので勉強になりました。
参考になれば幸いです。