Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSApplicationDelegate not working without Storyboard

Tags:

xcode

macos

swift

I'm attempting to create a macOS application without a storyboard in Xcode 8 (stable) on macOS Sierra. However, my AppDelegate is not even being initiated. Here's the code I have:

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    var window: NSWindow!

    override init() {
        super.init()
        print("Init")
    }

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        print("Finished launching")

        // Create a window
        window = NSWindow()

        // Add the view controller
        let viewController = ViewController()
        window.contentView?.addSubview(viewController.view)

        // Show the window
        window.makeKeyAndOrderFront(nil)
    }
}

Neither init or applicationDidFinishLaunching(_ aNotification: Notification) is being called. Any help would be much appreciated.

like image 342
Zoyt Avatar asked Oct 12 '16 20:10

Zoyt


3 Answers

You need to do a few things here

  1. Delete NSMainStoryboardFile key/value from the plist
  2. Create a NSApplication subclass and assign it to the Principal Class (NSPrincipalClass) key.

assign custom class to principal class

The name must be fully qualified with your module name.

  1. Manually instantiate your delegate in your NSApplication subclass and assign it to the delegate property.

Make sure you keep a strong reference to your delegate object. Ive just used a let here.

class GrookApplication: NSApplication {

    let strongDelegate = AppDelegate()

    override init() {
        super.init()
        self.delegate = strongDelegate
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

e.g a simple delegate.

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    override init() {
        super.init()
        print("wassup")
        //conceptual proof of life init override
        //wait until applicationDidFinishLaunching , specially for UI
    }

    var window: NSWindow!
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        print("yo! I'm alive")
        window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 200, height: 200), styleMask: .titled, backing: .buffered, defer: false)
        window.makeKeyAndOrderFront(nil)
    }

}

EDIT 2018 Verified High Sierra

Do NOT try and do window or view controller initialisation inside init this leads to double app initialisation issues and crashing. The app has not finished launching at this stage. Wait until applicationDidFinishLaunching to fire any significant operations.

like image 94
Warren Burton Avatar answered Nov 17 '22 21:11

Warren Burton


Warren Burton's accepted answer, utilising a strong reference to a @NSApplicationMain-annotated AppDelegate instance no longer works. I've confirmed it myself on OS X High Sierra, and Alex Sieroshtan commented that it didn't work back in OS X Yosemite, either. The failure point, as Tyler Durden noted, was this message:

Assertion failure in -[X.XApplication init], /Library/Caches/com.apple.xbs/Sources/AppKit/AppKit-1504.82.104/AppKit.subproj/NSApplication.m:1778
2017-04-08 13:25:35.761585+0100 X
[9073:1059806][General] An uncaught exception was raised 2017-04-08 13:25:35.761601+0100 X
[9073:1059806][General] Creating more than one Application

I struggled with this myself for a good while, but came up with two up-to-date solutions by no small amount of experimentation.

Option 1: Continue to use @NSApplicationMain via a workaround

I found that you can alter the code of the accepted answer to work around the bug. The way to do this is by not calling the super.init() method in your class named AppDelegate.

What?

Really. I think there is a too-eager assertion counting number of inits done by AppDelegate (or some logic along these lines), and thus the call to super.init() gets counted as well as the completion of the override init() block. You have two options for workarounds here:

  1. Don't call super.init(): This is actually possible and completely healthy for NSObject, at least in macOS. You lose the ability to reference self in the override init() block, however.

  2. Don't override init() at all: Consider doing your init process during a lifecycle method like applicationWillFinishLaunching(:).

I don't recommend either of these, of course.

Option 2: Give up on the @NSApplicationMain method altogether

@NSApplicationMain is just a macro which we can approximate ourselves. By some luck, I came across James H Fisher's blog post explaining how. I'll quote what matters in a moment.

If you have written @NSApplicationMain anywhere, please delete it before proceeding with these instructions.

No need to alter your Info.plist file

The key:value pair for NSPrincipalClass should keep its default value of:

<key>NSPrincipalClass</key>
<string>NSApplication</string>

Use main.swift instead of subclassing NSApplication

The file MUST be called main.swift; it's a special exception to Swift's "Expressions are not allowed at the top level" rule.

import AppKit

let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate

_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

The logic

James H Fisher explains, referencing the NSApplication documentation:

[Docs]

Every app must have exactly one instance of NSApplication (or a subclass of NSApplication). Your program’s main() function should create this instance by invoking the shared() class method.

[James]

First, main.swift runs NSApplication.shared, and assigns this NSApplication object to myApp. Notice the documentation refers to a main() function, even though in Swift there is none! The equivalent is the main.swift file.

Next, main.swift instantiates your AppDelegate class, and assigns it as the .delegate of myApp. You can now see why the default project chooses to call the class AppDelegate: it is set as the .delegate on an NSApplication.

Finally, main.swift calls the function NSApplicationMain(...)... The function NSApplicationMain(...) is the entry point for Cocoa applications. NSApplicationMain(...) never returns; instead, it sets up the UI event loop, and eventually exits using the C exit(...) function.

Additionally this StackOverflow post goes into some detail about why using sharedApplication remedies the "Creating more than one Application" bug.

... That's all you need! Hope this serves to help somebody else.

like image 22
Jamie Birch Avatar answered Nov 17 '22 23:11

Jamie Birch


In case someone is looking for a Swift version (based on @WarrenBurtons answer).

AppDelegate

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    var window: NSWindow?

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application
        window = NSWindow(contentViewController: RootViewController())
        window?.makeKeyAndOrderFront(self)
    }
}

class RootViewController: NSViewController {
    override func loadView() {
      self.view = NSView()
      self.view.frame = NSRect(x: 0, y: 0, width: 600, height: 400)
  }
}

NSApplication subclass

import Cocoa

class Application: NSApplication {
    let strongDelegate = AppDelegate()

    override init() {
        super.init()
        self.delegate = strongDelegate
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Info.plist entry

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    ...
    <key>NSPrincipalClass</key>
    <string>$(PRODUCT_MODULE_NAME).Application</string>
    ...
</dict>
</plist>

I've also created a gist for this, that I will keep up to date for new Xcode / Swift versions. https://gist.github.com/florieger/7ac5e7155f6faf18666f92f7d82f6cbc

Edit: Make sure to delete the Main.storyboard / MainMenu.xib, otherwise you might end up with two Windows in the UI Debugger.

like image 4
florieger Avatar answered Nov 17 '22 22:11

florieger