Firebase Analyticsを組み込むと、画面表示イベントが自動で記録されます。viewDidAppearにコードを1行も追加していないのに、です。
これが可能になる理由が**メソッドスウィズリング(Method Swizzling)**です。ランタイムでメソッドの実装を丸ごと置き換える手法で、Objective-Cランタイムの柔軟性を示す代表例である一方、誤用するとデバッグ地獄を招く両刃の剣でもあります。
objc_msgSendの記事で説明したように、Objective-Cのメソッド呼び出しは「セレクタでIMP(関数ポインタ)を探してジャンプする」仕組みです。スウィズリングは、このセレクタ→IMP対応表をランタイムで変更することです。
原理:対応表の2行を入れ替える
クラスのメソッドリストは「セレクタ → IMP」の対応表です。method_exchangeImplementationsは2つの項目のIMPを入れ替えます。
| セレクタ | 交換前のIMP | 交換後のIMP |
|---|---|---|
viewDidAppear: |
元の実装 | 自分の実装 |
swz_viewDidAppear: |
自分の実装 | 元の実装 |
交換後、システムがviewDidAppear:を呼び出すと、自分の実装が実行されます。元の実装が消えたのではなく、swz_viewDidAppear:という名前の背後へ移動しただけです。
慣用的な実装の全体像
実務で使われる安全策をすべて含めると、次のような形になります。
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSEL = @selector(viewDidAppear:);
SEL swizzledSEL = @selector(swz_viewDidAppear:);
Method original = class_getInstanceMethod(cls, originalSEL);
Method swizzled = class_getInstanceMethod(cls, swizzledSEL);
BOOL added = class_addMethod(cls, originalSEL,
method_getImplementation(swizzled),
method_getTypeEncoding(swizzled));
if (added) {
class_replaceMethod(cls, swizzledSEL,
method_getImplementation(original),
method_getTypeEncoding(original));
} else {
method_exchangeImplementations(original, swizzled);
}
});
}
- (void)swz_viewDidAppear:(BOOL)animated {
[self swz_viewDidAppear:animated]; // 再帰ではない!以下で説明
NSLog(@"画面表示: %@", NSStringFromClass([self class]));
}
@end
このコードで混乱しやすいポイントは2つあります。
**1つ目、[self swz_viewDidAppear:animated]は再帰ではありません。**このコードが実行される時点ではIMPがすでに入れ替わっているため、swz_セレクタには元の実装が接続されています。つまり、この1行が「元の実装の呼び出し」です。元の呼び出しを忘れると画面遷移ロジック全体が消えるため、実質的に必須です。
**2つ目、なぜmethod_exchangeImplementationsを直接呼ばず、先にclass_addMethodを試すのでしょうか?**対象メソッドがそのクラスではなく、親クラスにだけ実装されている場合があるためです。ここで直接exchangeすると親クラスのメソッドを変更してしまい、その親を継承する他のサブクラスすべてに影響します。class_addMethodが成功すれば「このクラスに元の実装を新しく追加」したことになるため、自クラスの範囲だけで安全に交換できます。
+loadとdispatch_onceを使う理由は単純です。スウィズリングはグローバル状態を変更するため、アプリのライフサイクル中に最も早いタイミングで一度だけ実行する必要があります。+loadの正確な呼び出しタイミングは、次の記事(+load vs +initialize)で別途扱います。
静かな副作用:_cmdが嘘をつく
スウィズリングしたメソッド内で_cmd(現在のセレクタ)を出力すると、viewDidAppear:ではなくswz_viewDidAppear:と表示されます。実装は変わっても、呼び出し経路のセレクタは変わらないためです。
通常は問題ありませんが、_cmdをキーとして使うコード(例:Associated Objectsのキーに_cmdを使うパターン)と組み合わせると、微妙なバグになります。元の実装が内部で_cmdに依存していた場合、動作が変わる可能性があります。
どこまで使ってよいのか
スウィズリングが正当化される領域はかなり限定的です。
- 全画面共通の計測:Analytics・ロギングSDKが画面表示やボタンタップを自動収集する場合
- サードパーティ・システムのバグ回避:ソースのないフレームワークの動作を一時的に修正する場合
- 開発用デバッグツール:特定メソッドの呼び出しをすべて追跡したい場合
逆に、次のような状況は危険信号です。
- 複数のライブラリが同じメソッドをスウィズリングすると、実行順序がロード順に左右され、1つでも元の呼び出しを忘れると他のすべてが崩れます
- スタックトレースに
swz_メソッドが入り込み、クラッシュレポートの解釈が難しくなります - システム内部の実装に依存するスウィズリングは、OSのアップデート1回で壊れる可能性があります
そのため、サブクラス化、デリゲートプロキシ、コンポジションで解決できる問題なら、必ずそちらを先に検討します。スウィズリングは「他の方法では構造的に不可能な場合」の最後の手段として残すべきです。
Swiftでは?
純粋なSwiftメソッドは静的ディスパッチ(またはvtable)なので、この手法は使えません。スウィズリングするには、対象メソッドがObjective-Cランタイムに公開されている必要があります。
class Tracker: NSObject {
@objc dynamic func fire() { }
}
@objc dynamicが付いていれば呼び出しはobjc_msgSend経路に流れ、そこで初めて対応表を置き換えられます。UIKitクラスはObjective-Cベースなので今もスウィズリングできますが、SwiftUIの世界に近づくほど、この手法の居場所は狭くなっています。
まとめ
- スウィズリングはセレクタ→IMP対応表をランタイムで入れ替える手法です — 元の実装は消えず、別のセレクタの背後へ移動します
- swizzled実装内の
[self swz_...]呼び出しは再帰ではなく、元の実装の呼び出しです class_addMethodを先に試すのは、親クラスのメソッドに触れないためです+load+dispatch_onceで、アプリのライフサイクル中に一度だけ実行されるようにします_cmdの不一致、ライブラリ間の競合、OSアップデートのリスクは現実にあるため、最後の手段としてのみ使います- Swiftでは、
@objc dynamicが付いたメソッドだけがスウィズリングされます
次の記事では、スウィズリングと混同しやすいもう1つのランタイムマジック、KVO(Key-Value Observing)のisa-swizzlingを掘り下げます。メソッドではなく、クラス自体を置き換える話です。

