iOS11からNavigationBarButton周りの仕様が変わったせいで、UIBarButtonItemの位置調整が上手くいかなくなった

# 追記
iOS13対応版あげました

> iOS13でもNavigationBarButtonの位置を調整したい - Qiita
https://qiita.com/rd0501/items/8e8254a23d87a9b2fea2

# 参考
iOS 11 - UIBarButtonItem horizontal position | Apple Developer Forums
https://forums.developer.apple.com/thread/80075
ios11 - iOS 11 UINavigationBar bar button items alignment - Stack Overflow
https://stackoverflow.com/questions/44677018/ios-11-uinavigationbar-bar-button-items-alignment
UIBarButtonItem & iOS 11
http://www.matrixprojects.net/p/uibarbuttonitem-ios11/


#　環境
Xcode9.2
iOS10 or iOS11

# 何がいいたいかというと

iOS11からNavigationBarButton周りの仕様が変わったので、NavigationBarButtonの位置を調整していた場合は、OS毎にやり方を変えないとうまくいかないですよって話。
特に、Defaultの位置よりも外側にButtonを置きたい場合。

# まずは、OS間での違い

iOS11以降とiOS10以前ではNavigationBarButtonがなんか違う！
View Hierarchyを見てみると、見たことないStackViewが入ってました。

わかりにくいですが...

|iOS11|iOS10|
|---|---|
|<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">|

濃い赤字になっている部分だけ注目してもらえたらと思います。
ひとまずなんかStackViewが増えてんなって感じです。

# なにがつらいのか

DefaultではNavigationBarButtonはどのOSでも外側から16pt離れた位置に置かれます。
そして、それを直接操作することはできない。
iOS10以前だとBarの上に直で置けたので、UIButtonのimageInsetをいじったり、`func point(inside point: CGPoint, with event: UIEvent?) -> Bool`を使ったりしてなんとかなっていた。
しかし、StackViewが入ってきたことでUIButtonのタップ領域を広げても、StackViewのタップ領域が狭いのでタップできない

→　どうにかしてStackViewのTap領域を広げないといけない！！

# 流れとか書いてきたけど、限界なのでさっさと結果を書きます。

# 実装

以下適宜数値を変えて調整してください

```swift:BaseNavigationController.swift
/// UINavigationControllerのサブクラス。
/// 基本的にこのクラスのinit(rootViewController:)のみ使用。
/// swift:BaseNavigationBarをBarに設定するためです。
final class BaseNavigationController: UINavigationController {
    
    // このInit以外は使用しないでください。(Storyboard & Xib からの呼び出しもやめて)
    // このInitを使用(BaseNavigationBarを使用)しないとNavigationBarButtonの表示位置がおかしくなります
    override init(rootViewController: UIViewController) {
        if #available(iOS 11, *) {
            super.init(navigationBarClass: BaseNavigationBar.self, toolbarClass: UIToolbar.self)
            self.viewControllers = [rootViewController]
        } else {
            super.init(rootViewController: rootViewController)
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }   
}
```

```swift:BaseNavigationBar.swift
/// 全ての子StackViewのMarginをZeroにするUINavigationBar
final class BaseNavigationBar: UINavigationBar {
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        if #available(iOS 11, *) {
            loop: for view in subviews {
                for stack in view.subviews where stack is UIStackView {
                    stack.superview?.layoutMargins = .zero
                    break loop
                }
            }
        }
        
    }
}

```

```swift:ExpansionButton.swift
/// タップ領域を簡単に拡大、縮小できるUIButton
final class ExpansionButton: UIButton {
    
    @IBInspectable var top: CGFloat {
        get { return insets.top }
        set { insets.top = newValue }
    }
    @IBInspectable var left: CGFloat {
        get { return insets.left }
        set { insets.left = newValue }
    }
    @IBInspectable var bottom: CGFloat {
        get { return insets.bottom }
        set { insets.bottom = newValue }
    }
    @IBInspectable var right: CGFloat {
        get { return insets.right }
        set { insets.right = newValue }
    }
    
    var insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        var rect = bounds
        rect.origin.x -= insets.left
        rect.origin.y -= insets.top
        rect.size.width += insets.left + insets.right
        rect.size.height += insets.top + insets.bottom
        
        return rect.contains(point)
    }
}
```
*引用*
*【iOS】UIButtonのタップ領域だけを拡大する - Qiita*
*https://qiita.com/KokiEnomoto/items/264f26bfa92d06b1996e*

```swift:ExpansionView.swift
/// タップ領域を簡単に拡大、縮小できるUIView
class ExpansionView: UIView {
    
    @IBInspectable var top: CGFloat {
        get { return insets.top }
        set { insets.top = newValue }
    }
    @IBInspectable var left: CGFloat {
        get { return insets.left }
        set { insets.left = newValue }
    }
    @IBInspectable var bottom: CGFloat {
        get { return insets.bottom }
        set { insets.bottom = newValue }
    }
    @IBInspectable var right: CGFloat {
        get { return insets.right }
        set { insets.right = newValue }
    }
    
    var insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        var rect = bounds
        rect.origin.x -= insets.left
        rect.origin.y -= insets.top
        rect.size.width += insets.left + insets.right
        rect.size.height += insets.top + insets.bottom
        
        return rect.contains(point)
    }
}
```

```swift:UIBarButtonItem+.swift
enum UIBarButtonItemPotition {
    case right
    case left
}

extension UIBarButtonItem {
    
    /// UIBarButtonItemを使用する場合は基本的にこれを使ってね！
    /// 画面の右端、左端からそれぞれ6pt離れた位置にアイコンが表示されるよ!
    /// アイコンサイズは28pt固定になっているよ！適宜直してね！
    ///
    /// - Parameters:
    ///   - image: Bar Button Icon Image
    ///   - position: NavigationBarの右か左か
    ///   - target: Tapした時に呼ばれるTarget
    ///   - action: Tapした時に呼ばれるAction
    /// - Returns: UIBarButtonItem
    static func createBarButton(image: UIImage, position: UIBarButtonItemPotition, target: Any?, action: Selector) -> UIBarButtonItem {
        let button = ExpansionButton()
        if #available(iOS 11, *) {
            button.frame = CGRect(x: 0, y: 0, width: 40, height: 28)
            button.insets = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0)
            button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 6)
        } else {
            button.frame = CGRect(x: 0, y: 0, width: 28, height: 28)
            // UINavigationBarのButtonは余白が強制的に16入るので、そこからデザインに合わせて位置をずらしています
            switch position {
            case .left:
                // なんかiOS10でタップ領域鬼広いんだけどなんでだろ？
                // 左にずらす
                button.insets = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: -4)
                button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: 0, right: 10)
            case .right:
                // 右にずらす
                button.insets = UIEdgeInsets(top: 10, left: -4, bottom: 10, right: 0)
                button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: -10)
            }
        }
        button.setImage(image, for: .normal)
        button.addTarget(target, action: action, for: .touchUpInside)
        return UIBarButtonItem(customView: button)
    }
}
```

上記を全部コピペして、以下のように使用すればOKです。

```swift:HogeViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()
    
    self.navigationItem.leftBarButtonItem = UIBarButtonItem.createBarButton(image: UIImage(named: "back")!, position: .left, target: self, action: #selector(back(_:)))
}

func back(_ button: UIButton) {
    // hogehoge
}

```