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 theNSApplicationMain(_:_:)
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?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With