47 votes

Swift 3.1 déprécie initialize(). Comment puis-je réaliser la même chose ?

Objective-C déclare une fonction de classe, initialize() qui est exécuté une fois pour chaque classe, avant d'être utilisé. 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éprécie cette fonction avec un avertissement :

La méthode 'initialize()' définit la méthode de classe Objective-C 'initialize', qui n'est pas garantie d'être invoquée par Swift et qui sera désapprouvée dans les futures versions

Comment résoudre ce problème, tout en conservant le même comportement et les mêmes fonctionnalités que ceux que j'implémente actuellement à l'aide de la fonction initialize() point d'entrée ?

1 votes

Pour être plus précis, le méthode +initialize est exécuté avant que quoi que ce soit dans la classe soit utilisé. En réalité, elle est exécutée lorsque le paquet qui contient la classe est chargé, ce qui peut arriver après le démarrage de l'application.

0 votes

@AminNegm-Awad : Je ne suis pas sûr que ce soit correct. Peut-être pensez-vous à la load (qui n'est pas disponible en Swift) ?

1 votes

Il en est de même pour +initialize . Le runtime envoie initialize à chaque classe d'un programme juste avant que la classe, ou toute classe qui en hérite, reçoive son premier message depuis le programme. Il n'y a aucune garantie que cela soit fait au début du programme. Une telle garantie entraînerait un chargement immédiat de tous les paquets afin d'obtenir le code en +initialize . - La différence avec +load c'est-à-dire qu'il est exécuté sur des classes et les catégories . (Méthodes avec le même sélecteur dans la classe et la catégorie !) Mais, bien sûr, le fait de l'invocation tardive possible est plus important pour les catégories que pour les classes.

35voto

Jordan Points 1377

Solution facile/simple

Le point d'entrée commun d'une application est le délégué d'une application. applicationDidFinishLaunching . Nous pourrions simplement ajouter une fonction statique à chaque classe que nous voulons notifier à l'initialisation, et l'appeler d'ici.

Cette première solution est simple et facile à comprendre. Pour la plupart des cas, c'est ce que je recommanderais. Bien que la solution suivante fournisse des résultats plus proches de ceux de la solution originale, elle n'est pas recommandée. initialize() mais le temps de démarrage de l'application s'en trouve légèrement allongé. Je ne pense plus que cela vaut l'effort, la dégradation des performances ou la complexité du code dans la plupart des cas. Un code simple est un bon code.

Lisez la suite pour une autre option. Vous aurez peut-être des raisons d'en avoir besoin (ou peut-être des parties de celle-ci).


Une solution pas si simple

La première solution n'est pas forcément adaptée. Et que se passe-t-il si vous construisez un framework, où vous souhaitez que votre code s'exécute sans que personne n'ait besoin de l'appeler depuis le délégué de l'application ?

Première étape

Définissez le code Swift suivant. L'objectif est de fournir un point d'entrée simple pour toute classe que vous souhaitez doter d'un comportement similaire à celui de la classe initialize() - cela 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)

    }

}

Deuxième étape

Tout cela est bien beau, mais il nous faut encore un moyen d'exécuter la fonction que nous avons définie, c'est-à-dire 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 plateformes où UIApplication n'est pas disponible, une variation 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
    }

}

Troisième étape

Nous disposons maintenant d'un point d'entrée au démarrage de l'application et d'un moyen d'y accéder à partir des classes de votre choix. Tout ce qu'il reste à faire : au lieu d'implémenter initialize() , se conformer à SelfAware et mettre en œuvre la méthode définie, awake() .

2 votes

El élégant en Swift fonctionne en utilisant le mécanisme de l'Objective-C ?

0 votes

Il est "élégant" parce qu'il peut être autonome (aucune initialisation n'est nécessaire de la part du délégué de l'application), et parce qu'il transfère la fonction objective c vers Swift, où elle peut être utilisée en mode natif.

6 votes

