Objective-C déclare une fonction de classe, initialize()
, qui est exécutée une fois pour chaque classe, avant son utilisation. Il est souvent utilisé comme point d'entrée pour échanger des implémentations de méthodes (swizzling), entre autres choses.
Swift 3.1 déconseille cette fonction avec un avertissement:
La méthode 'initialize ()' définit la méthode de classe Objective-C 'initialize', ce qui n’est pas garanti d’être invoqué par Swift et sera refusé dans les futures versions
Comment résoudre ce problème tout en conservant le même comportement et les mêmes fonctionnalités que celles que j'implémente actuellement à l'aide du point d'entrée initialize()
?
Un point d'entrée d'application commun est la variable applicationDidFinishLaunching
d'un délégué d'application. Nous pourrions simplement ajouter une fonction statique à chaque classe que nous voulons notifier lors de l'initialisation et l'appeler à partir d'ici.
Cette première solution est simple et facile à comprendre. Dans la plupart des cas, voici ce que je recommanderais. Bien que la solution suivante fournisse des résultats plus similaires à la fonction initialize()
d'origine, les délais de démarrage de l'application sont légèrement plus longs. Je ne pense plus que cela vaut la peine, la dégradation des performances ou la complexité du code dans la plupart des cas. Le code simple est un bon code.
Lisez la suite pour une autre option. Vous pouvez avoir des raisons d’en avoir besoin (ou même d’en faire partie).
La première solution ne s'adapte pas nécessairement si bien. Et si vous construisez un framework, vous souhaitez que votre code soit exécuté sans que quiconque ait besoin de l'appeler depuis le délégué de l'application?
Définissez le code Swift suivant. Le but est de fournir un point d’entrée simple à toute classe que vous souhaitez imbiber d’un comportement semblable à initialize()
- ceci peut maintenant être fait simplement en se conformant à SelfAware
. Il fournit également une fonction unique pour exécuter ce comportement pour chaque classe 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)
}
}
C’est très bien, mais nous avons toujours besoin d’un moyen d’exécuter la fonction que nous avons définie, à savoir NothingToSeeHere.harmlessFunction()
, au démarrage de l’application. Auparavant, cette réponse suggérait d'utiliser le code Objective-C pour ce faire. Cependant, il semble que nous puissions faire ce dont nous avons besoin en utilisant uniquement Swift. Pour macOS ou d'autres plates-formes où UIApplication n'est pas disponible, une variante de ce qui suit sera nécessaire.
extension UIApplication {
private static let runOnce: Void = {
NothingToSeeHere.harmlessFunction()
}()
override open var next: UIResponder? {
// Called before applicationDidFinishLaunching
UIApplication.runOnce
return super.next
}
}
Nous avons maintenant un point d’entrée au démarrage de l’application et un moyen d’y intégrer les classes de votre choix. Tout ce qui reste à faire: au lieu d'implémenter initialize()
, conformez-vous à SelfAware
et implémentez la méthode définie, awake()
.
Mon approche est essentiellement la même que celle d'Adib. Voici un exemple d'application de bureau qui utilise Core Data; le but ici est d’enregistrer notre transformateur personnalisé avant qu’un code le mentionne:
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
override init() {
super.init()
AppDelegate.doInitialize
}
static let doInitialize : Void = {
// set up transformer
ValueTransformer.setValueTransformer(DateToDayOfWeekTransformer(), forName: .DateToDayOfWeekTransformer)
}()
// ...
}
Ce qui est bien, c’est que cela fonctionne pour n’importe quelle classe, tout comme initialize
, à condition de couvrir toutes vos bases - c’est-à-dire que vous devez implémenter chaque initialiseur. Voici un exemple d'une vue de texte qui configure son propre proxy d'apparence une fois avant que toute instance ait la possibilité de s'afficher à l'écran. l'exemple est artificiel mais l'encapsulation est extrêmement agréable:
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
}()
}
Cela démontre l'avantage de cette approche bien mieux que le délégué de l'application. Il n'y a qu'une seule instance de délégué d'application, le problème n'est donc pas très intéressant. mais il peut y avoir beaucoup d'instances CustomTextView. Néanmoins, la ligne CustomTextView.appearance().backgroundColor = .green
sera exécutée une seule fois, car l'instance first est créée, car elle fait partie de l'initialiseur d'une propriété statique. Cela ressemble beaucoup au comportement de la méthode de classe initialize
.
Si vous souhaitez réparer votre méthode Swizzling in Pure Swift way:
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
}
}
Étant donné que le objc_getClassList
est Objective-C et qu'il ne peut pas obtenir la superclasse (par exemple UILabel), mais uniquement les sous-classes, nous souhaitons simplement l'exécuter une fois dans la superclasse. Il suffit d’exécuter inject () sur chaque classe cible au lieu de boucler l’ensemble des classes de votre projet.
Légère addition à l'excellente classe de @ JordanSmith qui garantit que chaque awake()
n'est appelée qu'une fois:
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)
}
}
Vous pouvez également utiliser des variables statiques puisque celles-ci sont déjà fainéantes et les référencer dans les initialiseurs de vos objets de niveau supérieur. Cela serait utile pour les extensions d'applications et similaires qui n'ont pas de délégué d'application.
class Foo {
static let classInit : () = {
// do your global initialization here
}()
init() {
// just reference it so that the variable is initialized
Foo.classInit
}
}
Si vous préférez Pure Swift ™! alors ma solution à ce genre de problème est à courir à _UIApplicationMainPreparations
moment pour lancer les choses:
@UIApplicationMain
private final class OurAppDelegate: FunctionalApplicationDelegate {
// OurAppDelegate() constructs these at _UIApplicationMainPreparations time
private let allHandlers: [ApplicationDelegateHandler] = [
WindowHandler(),
FeedbackHandler(),
...
En règle générale, j'évite le problème du délégué d'application massive en décomposant UIApplicationDelegate
en divers protocoles que des gestionnaires individuels peuvent adopter, au cas où vous vous poseriez la question. Mais le point important est qu’un moyen purement Swift d’arriver au travail le plus tôt possible est d’envoyer vos tâches de type +initialize
à l’initialisation de votre classe @UIApplicationMain
, comme la construction de allHandlers
ici. _UIApplicationMainPreparations
temps devrait être assez tôt pour à peu près tout le monde!
@objc
NSObject
initialize
dans la catégorieExemple
Fichiers rapides:
//MyClass.Swift
@objc class MyClass : NSObject
{
}
Fichiers 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