Al añadir Firebase Analytics, los eventos de entrada a pantalla se registran automáticamente. Aunque no añadimos ni una línea de código en viewDidAppear.
Esto es posible gracias a Method Swizzling. Es una técnica que reemplaza por completo la implementación de un método en tiempo de ejecución: un caso representativo de la flexibilidad del runtime de Objective-C, pero también un arma de doble filo que puede abrir las puertas del infierno del debugging si se usa mal.
Como se explicó en el artículo sobre objc_msgSend, una llamada a un método de Objective-C consiste en «buscar un IMP (puntero a función) mediante un selector y saltar a él». El swizzling modifica en tiempo de ejecución esta tabla de correspondencias selector→IMP.
Principio: intercambiar dos filas de la tabla
La lista de métodos de una clase es una tabla de correspondencias «selector → IMP». method_exchangeImplementationsintercambia los IMP de dos entradas.
| Selector | IMP antes del intercambio | IMP después del intercambio |
|---|---|---|
viewDidAppear: |
Implementación original | Mi implementación |
swz_viewDidAppear: |
Mi implementación | Implementación original |
Después del intercambio, cuando el sistema llama a viewDidAppear:, se ejecuta mi implementación. La original no ha desaparecido; solo se ha mudado detrás del nombre swz_viewDidAppear:.
Implementación convencional completa
Así queda la implementación habitual en producción, con todas las medidas de seguridad incluidas.
@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]; // ¡No es recursión! Explicación abajo
NSLog(@"Entrada a pantalla: %@", NSStringFromClass([self class]));
}
@end
En este código hay dos puntos fáciles de confundir.
Primero, [self swz_viewDidAppear:animated]no es recursión. Cuando se ejecuta este código, los IMP ya se han intercambiado, así que el selector swz_ está conectado a la implementación original. En otras palabras, esta línea es la «llamada original». Si se omite, desaparece toda la lógica de transición de pantalla, por lo que es prácticamente obligatoria.
Segundo, ¿por qué probar primero class_addMethod en lugar de llamar directamente a method_exchangeImplementations? Porque el método objetivo puede estar implementado solo en la clase padre, no en esa clase. Hacer exchange directamente modificaría el método de la clase padre y afectaría a todas las demás subclases que heredan de ella. Si class_addMethod tiene éxito, se ha «añadido una nueva implementación original a esta clase», por lo que el intercambio queda limitado de forma segura a la propia clase.
El motivo para usar +load y dispatch_once es sencillo. El swizzling cambia el estado global, así que debe ejecutarse una sola vez, lo antes posible durante la vida de la app. El momento exacto de invocación de +load se trata por separado en el siguiente artículo (+load vs +initialize).
Efecto secundario silencioso: _cmd miente
Si imprimes _cmd (el selector actual) dentro de un método con swizzling, aparece swz_viewDidAppear: en lugar de viewDidAppear:. La implementación ha cambiado, pero el selector de la ruta de llamada permanece igual.
Normalmente no hay problema, pero al combinarlo con código que usa _cmd como clave (por ejemplo, el patrón de usar _cmd como clave de Associated Objects), aparece un bug sutil. Si la implementación original dependía internamente de _cmd, el comportamiento puede cambiar.
Hasta dónde es seguro usarlo
El ámbito en el que el swizzling está justificado es bastante reducido.
- Instrumentación común a todas las pantallas: cuando un SDK de Analytics o logging recopila automáticamente entradas a pantalla y toques en botones
- Evasión de errores de terceros o del sistema: al corregir temporalmente el comportamiento de un framework cuyo código fuente no está disponible
- Herramientas de depuración para desarrollo: al querer rastrear todas las llamadas a un método concreto
En cambio, estas situaciones son señales de alarma.
- Si varias bibliotecas hacen swizzling del mismo método, el orden de ejecución depende del orden de carga, y si una omite la llamada original, todo lo demás se viene abajo
- Los métodos
swz_se cuelan en el stack trace y dificultan interpretar los informes de crash - El swizzling que depende de implementaciones internas del sistema puede romperse con una sola actualización del sistema operativo
Por eso, si el problema se puede resolver mediante subclasificación, un proxy de delegado o composición, esas opciones deben ir siempre primero. El swizzling debe reservarse como último recurso cuando «otros métodos sean estructuralmente imposibles».
¿Y en Swift?
Los métodos Swift puros usan despacho estático (o una vtable), así que esta técnica no funciona. Para aplicar swizzling, el método objetivo debe estar expuesto al runtime de Objective-C.
class Tracker: NSObject {
@objc dynamic func fire() { }
}
Debe añadirse @objc dynamic para que la llamada siga la ruta objc_msgSend y así poder reemplazar la tabla de correspondencias. Las clases de UIKit se basan en Objective-C, por lo que el swizzling sigue funcionando, pero cuanto más nos acercamos al mundo SwiftUI, menos espacio tiene esta técnica.
Resumen
- El swizzling intercambia en tiempo de ejecución la tabla selector→IMP; la implementación original no desaparece, sino que se muda detrás de otro selector
- La llamada
[self swz_...]dentro de la implementación con swizzling no es recursión, sino la llamada original - Se prueba primero
class_addMethodpara no tocar el método de la clase padre - Usa
+load+dispatch_oncepara ejecutarlo una sola vez durante la vida de la app - Como las inconsistencias de
_cmd, los conflictos entre bibliotecas y el riesgo de actualizaciones del sistema operativo son reales, úsalo solo como último recurso - En Swift, solo se pueden aplicar swizzling a los métodos marcados con
@objc dynamic
En el siguiente artículo analizaremos otra magia del runtime que se confunde fácilmente con el swizzling: el isa-swizzling de KVO (Key-Value Observing). Se trata de reemplazar la clase completa, no un método.

