wake-up-neo.net

Swift 3.1 lehnt initialize () ab. Wie kann ich dasselbe erreichen?

Objective-C deklariert eine Klassenfunktion initialize(), die vor ihrer Verwendung einmal für jede Klasse ausgeführt wird. Es wird häufig als Einstiegspunkt für den Austausch von Methodenimplementierungen (Swizzling) verwendet.

In Swift 3.1 wird diese Funktion mit einer Warnung verworfen: 

Die Methode 'initialize ()' definiert die Objective-C-Klassenmethode 'initialize', Dies ist nicht garantiert von Swift aufgerufen und wird nicht zugelassen in zukünftigen Versionen

Wie kann dies gelöst werden, während das gleiche Verhalten und die gleichen Funktionen beibehalten werden, die ich derzeit mit dem Einstiegspunkt initialize() implementiere?

39
Jordan Smith

Einfache/einfache Lösung

Ein allgemeiner Einstiegspunkt für Apps ist applicationDidFinishLaunching eines Anwendungsdelegierten. Wir könnten jeder Klasse, die wir bei der Initialisierung benachrichtigen möchten, einfach eine statische Funktion hinzufügen und sie von hier aus aufrufen. 

Diese erste Lösung ist einfach und leicht verständlich. In den meisten Fällen würde ich das empfehlen. Die nächste Lösung liefert zwar Ergebnisse, die der ursprünglichen initialize()-Funktion ähnlicher sind, führt jedoch auch zu etwas längeren Startzeiten der App. Ich denke nicht mehr, dass es in den meisten Fällen den Aufwand, die Leistungseinbußen oder die Komplexität des Codes wert ist. Einfacher Code ist guter Code.

Lesen Sie weiter für eine andere Option. Möglicherweise haben Sie einen Grund, es zu brauchen (oder vielleicht Teile davon). 


Nicht so einfache Lösung

Die erste Lösung lässt sich nicht unbedingt gut skalieren. Und was ist, wenn Sie ein Framework erstellen, in dem Ihr Code ausgeführt werden soll, ohne dass jemand ihn vom Anwendungsdelegierten anrufen muss?

Schritt eins

Definieren Sie den folgenden Swift-Code. Der Zweck besteht darin, einen einfachen Einstiegspunkt für jede Klasse bereitzustellen, die Sie mit einem Verhalten ähnlich wie initialize() durchdringen möchten - dies kann jetzt einfach durch Anpassung an SelfAware durchgeführt werden. Es bietet auch eine einzige Funktion, um dieses Verhalten für jede konforme Klasse auszuführen.

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)

    }

}

Schritt zwei

Das ist alles gut und gut, aber wir brauchen immer noch eine Möglichkeit, die von uns definierte Funktion, d. H. NothingToSeeHere.harmlessFunction(), beim Start der Anwendung auszuführen. Zuvor schlug diese Antwort vor, dafür Objective-C-Code zu verwenden. Es scheint jedoch, dass wir nur mit Swift das tun können, was wir brauchen. Für MacOS oder andere Plattformen, auf denen UIApplication nicht verfügbar ist, ist eine Variation der folgenden erforderlich.

extension UIApplication {

    private static let runOnce: Void = {
        NothingToSeeHere.harmlessFunction()
    }()

    override open var next: UIResponder? {
        // Called before applicationDidFinishLaunching
        UIApplication.runOnce
        return super.next
    }

}

Schritt drei

Wir haben jetzt einen Einstiegspunkt beim Start der Anwendung und eine Möglichkeit, sich von Klassen Ihrer Wahl darauf einzuhaken. Alles, was noch zu tun bleibt: Anstatt initialize() zu implementieren, passen Sie sich an SelfAware an und implementieren die definierte Methode awake()

30
Jordan Smith

Mein Ansatz ist im Wesentlichen der gleiche wie bei Adib. Hier ist ein Beispiel aus einer Desktopanwendung, die Core Data verwendet. Das Ziel hier ist, unseren benutzerdefinierten Transformator zu registrieren, bevor er in einem Code erwähnt wird:

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    override init() {
        super.init()
        AppDelegate.doInitialize
    }

    static let doInitialize : Void = {
        // set up transformer
        ValueTransformer.setValueTransformer(DateToDayOfWeekTransformer(), forName: .DateToDayOfWeekTransformer)
    }()

    // ...
}

