简体   繁体   中英

Swift 3.1 deprecates initialize(). How can I achieve the same thing?

Objective-C declares a class function, initialize() , that is run once for each class, before it is used. It is often used as an entry point for exchanging method implementations (swizzling), among other things.

Swift 3.1 deprecates this function with a warning:

Method 'initialize()' defines Objective-C class method 'initialize', which is not guaranteed to be invoked by Swift and will be disallowed in future versions

How can this be resolved, while still maintaining the same behaviour and features that I currently implement using the initialize() entry point?

Easy/Simple Solution

A common app entry point is an application delegate's applicationDidFinishLaunching . We could simply add a static function to each class that we want to notify on initialization, and call it from here.

This first solution is simple and easy to understand. For most cases, this is what I'd recommend. Although the next solution provides results that are more similar to the original initialize() function, it also results in slightly longer app start up times. I no longer think it is worth the effort, performance degradation, or code complexity in most cases. Simple code is good code.

Read on for another option. You may have reason to need it (or perhaps parts of it).


Not So Simple Solution

The first solution doesn't necessarily scale so well. And what if you are building a framework, where you'd like your code to run without anyone needing to call it from the application delegate?

Step One

Define the following Swift code. The purpose is to provide a simple entry point for any class that you would like to imbue with behavior akin to initialize() - this can now be done simply by conforming to SelfAware . It also provides a single function to run this behavior for every conforming class.

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)

    }

}

Step Two

That's all good and well, but we still need a way to actually run the function we defined, ie NothingToSeeHere.harmlessFunction() , on application startup. Previously, this answer suggested using the Objective-C code to do this. However, it seems that we can do what we need using only Swift. For macOS or other platforms where UIApplication is not available, a variation of the following will be needed.

extension UIApplication {

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

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

}

Step Three

We now have an entry point at application startup, and a way to hook into this from classes of your choice. All that is left to do: instead of implementing initialize() , conform to SelfAware and implement the defined method, awake() .

My approach is essentially the same as adib's. Here's an example from a desktop application that uses Core Data; the goal here is to register our custom transformer before any code mentions it:

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

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

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

    // ...
}

The nice thing is that this works for any class, just as initialize did, provided you cover all your bases — that is, you must implement every initializer. Here's an example of a text view that configures its own appearance proxy once before any instances have a chance to appear onscreen; the example is artificial but the encapsulation is extremely nice:

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

}

That demonstrates the advantage of this approach much better than the app delegate does. There is only one app delegate instance, so the problem isn't very interesting; but there can be many CustomTextView instances. Nevertheless, the line CustomTextView.appearance().backgroundColor = .green will be executed only once , as the first instance is created, because it is part of the initializer for a static property. That is very similar to the behavior of the class method initialize .

If you want to fix your Method 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

    }
}

Since the objc_getClassList is Objective-C and it cannot get the superclass (eg UILabel) but all the subclasses only, but for UIKit related swizzling we just want to run it once on the superclass. Just run inject() on each target class instead of for-looping your whole project classes.

A slight addition to @JordanSmith's excellent class which ensures that each awake() is only called once:

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

You can also use static variables since those are already lazy and refer them in your top-level objects' initializers. This would be useful for app extensions and the like which doesn't have an application delegate.

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

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

If you prefer Pure Swift™! then my solution to this kind of thing is running at _UIApplicationMainPreparations time to kick things off:

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

Pattern here is I'm avoiding the Massive Application Delegate problem by decomposing UIApplicationDelegate into various protocols that individual handlers can adopt, in case you're wondering. But the important point is that a pure-Swift way to get to work as early as possible is dispatch your +initialize type tasks in the initialization of your @UIApplicationMain class, like the construction of allHandlers here. _UIApplicationMainPreparations time ought to be early enough for pretty much anybody!

  1. Mark your class as @objc
  2. Inherit it from NSObject
  3. Add ObjC category to your class
  4. Implement initialize in category

Example

Swift files:

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

Objc files:

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

Here is a solution that does work on 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!);
    }()
}

Then on AppDelegate:

UIViewController.swizzleViewWillAppear()

Taking from the following post

Init static stored property with closure

[static stored property with closure]

One more example to execute something once using

extension MyClass {
    static let shared: MyClass = {
        //create an instance and setup it
        let myClass = MyClass(parameter: "parameter")
        myClass.initialize()//setup

        return myClass
    }()
    //() to execute the closure.

    func initialize() {
        //is called once
    }
}

//using
let myClass = MyClass.shared

I think that is a workaround way.

Also we can write initialize() function in objective-c code, then use it by bridge reference

Hope the best way.....

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM