経緯
UINavigationControllerでpush遷移を行った際、UINavigationBarのbackBarButtonItemに対して空文字を設定し、前画面のタイトル名を出さないようにするのはよくあることだと思う。
たまたま直近でこの作業をする必要出てきて、全ViewControllerに漏れなく簡単に対応させる方法何かなかったっけ?と思って呟いたのが始まり。
Plan A - UIStoryboardで処理
昔にも同じことやってるから、昔のソースコードを掘り起こして眺めてみたが、UIStoryboard上のViewControllerに1つずつ空白文字を設定していた。(UIStoryboard上ではデフォルトが空文字のため、空白を入れないとbackBarButtonItemのタイトルを消せない)
メリット
- ストレートなやりかたでシンプル
デメリット
- UIStoryboard上で設定するは見逃し易く、対応漏れが起き易い
- UIStoryboard上で設定されているのを確認しても空なのか空白なのか判別しにくい
Plan B - extension methodを作ってviewDidLoadで処理
extension UIViewController {
func setupBackBarButtonItem() {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
}
UIViewControllerのextension内でbackBarButtonItemをセットするメソッドを作り、
final class FooViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setupBackBarButtonItem()
}
}
各ViewControllerのViewDidLoad
で呼び出す。
メリット
- extensionに記述しているので、メソッドを呼ぶだけでいい
- ViewController毎にメソッドを呼ぶかどうかを分けれる
デメリット
- 何も知らないと呼び出し忘れが発生する
- 全画面共通でbackBarButtonItemにタイトルを表示しないという仕様が確定しているときに限るが、各ViewControllerで呼び出すのが煩わしかったり、実装漏れが出る可能性はある
Plan C - UINavigationControllerDelegateのnavigationController(_:willShow:animated:)で処理
extension UIViewController: UINavigationControllerDelegate {
public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
viewController.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
viewController.navigationController?.delegate = viewController
}
}
UINavigationControllerDelegateの共通処理を作成し、navigationController(_:willShow:animated:)の中でbackBarButtonItemをセットする。
final class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}
}
UINavigationControllerを持つ最初のViewControllerだけは、navigationController?.delegate
のセットをしてあげる必要がある。
メリット
- 同じUINavigationControllerの延長上では漏れなく処理される
デメリット
- navigationController(_:willShow:animated:)で別の処理を行いたいときに、viewControllerの型を見る等して個別分岐処理を足す必要がある
Plan D - 親となるBaseViewControllerを作って処理
class BaseViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
}
親となるBaseViewControllerのviewDidLoad
でbackBarButtonItemをセットする。
final class ChildViewController: BaseViewController {
}
BaseViewControllerを継承する。
メリット
- BaseViewControllerを継承する限りは基本的に処理漏れは発生しない
デメリット
- ViewController毎でbackBarButtonItemに対して個別の対応が必要になったとき、
super.viewDidLoad()
を意図的に呼ばないように実装することになるため、呼び忘れじゃないことを周知しないといけない - 上記に追加で、
super.viewDidLoad()
で行われている他の処理があったとき、処理されない内容が出てくるので対応が必要になる
Plan E - 初期化時に処理
static func instantiateInitialViewController<T>(from className: T.Type) -> T where T : UIViewController {
let name = String(describing: className.self)
guard let vc = UIStoryboard(name: name, bundle: .main).instantiateInitialViewController() as? T else {
fatalError("型が不一致")
}
vc.navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
return vc
}
instanceを作成する際にbackBarButtonItemをセットする。
メリット
- 共通のinitializerを使う限り処理漏れは発生しない
デメリット
- 初期化処理が共通化されている必要がある
- 個別処理を行う場合に場合分けが必要
- 独自のinitializerを定義することになる
Plan F - Method Swizzlingで入れ替えて処理
extension UIViewController {
static func methodSwizzling() {
guard let from = class_getInstanceMethod(self, #selector(viewDidLoad)) else { return }
guard let to = class_getInstanceMethod(self, #selector(swizzlingViewDidLoad)) else { return }
method_exchangeImplementations(from, to)
}
@objc func swizzlingViewDidLoad() {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
}
Method Swizzlingの下準備で入れ替える処理を記述し、入れ替えたメソッド側でbackBarButtonItemをセットする。
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
UIViewController.methodSwizzling()
return true
}
}
起動時に入れ替え処理を呼び出して処理させる。
メリット
- 1度実装してしまえば処理漏れは発生しない
デメリット
- 黒魔術故に一般的ではない
- 個別処理を行う必要がある場合には対応が面倒くさい
まとめ
A〜Fと実装パターンをあげたが、どれでも目的は達成できる。
あとは柔軟性を求めるのか漏れをなくすためにどうするかチーム間での合意次第。
自分は今回Plan B
をチョイスした。
何気ない発言から話が広がったのでまとめてみた。
メリット/デメリットは、記述した以外にもあるかもしれない。