18

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. Its use was deprecated in Swift 3.1.

This is what I used to do:

extension NSView {
    public override class func initialize() {
        // This is called on class init and before `applicationDidFinishLaunching`
    }
}

How can I achieve the same thing without initialize?

I need it for a framework, so requiring calling something in the AppDelegate is a no-go. I need it called before applicationDidFinishLaunching.

I really like this solution. It's exactly what I'm looking for, but it's for iOS. I need it for macOS. Could someone suggest a macOS version of that?

To be specific, I need the equivalent of this, but for macOS:

extension UIApplication {
    private static let runOnce: Void = {
        // This is called before `applicationDidFinishLaunching`
    }()

    override open var next: UIResponder? {
        UIApplication.runOnce
        return super.next
    }
}

I've tried overriding various properties on NSApplication with no success.

The solution needs to be in pure Swift. No Objective-C.

jscs
  • 63,694
  • 13
  • 151
  • 195
Sindre Sorhus
  • 62,972
  • 39
  • 168
  • 232
  • 2
    How about replacing `UIApplication` by `NSApplication` in the code you refer to? – Cristik Feb 28 '18 at 12:28
  • @Cristik `NSApplication` has no `next` instance variable. – Sindre Sorhus Feb 28 '18 at 13:01
  • @MartinR I've explicitly mentioned I need a solution that doesn't require calling something in AppDelegate. – Sindre Sorhus Feb 28 '18 at 13:02
  • 2
    @SindreSorhus maybe you can give us some concrete example of what you try to achieve. Otherwise the question is kinda broad. – Cristik Feb 28 '18 at 13:34
  • @MartinR As I've mentioned multiple times now, I don't have access to the AppDelegate. – Sindre Sorhus Feb 28 '18 at 17:47
  • @Cristik I've tried to make it clearer. – Sindre Sorhus Feb 28 '18 at 17:56
  • @SindreSorhus what kind of stuff do you need to do in the pseudo-initialize method? You might be able to take advantage of lazy stuff like static properties – Cristik Feb 28 '18 at 18:10
  • 1
    You say you need it called before `applicationDidFinishLaunching`, however Objective-C's `initialized` is not guaranteed to be called before `applicationDidFinishLaunching`, so perhaps you're looking for something else. – Cristik Mar 04 '18 at 07:08
  • 6
    The solutions you've provided don't quite do what you're asking. They happen to work by luck and are not robust (you're betting, for instance, that an `NSView` is created before the application finishes launching; common, but not promised). This problem is easily solved without any trickery if you have control of the NIB files or `NSApplicationMain`. It's easily solved with an ObjC file (without even requiring an Objective-C bridge; you can literally just drop an ObjC file in the project). It's not clear why you've taken all the trivial approaches off the table and what your constraints are. – Rob Napier Mar 04 '18 at 15:38
  • 3
    @SindreSorhus quick question - do you actually need an identical `initialize()` behaviour, or do you just need a way to execute code before `appDidFinishLaunching`? – Cristik Mar 04 '18 at 16:29
  • @Cristik I need it executed before `appDidFinishLaunching`. – Sindre Sorhus Mar 05 '18 at 04:04

3 Answers3

6

EDIT: Since I wrote this answer, the OP has added "pure Swift" to the question in an edit. However, I am leaving this answer here because it remains the only correct way to do this at the time of this writing. Hopefully, module initialization hooks will be added in Swift 6 or 7 or 8, but as of March 2018, pure Swift is the wrong tool for this use case.

Original answer follows:

Unfortunately, Swift doesn't have any direct equivalent to the old initialize() and load() methods, so this can't be done in pure Swift AFAIK. However, if you're not averse to mixing a small amount of Objective-C into your project, this isn't hard to do. Simply make a Swift class that's fully exposed to Objective-C:

class MyInitThingy: NSObject {
    @objc static func appWillLaunch(_: Notification) {
        print("App Will Launch")
    }
}

Then add this short Objective-C file to the project:

#import <Cocoa/Cocoa.h>

static void __attribute__ ((constructor)) Initer() {
    // Replace "MyFrameworkName" with your framework's module name.

    // You can also just #import <MyFrameworkName/MyFrameworkName-Swift.h>
    // and then access the class directly, but that requires the class to
    // be public, which pollutes the framework's external interface.
    // If we just look up the class and selector via the Objective-C runtime,
    // we can keep everything internal.

    Class class = NSClassFromString(@"MyFrameworkName.MyInitThingy");
    SEL selector = NSSelectorFromString(@"appWillLaunch:");

    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];

    [center addObserver:class
               selector:selector
                   name:NSApplicationWillFinishLaunchingNotification
                 object:nil];
}

