Swift
ios11

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

参考

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
スクリーンショット 2018-03-19 15.12.28.png スクリーンショット 2018-03-19 15.06.17.png

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

なにがつらいのか

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

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

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

実装

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

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)
    }   
}
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
                }
            }
        }

    }
}

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

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)
    }
}
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です。

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
}