Eh bien, c'est la nature de +initialize qu'il est autonome. C'est à cela qu'il sert. Toute autre approche n'est pas du tout une solution au lieu d'être une solution inélégante. Et je ne pense pas que ce soit un portage. Si c'était un portage d'Objective-C, il fonctionnerait sans Objective-C. Mais ce n'est pas le cas. Ne vous méprenez pas, j'ai upvoted votre réponse. Cependant, ce n'est pas une solution élégante, mais un hack, car il n'y a aucun moyen de faire cela en Swift.

7voto

matt Points 60113

Mon approche est essentiellement la même que celle d'adib. Voici un exemple tiré d'une application de bureau qui utilise Core Data ; le but ici est d'enregistrer notre transformateur personnalisé avant que le code ne 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 a fait, à condition que vous couvriez toutes vos bases - c'est-à-dire que vous devez implémenter chaque initialisateur. Voici un exemple de vue de texte qui configure son propre proxy d'apparence une fois avant que les instances n'aient une chance d'apparaître à 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 ne le fait le délégué d'application. Il n'y a qu'une seule instance de délégué d'application, donc le problème n'est pas très intéressant ; mais il peut y avoir plusieurs instances de CustomTextView. Néanmoins, la ligne CustomTextView.appearance().backgroundColor = .green sera exécuté une seule fois comme le premièrement est créée, car elle fait partie de l'initialisateur d'une propriété statique. Ce comportement est très similaire à celui de la méthode de la classe initialize .

0 votes

Je me demande si le compilateur optimiseur serait autorisé à no initialiser la propriété statique puisque CustomTextView.doInitialize évalue une expression inutilisée - juste une idée, je n'ai aucune idée si c'est un problème ou non.

0 votes

@MartinR Si c'était le cas, la même inquiétude ne hanterait-elle pas le modèle standard de répartition et de remplacement ?

0 votes

Dispatch_once exécute un bloc exactement une fois. Ici, la partie "une fois" est un effet secondaire de l'initialisation de la propriété statique. - Le migrateur Swift convertit dispatch_once en _ = MyClass.__once (comme par exemple ici : stackoverflow.com/q/39576848/1187415 ). L'affectation à _ être nécessaire ?

5voto

vk.edward.li Points 970

Si vous voulez réparer votre méthode Swizzling dans Pure Swift manière :

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

    }
}

Depuis le objc_getClassList est en Objective-C et il ne peut pas récupérer la superclasse (par exemple UILabel) mais seulement toutes les sous-classes, mais pour le swizzling lié à UIKit nous voulons juste l'exécuter une fois sur la superclasse. Il suffit d'exécuter inject() sur chaque classe cible au lieu de for-looping toutes les classes de votre projet.

0 votes

Même problème pour moi : objc_getClassList me dit que UILabel n'est pas conforme à mon protocole SwizzlingInjection. Seules les sous-classes (internes) de UILabel semblent se conformer à ce protocole... dommage... je ne peux pas boucler sur les classes d'exécution pour effectuer une injection automatique de swizzling sur les classes conformes, je dois injecter manuellement le swizzling pour chaque classe nécessaire. C'est pourquoi, pour moi, cette réponse devrait être la réponse acceptée ^^

3voto

adib Points 2315

Vous pouvez également utiliser des variables statiques puisque ceux-là sont déjà paresseux et les référencer dans les initialisateurs de vos objets de haut niveau. Cela serait utile pour les extensions d'applications et autres 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
    }
}

3 votes

Si vous devez les référencer dans les initialisateurs de vos objets de haut niveau. pourquoi ne pas appeler directement classInit() ? Y a-t-il un avantage ? Ce n'est pas du tout une solution autonome.

0 votes

Dans tous les cas classInit doit être interne à la classe.

0 votes

@AminNegm-Awad Appel direct classInit() le code sera exécuté chaque fois que vous créez une instance de la classe Foo . Ce n'est pas ce que nous voulons.

3voto

Scott Corscadden Points 2009

Un léger ajout à l'excellent cours de @JordanSmith qui garantit que chaque awake() n'est appelé qu'une seule 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)
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X