iOS13でもNavigationBarButtonの位置を調整したい

# はじめに

**iOS11**でNavigationBarButtonにStackViewが入ってきた影響で、private view を操作する方法を以前Qiitaに書きましたが、**iOS13**から private view を操作すると**怒られる(Crash)**ようです。
ちなみに以下のErrorが出ます。

```
Client error attempting to change layout margins of a private view
```

なので、Appleに怒られないように操作する方法をご紹介します。


**以下は以前の記事です。この記事は以下の改良版的な立ち位置になります。**
> iOS11からNavigationBarButton周りの仕様が変わったせいで、UIBarButtonItemの位置調整が上手くいかなくなった - Qiita
https://qiita.com/rd0501/items/2309e14c5a3f365867ec

# 先に結果をまとめると

**Tap範囲もちゃんとしたい場合は両端8ptより外側にBarButtonを配置するのは無理でした**
> 参考: UIBarButtonItem & iOS 11
https://www.matrixprojects.net/p/uibarbuttonitem-ios11#alignment-rect-insets

# 一応、各OSでのView Hierarchy を確認

|iOS13|iOS11|iOS10|
|---|---|---|
|<img width="429" alt="スクリーンショット 2019-09-25 14.57.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70276/fb9539ff-85de-62a8-b3aa-8fe00a2fee55.png">|<img width="540" alt="スクリーンショット 2018-03-19 15.12.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/70276/94c1ebcb-05ff-ea06-dbb6-2b14521866ea.png">|<img width="474" alt="スクリーンショット 2018-03-19 15.06.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/70276/c5eb216a-1df9-fb68-ada5-5ab7673de607.png">|
※前回の画像の使い回しなのでちょいバランス悪いですが...
View Hierarchyを見た感じだと、**iOS11とiOS13はほぼ同じですね。**
違いは private なクラス使用しているかどうかぐらいでしょうか。


# 対応方法

本題です。
先に言うと上でも引用した以下の記事のまとめ的な感じです。

> UIBarButtonItem & iOS 11
https://www.matrixprojects.net/p/uibarbuttonitem-ios11/


## 概要
  
１. **[alignmentRectInsets](https://developer.apple.com/documentation/uikit/uiview/1622648-alignmentrectinsets?language=objc)**を操作できるView等を用意します  
２. **navigationItem.left or rightBarButtonItems** の firstItem に、幅8ptの **[.fixedSpace](https://developer.apple.com/documentation/uikit/uibarbuttonitem/systemitem/fixedspace) の UIBarButtonItem** を入れる  
３. 追加するUIBarButtonItemを生成するが、**alignmentRectInsetsを操作して、spacer(.fixedSpace)の分だけずらす**(leftBarButtonItem左に8pt, rightBarButtonItem右に8pt)  

### alignmentRectInsetsを操作できるView等を用意します。

```swift
// 以下のような、後からalignmentRectInsetsを操作できるView等を用意します。

class CustomView: UIView {
    var alignmentRectInsetsOverride: UIEdgeInsets?
    override var alignmentRectInsets: UIEdgeInsets {
        return alignmentRectInsetsOverride ?? super.alignmentRectInsets
    }
}

class CustomButton: UIButton {
    var alignmentRectInsetsOverride: UIEdgeInsets?
    override var alignmentRectInsets: UIEdgeInsets {
        return alignmentRectInsetsOverride ?? super.alignmentRectInsets
    }
}
```

### navigationItem.left or rightBarButtonItems の firstItem に、幅8ptの .fixedSpace の UIBarButtonItem を入れる

```swift
// UIViewController の extension などに以下のような処理を追加して、 UIBarButtonItem を Set するときは以下の setNavLeftButton と setNavRightButton のメソッドを使用するようにする

extension UIViewController {

    func setNavLeftButton(_ button: UIBarButtonItem) {
        self.navigationItem.leftBarButtonItems = self.createBarButtonItemsForEachOS(button: button)
    }
    
    func setNavRightButton(_ button: UIBarButtonItem) {
        self.navigationItem.rightBarButtonItems = self.createBarButtonItemsForEachOS(button: button)
    }
    
    // iOS11以降の場合はcustom以外のButtonを入れないと外側からの余白が16ptになってしまうので、その対応のために.fixedSpaceを入れている
    // 入れると8ptになる
    private func createBarButtonItemsForEachOS(button: UIBarButtonItem) -> [UIBarButtonItem] {
        if #available(iOS 11, *) {
            return [self.createSpacer(), button]
        } else {
            return [button]
        }
    }
    
    private func createSpacer() -> UIBarButtonItem {
        let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
        spacer.width = 8 // なんか0にしても8になるので一旦8で
        return spacer
    }
}
```

### 追加するUIBarButtonItemを生成するが、alignmentRectInsetsを操作して、spacer(.fixedSpace)の分だけずらす**(leftBarButtonItem左に8pt, rightBarButtonItem右に8pt)  


```swift
// 以下のような、処理を行ってUIBarButtonItemを生成して、Right or LeftBarButtonItem に入れます。

extension UIBarButtonItem {

    enum BarButtonItemPosition {
        case right
        case left
    }

    static func createBarButtonItem(position: BarButtonItemPosition) -> UIBarButtonItem {
        let button = CustomButton(frame: CGRect(x:0, y: 0, width: 24, height: 24))
        // spacerが入っている分8pt外側にずらす
        switch position {
        case .left:
            button.alignmentRectInsetsOverride = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: -8)
        case .right:
            button.alignmentRectInsetsOverride = UIEdgeInsets(top: 0, left: -8, bottom: 0, right: 8)
        }
        button.translatesAutoresizingMaskIntoConstraints = false
        return UIBarButtonItem(customView: button)
    }
}
```

**以下、実装例です。**

```swift
// 使用例
class HogeViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    
        // barItem
        let leftButton = UIBarButtonItem.createBarButtonItem(position: .left)
        let rightButton = UIBarButtonItem.createBarButtonItem(position: .right)
        self.setNavLeftButton(leftButton)
        self.setNavRightButton(rightButton)
    }
}
```

※上記のコードで一部動かない可能性がありますので、エラーが発生したら修正リクエストください！

# 参考

UIBarButtonItem & iOS 11
https://www.matrixprojects.net/p/uibarbuttonitem-ios11/

alignmentRectInsets - UIView | Apple Developer Documentation
https://developer.apple.com/documentation/uikit/uiview/1622648-alignmentrectinsets?language=objc

iOS11からNavigationBarButton周りの仕様が変わったせいで、UIBarButtonItemの位置調整が上手くいかなくなった - Qiita
https://qiita.com/rd0501/items/2309e14c5a3f365867ec