web-dev-qa-db-fra.com

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

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()?

39
Jordan Smith

Solution facile/simple

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). 


Pas si simple solution

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?

La première étape

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)

    }

}

Deuxième étape

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
    }

}

Troisième étape

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()

30
Jordan Smith

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.

7
matt

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.

5
vk.edward.li

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)
    }
}
2
Scott Corscadden

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
    }
}
2
adib

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!

1
Alex Curylo
  1. Marquez votre classe comme @objc
  2. Hériter de NSObject
  3. Ajoutez la catégorie ObjC à votre classe
  4. Implémenter initialize dans la catégorie

Exemple

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
0
Cy-4AH