ios - ventajas - Swift 3.1 deprecates initialize(). ¿Cómo puedo lograr lo mismo?
ventajas de objective c (9)
Objective-C declara una función de clase,
initialize()
, que se ejecuta una vez para cada clase, antes de usarla.
A menudo se usa como un punto de entrada para intercambiar implementaciones de métodos (swizzling), entre otras cosas.
Swift 3.1 desprecia esta función con una advertencia:
El método ''initialize ()'' define el método de clase Objective-C ''initialize'', que Swift no puede invocar y que no se permitirá en futuras versiones.
¿Cómo se puede resolver esto, manteniendo el mismo comportamiento y características que implemento actualmente usando el punto de entrada
initialize()
?
Solución fácil / simple
Un punto de entrada de aplicación común es la aplicación de un delegado de
applicationDidFinishLaunching
.
Podríamos simplemente agregar una función estática a cada clase que queremos notificar en la inicialización, y llamarla desde aquí.
Esta primera solución es simple y fácil de entender.
Para la mayoría de los casos, esto es lo que recomendaría.
Aunque la próxima solución proporciona resultados que son más similares a la función
initialize()
original, también da como resultado tiempos de inicio de la aplicación un poco más largos.
Ya no creo que valga la pena el esfuerzo, la degradación del rendimiento o la complejidad del código en la mayoría de los casos.
El código simple es un buen código.
Sigue leyendo para otra opción. Es posible que tenga razones para necesitarlo (o tal vez partes de él).
Solución no tan simple
La primera solución no necesariamente escala tan bien. ¿Y qué pasa si está creando un marco de trabajo, donde desea que se ejecute su código sin que nadie necesite llamarlo desde el delegado de la aplicación?
Paso uno
Defina el siguiente código Swift.
El propósito es proporcionar un punto de entrada simple para cualquier clase que desee imbuir con un comportamiento similar al de
initialize()
; esto ahora se puede hacer simplemente al conformarse con
SelfAware
.
También proporciona una función única para ejecutar este comportamiento para cada clase conforme.
protocol SelfAware: class {
static func awake()
}
class NothingToSeeHere {
static func harmlessFunction() {
let typeCount = Int(objc_getClassList(nil, 0))
let types = UnsafeMutablePointer<AnyClass?>.allocate(capacity: typeCount)
let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass?>(types)
objc_getClassList(autoreleasingTypes, Int32(typeCount))
for index in 0 ..< typeCount { (types[index] as? SelfAware.Type)?.awake() }
types.deallocate(capacity: typeCount)
}
}
Segundo paso
Eso está muy bien, pero aún necesitamos una forma de ejecutar la función que definimos, es decir,
NothingToSeeHere.harmlessFunction()
, en el inicio de la aplicación.
Anteriormente, esta respuesta sugería usar el código Objective-C para hacer esto.
Sin embargo, parece que podemos hacer lo que necesitamos usando solo Swift.
Para macOS u otras plataformas donde UIApplication no está disponible, se necesitará una variación de lo siguiente.
extension UIApplication {
private static let runOnce: Void = {
NothingToSeeHere.harmlessFunction()
}()
override open var next: UIResponder? {
// Called before applicationDidFinishLaunching
UIApplication.runOnce
return super.next
}
}
Paso tres
Ahora tenemos un punto de entrada al inicio de la aplicación, y una forma de conectarlo desde las clases de su elección.
Todo lo que queda por hacer: en lugar de implementar
initialize()
, conformarse con
SelfAware
e implementar el método definido,
awake()
.
-
Marque su clase como
@objc
-
NSObject
deNSObject
- Agregue la categoría ObjC a su clase
-
Implementar
initialize
en categoría
Ejemplo
Archivos Swift:
//MyClass.swift
@objc class MyClass : NSObject
{
}
Archivos Objc:
//MyClass+ObjC.h
#import "MyClass-Swift.h"
@interface MyClass (ObjC)
@end
//MyClass+ObjC.m
#import "MyClass+ObjC.h"
@implement MyClass (ObjC)
+ (void)initialize {
[super initialize];
}
@end
Aquí hay una solución que funciona en swift 3.1+
@objc func newViewWillAppear(_ animated: Bool) {
self.newViewWillAppear(animated) //Incase we need to override this method
let viewControllerName = String(describing: type(of: self)).replacingOccurrences(of: "ViewController", with: "", options: .literal, range: nil)
print("VIEW APPEAR", viewControllerName)
}
static func swizzleViewWillAppear() {
//Make sure This isn''t a subclass of UIViewController, So that It applies to all UIViewController childs
if self != UIViewController.self {
return
}
let _: () = {
let originalSelector = #selector(UIViewController.viewWillAppear(_:))
let swizzledSelector = #selector(UIViewController.newViewWillAppear(_:))
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
method_exchangeImplementations(originalMethod!, swizzledMethod!);
}()
}
Luego en AppDelegate:
UIViewController.swizzleViewWillAppear()
Creo que es una forma alternativa.
También podemos escribir la función
initialize()
en el código de Objective-C, luego usarlo por referencia de puente
Espero la mejor manera .....
Ligera adición a la excelente clase de @ JordanSmith que asegura que cada
awake()
solo se llame una vez:
protocol SelfAware: class {
static func awake()
}
@objc class NothingToSeeHere: NSObject {
private static let doOnce: Any? = {
_harmlessFunction()
}()
static func harmlessFunction() {
_ = NothingToSeeHere.doOnce
}
private static func _harmlessFunction() {
let typeCount = Int(objc_getClassList(nil, 0))
let types = UnsafeMutablePointer<AnyClass?>.allocate(capacity: typeCount)
let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass?>(types)
objc_getClassList(autoreleasingTypes, Int32(typeCount))
for index in 0 ..< typeCount { (types[index] as? SelfAware.Type)?.awake() }
types.deallocate(capacity: typeCount)
}
}
Mi enfoque es esencialmente el mismo que el de Adib. Aquí hay un ejemplo de una aplicación de escritorio que usa Core Data; El objetivo aquí es registrar nuestro transformador personalizado antes de que un código lo mencione:
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
override init() {
super.init()
AppDelegate.doInitialize
}
static let doInitialize : Void = {
// set up transformer
ValueTransformer.setValueTransformer(DateToDayOfWeekTransformer(), forName: .DateToDayOfWeekTransformer)
}()
// ...
}
Lo bueno es que esto funciona para cualquier clase, tal como lo hizo
initialize
, siempre que cubra todas sus bases, es decir, debe implementar cada inicializador.
Aquí hay un ejemplo de una vista de texto que configura su propio proxy de apariencia una vez antes de que cualquier instancia tenga la oportunidad de aparecer en pantalla;
El ejemplo es artificial pero la encapsulación es extremadamente agradable:
class CustomTextView : UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame:frame, textContainer: textContainer)
CustomTextView.doInitialize
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
CustomTextView.doInitialize
}
static let doInitialize : Void = {
CustomTextView.appearance().backgroundColor = .green
}()
}
Eso demuestra la ventaja de este enfoque mucho mejor que el delegado de la aplicación.
Solo hay una instancia de delegado de aplicación, por lo que el problema no es muy interesante;
pero puede haber muchas instancias CustomTextView.
Sin embargo, la línea
CustomTextView.appearance().backgroundColor = .green
se ejecutará
solo una vez
, ya que se crea la
primera
instancia, porque es parte del inicializador de una propiedad estática.
Eso es muy similar al comportamiento del método de clase
initialize
.
Si desea arreglar su Método Swizzling de manera Pure Swift :
public protocol SwizzlingInjection: class {
static func inject()
}
class SwizzlingHelper {
private static let doOnce: Any? = {
UILabel.inject()
return nil
}()
static func enableInjection() {
_ = SwizzlingHelper.doOnce
}
}
extension UIApplication {
override open var next: UIResponder? {
// Called before applicationDidFinishLaunching
SwizzlingHelper.enableInjection()
return super.next
}
}
extension UILabel: SwizzlingInjection
{
public static func inject() {
// make sure this isn''t a subclass
guard self === UILabel.self else { return }
// Do your own method_exchangeImplementations(originalMethod, swizzledMethod) here
}
}
Dado que
objc_getClassList
es Objective-C y no puede obtener la superclase (p. Ej., UILabel) sino todas las subclases, pero solo para el swizzling relacionado con UIKit, solo queremos ejecutarlo una vez en la superclase.
Simplemente ejecute inject () en cada clase de destino en lugar de for-looping todas sus clases de proyecto.
Si prefieres
Pure Swift ™!
entonces mi solución a este tipo de cosas se está ejecutando en el momento
_UIApplicationMainPreparations
para comenzar:
@UIApplicationMain
private final class OurAppDelegate: FunctionalApplicationDelegate {
// OurAppDelegate() constructs these at _UIApplicationMainPreparations time
private let allHandlers: [ApplicationDelegateHandler] = [
WindowHandler(),
FeedbackHandler(),
...
El patrón aquí es que estoy evitando el problema del delegado de aplicaciones masivas al descomponer
UIApplicationDelegate
en varios protocolos que los manejadores individuales pueden adoptar, en caso de que se lo pregunten.
Pero el punto importante es que una forma pura y rápida de llegar al trabajo lo antes posible es enviar sus tareas de tipo
+initialize
en la inicialización de su clase
@UIApplicationMain
, como la construcción de
allHandlers
aquí.
_UIApplicationMainPreparations
tiempo debe ser lo suficientemente temprano para casi cualquier persona!
También puede usar variables estáticas, ya que esas son flojas y referirlas en los inicializadores de sus objetos de nivel superior. Esto sería útil para extensiones de aplicaciones y similares que no tienen un delegado de aplicaciones.
class Foo {
static let classInit : () = {
// do your global initialization here
}()
init() {
// just reference it so that the variable is initialized
Foo.classInit
}
}