Edited at

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

More than 1 year has passed since last update.


参考

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


濃い赤字になっている部分だけ注目してもらえたらと思います。

ひとまずなんか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
}