LoginSignup
17
8

More than 3 years have passed since last update.

UIViewControllerに標準で行わせたい共通処理の実装例(backBarButtonItem編)

Last updated at Posted at 2019-11-06

経緯

UINavigationControllerでpush遷移を行った際、UINavigationBarのbackBarButtonItemに対して空文字を設定し、前画面のタイトル名を出さないようにするのはよくあることだと思う。
たまたま直近でこの作業をする必要出てきて、全ViewControllerに漏れなく簡単に対応させる方法何かなかったっけ?と思って呟いたのが始まり。

Plan A - UIStoryboardで処理

昔にも同じことやってるから、昔のソースコードを掘り起こして眺めてみたが、UIStoryboard上のViewControllerに1つずつ空白文字を設定していた。(UIStoryboard上ではデフォルトが空文字のため、空白を入れないとbackBarButtonItemのタイトルを消せない)

メリット

  • ストレートなやりかたでシンプル

デメリット

  • UIStoryboard上で設定するは見逃し易く、対応漏れが起き易い
  • UIStoryboard上で設定されているのを確認しても空なのか空白なのか判別しにくい

Plan B - extension methodを作ってviewDidLoadで処理

UIViewController+Extentions.swift
extension UIViewController {
    func setupBackBarButtonItem() {
        navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
    }
}

UIViewControllerのextension内でbackBarButtonItemをセットするメソッドを作り、

FooViewController.swift
final class FooViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        setupBackBarButtonItem()
    }
}

各ViewControllerのViewDidLoadで呼び出す。

メリット

  • extensionに記述しているので、メソッドを呼ぶだけでいい
  • ViewController毎にメソッドを呼ぶかどうかを分けれる

デメリット

  • 何も知らないと呼び出し忘れが発生する
  • 全画面共通でbackBarButtonItemにタイトルを表示しないという仕様が確定しているときに限るが、各ViewControllerで呼び出すのが煩わしかったり、実装漏れが出る可能性はある

Plan C - UINavigationControllerDelegateのnavigationController(_:willShow:animated:)で処理

UIViewController+Extentions.swift
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をセットする。

FirstViewController.swift
final class FirstViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationController?.delegate = self
    }
}

UINavigationControllerを持つ最初のViewControllerだけは、navigationController?.delegateのセットをしてあげる必要がある。

メリット

  • 同じUINavigationControllerの延長上では漏れなく処理される

デメリット

  • navigationController(_:willShow:animated:)で別の処理を行いたいときに、viewControllerの型を見る等して個別分岐処理を足す必要がある

Plan D - 親となるBaseViewControllerを作って処理

BaseViewController.swift
class BaseViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
    }
}

親となるBaseViewControllerのviewDidLoadでbackBarButtonItemをセットする。

ChildViewController.swift
final class ChildViewController: BaseViewController {
}

BaseViewControllerを継承する。

メリット

  • BaseViewControllerを継承する限りは基本的に処理漏れは発生しない

デメリット

  • ViewController毎でbackBarButtonItemに対して個別の対応が必要になったとき、super.viewDidLoad()を意図的に呼ばないように実装することになるため、呼び忘れじゃないことを周知しないといけない
  • 上記に追加で、super.viewDidLoad()で行われている他の処理があったとき、処理されない内容が出てくるので対応が必要になる

Plan E - 初期化時に処理

UIStoryboard+Extensions.swift
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で入れ替えて処理

UIViewController+Extensions.swift
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をセットする。

AppDelegate.swift
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をチョイスした。

何気ない発言から話が広がったのでまとめてみた。
メリット/デメリットは、記述した以外にもあるかもしれない。

17
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
8