12

Creating a new Cocoa project in XCode gives me an AppDelegate.swift file which looks like this:

import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
    @IBOutlet weak var window: NSWindow!
}

The @NSApplicationMain attribute is documented here as

NSApplicationMain

Apply this attribute to a class to indicate that it is the application delegate. Using this attribute is equivalent to calling the NSApplicationMain(_:_:) function.

If you do not use this attribute, supply a main.swift file with code at the top level that calls the NSApplicationMain(_:_:) function as follows:

import AppKit
NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

The instructions in the documentation do not work: the AppDelegate class is never instantiated. In this answer, vadian suggests the following contents for main.swift, which work better than the code in the documentation:

import Cocoa
let appDelegate = AppDelegate()
NSApplication.shared().delegate = appDelegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

However, this still does not provide the same behavior as @NSApplicationMain. Consider using the above main.swift with the following AppDelegate.swift:

import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
    @IBOutlet weak var window: NSWindow!
    var foo: NSStatusBar! = NSStatusBar.system();
}

The above AppDelegate.swift works with an @NSApplicationMain annotation, but when using the above main.swift, it fails at runtime with the error

Assertion failed: (CGAtomicGet(&is_initialized)), function CGSConnectionByID, file Services/Connection/CGSConnection.c, line 127.

I think this is_initialized error means that @NSApplicationMain sets things up so that the AppDelegate is instantiated after some initialization by the NSApplicationMain function. This suggests the following main.swift, which moves the delegate initialization to after the NSApplicationMain call:

import Cocoa
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
let appDelegate = AppDelegate()
NSApplication.shared().delegate = appDelegate

However, this doesn't work either, because NSApplicationMain never returns! The above main.swift is equivalent to the broken suggestion in the documentation, because the latter two lines are dead code.

I therefore think there must be some way to pass a reference to my AppDelegate class as an argument to the NSApplicationMain function, so that Cocoa can do its initialization and then instantiate my AppDelegate class itself. However, I see no way to do this.

Is there a main.swift which provides behavior which is truly equivalent to the @NSApplicationMain annotation? If so, what does that main.swift look like? If not, what is @NSApplicationMain actually doing, and how do I modify it?

Community
  • 1
  • 1
jameshfisher
  • 34,029
  • 31
  • 121
  • 167

3 Answers3

14

The documentation assumes that there is a xib or storyboard which instantiates the AppDelegate class via an object (blue cube) in Interface Builder. In this case both

  • main.swift containing NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

and

  • @NSApplicationMain in the AppDelegate class

behave exactly the same.

If there is no xib or storyboard you are responsible to initialize the AppDelegate class, assign it to NSApplication.shared.delegate and run the app. You have also to consider the order of appearance of the objects. For example you cannot initialize objects related to AppKit before calling NSApplication.shared to launch the app.


For example with this slightly changed syntax

let app = NSApplication.shared
let appDelegate = AppDelegate()
app.delegate = appDelegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

you can initialize the status bar in AppDelegate outside ofapplicationDidFinishLaunching:

let statusItem = NSStatusBar.system().statusItem(withLength: -1)

because NSApplication.shared() to launch the app is called before initializing the AppDelegate class.

Charlton Provatas
  • 2,184
  • 25
  • 18