With these two pieces of code in place, an app that links against your framework should get "App Will Launch" logged to the console sometime before applicationDidFinishLaunching is called.

Alternatively, if you already have a public ObjC-visible class in your module, you can do this without having to use the runtime functions, via a category:

public class SomeClass: NSObject {
    @objc static func appWillLaunch(_: Notification) {
        print("App Will Launch")
    }
}

and:

#import <Cocoa/Cocoa.h>
#import <MyFrameworkName/MyFrameworkName-Swift.h>

@interface SomeClass (InternalSwiftMethods)

+ (void)appWillLaunch:(NSNotification *)notification;

@end

@implementation SomeClass (FrameworkInitialization)

+ (void)load {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];

    [center addObserver:self
               selector:@selector(appWillLaunch:)
                   name:NSApplicationWillFinishLaunchingNotification
                 object:nil];
}

@end
Charles Srstka
  • 16,665
  • 3
  • 34
  • 60
  • I appreciate the effort, but I'm looking for a pure Swift solution. I've already linked to a pure Swift solution in the question, but it only works for iOS, so I'm looking for a macOS adaption of that. – Sindre Sorhus Mar 03 '18 at 11:23
  • 2
    @SindreSorhus The hack in the linked solution is not advisable, for two reasons: 1) If two separate frameworks try to override `nextResponder` on `NSApplication`, one of them will get stomped on and it's undefined which one it will be, and 2) if `NSApplication` gets its own override of `nextResponder` in some future version of AppKit, this will clobber that and cause who knows what amount of undefined behavior. I am sorry, but there is no pure-Swift solution to this problem that is not actively harmful. – Charles Srstka Mar 03 '18 at 15:07
  • @CharlesSrstka there is a slight difference between your solution and the `initialize` behaviour: `initialized` gets called right before the first message is sent to the class, while the constructor/load approach executes the code as soon as the binary is loaded into memory. The difference might not matter though, as the OP seems to want some code to get executed at startup. – Cristik Mar 04 '18 at 09:35
  • @Cristik I interpreted the "before `applicationDidFinishLaunching`" requirement literally; the `applicationWillFinishLaunching` notification should be fired shortly before `applicationDidFinishLaunching`. If OP wants to call the code right at launch time, the method could also be called immediately via `performSelector:` rather than going through `NSNotificationCenter`. – Charles Srstka Mar 04 '18 at 16:19
  • @CharlesSrstka yes, this how that phrase should be interpreted, my only thoughts were why is the OP interested of `initialize`? The `load`-like solution you posted should provide the "before didFinishLaunch" requirement. – Cristik Mar 04 '18 at 16:22
  • @Cristik Only OP can answer that. – Charles Srstka Mar 04 '18 at 16:23
  • @CharlesSrstka let's find out, I asked the OP about this :) – Cristik Mar 04 '18 at 16:29
6

Nope, a Swift alternative to initialize() doesn't exist, mainly because Swift is statically dispatched, so method calls can't be intercepted. And is really no longer needed, as the method was commonly used to initialize static variables from Objective-C files, and in Swift static/global variables are always lazy and can be initialized with the result of an expression (thing not possible in Objective-C).

Even if it would be possible to achieve something similar, I would discourage you to implicitly run stuff without the knowledge of the framework users. I'd recommend to add a configure method somewhere in the library and ask the users of your library to call it. This way they have control over the initialization point. There are only a few things worser than having a framework that I linked against, but no longer (or not yet) using, to start executing code without my consent.

It's just saner to give framework users control over when they want the framework code to start. If indeed your code must run at the very beginning of the application lifecycle, then ask the framework users to make the call to your framework entry point before any other calls. It's also in their interest for your framework to be properly configured.

E.g.:

MyFramework.configure(with: ...)
// or
MyManager.start()
Cristik
  • 30,989
  • 25
  • 91
  • 127
2

As pointed out previously by others it is neither possible (nor good programming) to do what you ask for in a Framework in Swift. Achieving the functionality from the application itself (where this sort of behaviour belongs) is fairly simple though - no need to mess with notifications or selectors. You simply override the init of your NSApplicationDelegate (or UIApplicationDelegate) and set up your class initializer there:

class AppDelegate: NSObject, NSApplicationDelegate {

    override init() {
        super.init()
        YourClass.initializeClass()
    }
}

And the corresponding static function:

class YourClass {
    static func initializeClass() {
        // do stuff
    }
}

This will achieve the same functionality as initialize().

Oskar
  • 3,625
  • 2
  • 29
  • 37