viewDidLoadviewWillAppearあるいはpresent(viewController)などの実行時に、共通で処理を差し込みたいことがあります。そういった場合には、SwiftのMethod Swizzlingという機能が便利です。今回は、UIViewControllerをモーダル表示する際に、そのタイトルをログ出力するという実装例で、Method Swizzlingを使ってみます。

なお、今回参考にした資料は以下のとおりです。

Method Swizzlingとは

Method Swizzlingとは、関数の実装を、他の実装とランタイムで入れ替える機能のことです。 これはとても便利な機能です。例えば、アプリプロジェクト全体に共通処理を導入する際に、既存の実装に大きく手を入れずに解決できることがあります。 参考にした資料によれば、Method Swizzlingができる関数には条件があるようです。

  • NSObjectのサブクラス
  • @objcがついたクラス
  • dynamicのメソッド

今回は、UIViewControllerのpresentを入れ替えますが、UIViewControllerはNSObjectのクラスだから、大丈夫ということになりますね。(ちょっと自信ない)

presentに差し込む処理のフロー

それでは、present実行時の差し込む処理フローを考えてみます。 今回は、presentが実行されたときに、そのタイトルを出力したいので、ざっくり以下の流れです。

  1. いつも通りpresentを呼び出す
  2. ViewControllerが表示される前に、独自実装したログ出力処理が呼ばれる
  3. 独自実装の処理が終わると、通常のpresent処理が実行される

このような流れです。

実装の仕方

では実装していきます。ここで例示するコードは、参考にした資料のプログラムに大きく影響を受けた実装になっています

import UIKit

extension UIViewController {
    private struct SwizzleStatic {
        // Method Swizzlingが、アプリ実行中に1度しか実行されないようにしている
        static var once = true
    }

    // メソッドの交換を実行する
    class func swizzle() {
        guard SwizzleStatic.once else { return }
        SwizzleStatic.once = false

        // 交換処理をまとめたメソッド
        let swizzleMethod = { (original: Selector, swizzled: Selector) in
            guard let originalMethod = class_getInstanceMethod(self, original),
                  let swizzledMethod = class_getInstanceMethod(self, swizzled) else { return }
            method_exchangeImplementations(originalMethod, swizzledMethod)
        }

        // メソッドの交換
        swizzleMethod(#selector(present(_:animated:completion:)),
                      #selector(swizzledPresent(_:animated:completion:)))
    }

    // 上の `swizzle()` を実行すると、このメソッドが `present` として振る舞う
    @objc func swizzledPresent(_ viewControllerToPresent: UIViewController,
                               animated flag: Bool,
                               completion: (() -> Void)? = nil) {
        // ログ出力する
        print(viewControllerToPresent.title)

        // 通常のpresent処理を実行する
        // 呼び出すメソッドは `swizzledPresent` だが、メソッドが入れ替わっているから、
        // 実装の中身は「オリジナルの」`present` になっている
        swizzledPresent(viewControllerToPresent, animated: flag, completion: completion)
    }
}

意外とサクッと、実装することができました。 あとは、AppDelegateのdidFinishLaunchingWithOptionsなどで、上のswizzle()を実行してあげればOK。

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
    ...
    // メソッドを交換する
    UIViewController.swizzle()
    ...
}

これで完成! ここではInstance Functionを入れ替えましたが、Class Functionも入れ替えられます。 詳しくは、参考資料の方を読んでみてください。

まとめ

Method Swizzlingを使った、アプリの共通処理を差し込む方法を紹介しました。 さほど難しくはないので、プロジェクト全体でライフサイクルやイベントの共通処理を実装したいときには、使いやすいかなと思います。

一方で、実装の「見た目」はオリジナルそのままの呼び出し方であるため、予め実装が入れ替わっていることを知っておかないと、バグ・不具合の原因になりそうです。例えば、チーム開発のプロジェクトでMethod Swizzlingを使うと、新しいメンバーがそのことを知らずに開発してしまうこともあるかもしれません。メソッドが入れ替わっていることが、明確に、はっきりと分かるように、プロジェクトやコードを整えておく工夫も必要かもしれません。

ところで、このような横断的な共通処理を実装する方法は、アスペクト指向プログラミングと言うそうです。そこについては、詳しくないので書かずに終了します。