vadian
  • 274,689
  • 30
  • 353
  • 361
  • Aha! I was wrong in my assumption that the `NSApplicationMain` was doing the required initialization. As you realized, [`NSApplication.shared()` ](https://developer.apple.com/reference/appkit/nsapplication/1428360-shared) does the required initialization. This seems to work perfectly now. Thanks! – jameshfisher Mar 18 '17 at 15:21
7

Here is what I did in order to run application without @NSApplicationMain annotation and function NSApplicationMain(_, _) while using Storyboard with initial NSWindowController generated by Xcode application template (with slight modification related to Main Menu described below).

File: AppConfig.swift (Swift 4)

struct AppConfig {

   static var applicationClass: NSApplication.Type {
      guard let principalClassName = Bundle.main.infoDictionary?["NSPrincipalClass"] as? String else {
         fatalError("Seems like `NSPrincipalClass` is missed in `Info.plist` file.")
      }
      guard let principalClass = NSClassFromString(principalClassName) as? NSApplication.Type else {
         fatalError("Unable to create `NSApplication` class for `\(principalClassName)`")
      }
      return principalClass
   }

   static var mainStoryboard: NSStoryboard {
      guard let mainStoryboardName = Bundle.main.infoDictionary?["NSMainStoryboardFile"] as? String else {
         fatalError("Seems like `NSMainStoryboardFile` is missed in `Info.plist` file.")
      }

      let storyboard = NSStoryboard(name: NSStoryboard.Name(mainStoryboardName), bundle: Bundle.main)
      return storyboard
   }

   static var mainMenu: NSNib {
      guard let nib = NSNib(nibNamed: NSNib.Name("MainMenu"), bundle: Bundle.main) else {
         fatalError("Resource `MainMenu.xib` is not found in the bundle `\(Bundle.main.bundlePath)`")
      }
      return nib
   }

   static var mainWindowController: NSWindowController {
      guard let wc = mainStoryboard.instantiateInitialController() as? NSWindowController else {
         fatalError("Initial controller is not `NSWindowController` in storyboard `\(mainStoryboard)`")
      }
      return wc
   }
}

File main.swift (Swift 4)

// Making NSApplication instance from `NSPrincipalClass` defined in `Info.plist`
let app = AppConfig.applicationClass.shared

// Configuring application as a regular (appearing in Dock and possibly having UI)
app.setActivationPolicy(.regular)

// Loading application menu from `MainMenu.xib` file.
// This will also assign property `NSApplication.mainMenu`.
AppConfig.mainMenu.instantiate(withOwner: app, topLevelObjects: nil)

// Loading initial window controller from `NSMainStoryboardFile` defined in `Info.plist`.
// Initial window accessible via property NSWindowController.window
let windowController = AppConfig.mainWindowController
windowController.window?.makeKeyAndOrderFront(nil)

app.activate(ignoringOtherApps: true)
app.run()

Note regarding MainMenu.xib file:

Xcode application template creates storyboard with Application Scene which contains Main Menu. At the moment seems there is no way programmatically load Main Menu from Application Scene. But there is Xcode file template Main Menu, which creates MainMenu.xib file, which we can load programmatically.

Vlad
  • 6,402
  • 1
  • 60
  • 74
  • This really really helped me. NSApplicationMain and what it does "under the hood" are convenient when you don't need to do something custom, but when you do, it's a fight. This post peeks under that hood, exposing the app's window creation and entry into the message loop (which is done by simply calling NSApplicationMain(), but that doesn't return and doesn't tell you anything). – electromaggot Jun 06 '18 at 14:07
0

Replace the default Cocoa project's AppDelegate.swift with the following main.swift. The application will behave the same as before. Thus, the following code provides the semantics of the @NSApplicationMain annotation.

import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate { }
let myApp: NSApplication = NSApplication.shared()
let myDelegate: AppDelegate = AppDelegate()
myApp.delegate = myDelegate
let mainBundle: Bundle = Bundle.main
let mainNibFileBaseName: String = mainBundle.infoDictionary!["NSMainNibFile"] as! String
mainBundle.loadNibNamed(mainNibFileBaseName, owner: myApp, topLevelObjects: nil)
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

(I constructed this with much help from vadian's answer. If there are any differences in behavior between the above and the default Cocoa project application, please let me know.)

Community
  • 1
  • 1
jameshfisher
  • 34,029
  • 31
  • 121
  • 167
  • Once again, basically `@NSApplicationMain` makes the affected class the `main` entry and calls `NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)`, nothing else. Older Xcode versions have created the `main.swift` file. To use `@NSApplicationMain` you had to delete `main.swift` and put `@NSApplicationMain` above the `AppDelegate` class to get the same behavior. – vadian Mar 19 '17 at 10:19
  • Hmmm ... so the various `Bundle` things are done by the `NSApplicationMain` _function_, not the `@NSApplicationMain` _annotation_? That could be true. I'll investigate – jameshfisher Mar 19 '17 at 10:31
  • To clarify @vadian: you still mean that `@NSApplicationMain` instantiates the class it's attached to and sets it as the `delegate` of `NSApplication.shared()`, right? – jameshfisher Mar 19 '17 at 10:33
  • **No, it does not**, please read my answer again. In a standard application the *hard-coded* object (blue cube) in `MainNib` does that. If you delete the blue cube in Interface Builder `AppDelegate` doesn't get initialized implicitly. – vadian Mar 19 '17 at 10:37
  • @vadian It seems you're right! Here's a reason I'm confused: the `@NSApplicationMain` annotation is attached to a class. But it doesn't seem to matter which class it's attached to! If I create a new class `Foo` and move the annotation to there, my program still behaves the same, using my `AppDelegate` class as the delegate! This suggests you're right: the delegate class is specified by the `MainMenu.xib` file, and _not_ by the annotation. This is a strange design though! – jameshfisher Mar 19 '17 at 11:35
  • Yes, of course it doesn't matter because `@NSApplicationMain` only calls `NSApplicationMain()` which launches the application and instantiates `MainNib`. `MainNib` initializes `AppDelegate` via the blue cube, sets the delegate and the application calls the `NSApplicationDelegate` methods. It's is just a convention to use the annotation in `AppDelegate` because this class always exists in a new created project. – vadian Mar 19 '17 at 11:41
  • @vadian thanks. I now realize my initial question was wrong, and the Apple docs are correct. My delegate was not getting instantiated because I had also deleted the `NSMainNibFile` key. The `@NSApplicationMain` language feature seems to only save one line of code and makes the program much less clear; I'll choose not to use it. – jameshfisher Mar 19 '17 at 11:55
  • In 2022 this gives the error `Cannot call value of non-function type 'NSApplication'` – hippietrail Aug 14 '22 at 08:10