2

I am making a macOS Cocoa application which only runs in the menu bar. When I initially created the project, Xcode gave me a file called MainMenu.xib. This MainMenu.xib file does not appear to contain anything relevant to my menu bar application, so I wish to remove it.

The Cocoa entry point, the function NSApplicationMain, "loads the main nib file from the application’s main bundle". I read elsewhere that Cocoa finds "the main nib file" by consulting the key NSMainNibFile in my Info.plist, which was set to the string MainMenu. I deleted the key NSMainNibFile, and my program still behaved the same. From this, I assumed that Cocoa saw the absence of this key, and so it skipped its nib/xib loading step.

Since my MainMenu.xib file was no longer referenced from anywhere, I deleted it (which deleted some innocent-looking references from my project.pbxproj). However, after deleting MainMenu.xib, my application no longer works. The applicationDidFinishLaunching method on my AppDelegate class is not called!

This means two things. First, this means Cocoa magically still finds my MainMenu.xib file even if NSMainNibFile is not present (and Cocoa still finds the file if I rename it to Foo.xib). More importantly, this means means that something in my MainMenu.xib is still required for my application to run. Here is the complete MainMenu.xib:

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
    <dependencies>
        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11762"/>
    </dependencies>
    <objects>
        <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
            <connections>
                <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
            </connections>
        </customObject>
        <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
        <customObject id="-3" userLabel="Application" customClass="NSObject"/>
        <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Foo" customModuleProvider="target"/>
    </objects>
</document> 

I think the problem is that my MainMenu.xib refers to my AppDelegate class, and Cocoa uses this in order to instantiate the AppDelegate class, and then call applicationDidFinishLaunching on the object. This is confusing, because I thought the @NSApplicationMain annotation on my AppDelegate class was sufficient.

I have a few questions as a result of this:

  1. How is Cocoa (i.e. NSApplicationMain) finding my MainMenu.xib file? What is Cocoa's search procedure? How do I tell Cocoa to skip this loading step?
  2. What is my MainMenu.xib file doing? That is, how does Cocoa interpret the MainMenu.xib file, and what does it do as a result? Specifically, how does this file result in my AppDelegate class getting instantiated?
  3. How do I recreate this logic purely programmatically in Swift 3, so that I can delete the MainMenu.xib file? What are the APIs I need to use?
jameshfisher
  • 34,029
  • 31
  • 121
  • 167
  • Possible duplicate for one of the questions http://stackoverflow.com/questions/5237833/how-does-xcode-decide-which-xib-to-show-first – Sulthan Mar 18 '17 at 13:13
  • Also note https://developer.apple.com/reference/appkit/nsapplication which describes what `NSApplicationMain` actually does. – Sulthan Mar 18 '17 at 13:17
  • @Sulthan thanks, but not quite: that question is concerned with splitting up an .xib file, whereas I am concerned with removing the .xib file entirely. – jameshfisher Mar 18 '17 at 13:17
  • IMHO the nib does only one thing - it creates an instance of the delegate and it connects it to the `NSApp`. – Sulthan Mar 18 '17 at 13:18
  • I'm curious…why are you interested in getting rid of the XIB? – Bob Mar 18 '17 at 13:25
  • @Sulthan thanks, the `NSApplication` docs clarify some things. I think part of the problem is Swift, particularly the `@NSApplicationMain`, which is obscuring some things. – jameshfisher Mar 18 '17 at 13:34
  • @Bob mainly because I don't understand what it does and I can't see that it does anything useful for me – jameshfisher Mar 18 '17 at 13:35

2 Answers2

1

Answer to question #3:

  • Create a Swift file main.swift.
  • Replace the code in main.swift with

    import Cocoa
    
    let appDelegate = AppDelegate()
    NSApplication.shared().delegate = appDelegate
    _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
    
  • Delete @NSApplicationMain in AppDelegate.
  • Delete the key / value pair NSMainNibFile in Info.plist.
  • Delete the .xib file.
vadian
  • 274,689
  • 30
  • 353
  • 361
  • Thanks! This _almost_ works! I call `NSStatusBar.system()` in my `AppDelegate` initialization, and this fails with "Assertion failed: (CGAtomicGet(&is_initialized))". This must be because `NSApplicationMain` should be run before my `AppDelegate` is instantiated, but `NSApplicationMain` never returns, so I think I have to somehow pass a reference to `AppDelegate` in the arguments to `NSApplicationMain` so that it can instantiate it at the appropriate time ...? – jameshfisher Mar 18 '17 at 14:04
  • If you have a standard macOS project and follow exactly my suggestions (for example `main` must start with a lowercase letter) it's supposed to work because `main` is always be executed before `applicationDidFinishLaunching` which you should create your status bar in. – vadian Mar 18 '17 at 14:07
  • take this example `class AppDelegate: NSObject, NSApplicationDelegate { var statusBar: NSStatusBar! = NSStatusBar.system(); }` - it works with `@NSApplicationMain`, but with the code above it throws an assertion error. Somehow, the `@NSApplicationMain` is delaying the instantiation of `AppDelegate` until after some initialization takes place in `NSApplicationMain`. – jameshfisher Mar 18 '17 at 14:19
  • Initialize `statusBar` in `applicationDidFinishLaunching`. – vadian Mar 18 '17 at 14:21
  • that does work, but I'm particularly interested to know exactly how `@NSApplicationMain` achieves the execution sequence described ... anyway, I'm going to accept your answer (extremely helpful) and move this to a separate question. Thanks again! – jameshfisher Mar 18 '17 at 14:24
0
  1. The name of your XIB file is stored in your Info.plist. You can directly modify it by selecting your Xcode project file and clicking the Info tab, or you can make the change from the General tab, under "Main Interface."

  2. At the top level of every default MainMenu.xib is an AppDelegate object. This object is the one that receives all the app delegate messages. It's connected to the delegate outlet of the placeholder Application object.

  3. To replicate this behavior, you need to subclass NSApplication and modify your target's "Principal Class" Info.plist setting to reflect the new subclass. In your subclass, you can override something like finishLaunching() to create your app delegate object and set it up. Do keep in mind, though, when doing it this way, that you need to make sure your new delegate has a strong reference somewhere because it's no longer owned by the nib file. I recommend adding a strong property to your subclass, maybe something like strongDelegate.

Bob
  • 910
  • 1
  • 8
  • 12
  • Hi Bob! Thanks. I edited the `Info.plist` to remove the `NSMainNibFile` key, which I think you're referring to. But Cocoa still found my `.xib` file, which is why I asked for Cocoa's search procedure - it's definitely not just looking at my `Info.plist` for the answer. – jameshfisher Mar 18 '17 at 13:30