Das Schöne daran ist, dass dies für jede Klasse funktioniert, genau wie initialize, vorausgesetzt, Sie decken alle Ihre Grundlagen ab - das heißt, Sie müssen jeden Initialisierer implementieren. Hier ist ein Beispiel für eine Textansicht, in der der eigene Darstellungs-Proxy einmal konfiguriert wird, bevor eine Instanz auf dem Bildschirm angezeigt wird. Das Beispiel ist künstlich, aber die Verkapselung ist extrem schön:

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

}

Dies zeigt den Vorteil dieses Ansatzes viel besser als der App-Delegierte. Es gibt nur eine App-Delegateninstanz, daher ist das Problem nicht sehr interessant. Es können jedoch viele CustomTextView-Instanzen vorhanden sein. Trotzdem wird die Zeile CustomTextView.appearance().backgroundColor = .greennur einmal ausgeführt, wenn die Instanz first erstellt wird, da sie Teil des Initialisierers für eine statische Eigenschaft ist. Das ist dem Verhalten der Klassenmethode initialize sehr ähnlich.

7
matt

Wenn Sie Ihre Methode in Pure Swift way fixieren wollen:

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

    }
}

Da objc_getClassList Objective-C ist und nicht die Oberklasse (z. B. UILabel) erhalten kann, sondern alle Unterklassen, aber für UIKit-bezogene Swizzlings wollen wir es nur einmal in der Oberklasse ausführen. Führen Sie einfach inject () für jede Zielklasse aus, anstatt Ihre gesamten Projektklassen zu schleifen.

5
vk.edward.li

Leichte Ergänzung der exzellenten Klasse von @ JordanSmith, die sicherstellt, dass jede awake() nur einmal aufgerufen wird:

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

Sie können auch statische Variablen verwenden, da die bereits faul sind und sie in den Initialisierern Ihrer Objekte auf oberster Ebene referenzieren. Dies wäre nützlich für App-Erweiterungen und Ähnliches, die keinen Anwendungsdelegierten haben.

class Foo {
    static let classInit : () = {
        // do your global initialization here
    }()

    init() {
        // just reference it so that the variable is initialized
        Foo.classInit
    }
}
2
adib

Wenn Sie Pure Swift ™ bevorzugen! dann läuft meine Lösung für diese Art von Dinge zum _UIApplicationMainPreparations-Zeitpunkt, um die Dinge loszulegen:

@UIApplicationMain
private final class OurAppDelegate: FunctionalApplicationDelegate {
    // OurAppDelegate() constructs these at _UIApplicationMainPreparations time
    private let allHandlers: [ApplicationDelegateHandler] = [
        WindowHandler(),
        FeedbackHandler(),
        ...

Das Muster hier ist: Ich vermeide das Problem der massiven Anwendungsdelegierung, indem ich UIApplicationDelegate in verschiedene Protokolle zerlegte, die von einzelnen Handlern übernommen werden können, falls Sie sich fragen. Der entscheidende Punkt ist jedoch, dass ein möglichst schneller Weg zur Arbeit so früh wie möglich ist, wenn Sie Ihre +initialize-Typaufgaben in der Initialisierung Ihrer @UIApplicationMain-Klasse abschicken, wie hier die Konstruktion von allHandlers. _UIApplicationMainPreparations Zeit sollte für so ziemlich alle recht früh sein!

1
Alex Curylo
  1. Markieren Sie Ihre Klasse als @objc
  2. Erben es von NSObject
  3. Fügen Sie Ihrer Klasse die Kategorie ObjC hinzu
  4. Implementieren Sie initialize in der Kategorie

Beispiel

Swift-Dateien:

//MyClass.Swift
@objc class MyClass : NSObject
{
}

Objektdateien:

//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

Hier ist eine Lösung, die funktioniert 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!);
    }()
}

Dann auf AppDelegate:

UIViewController.swizzleViewWillAppear()

Aus folgendem Beitrag

0
Tomer