Skip to content

Instantly share code, notes, and snippets.

@sp1ke23
Last active June 15, 2026 21:16
Show Gist options
  • Select an option

  • Save sp1ke23/fec555e0b83b6d48722227e27825fc00 to your computer and use it in GitHub Desktop.

Select an option

Save sp1ke23/fec555e0b83b6d48722227e27825fc00 to your computer and use it in GitHub Desktop.
Minimal MainMenu.xib file - HOWTO create a minimal macOS app without Xcode
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="15702" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15702"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application"/>
</objects>
</document>
@sp1ke23

sp1ke23 commented Sep 1, 2020

Copy link
Copy Markdown
Author

MainMenu.xib with a Delegate object

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="15702" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
    <dependencies>
        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15702"/>
    </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"/>
    </objects>
</document>

@sp1ke23

sp1ke23 commented Sep 1, 2020

Copy link
Copy Markdown
Author

HOWTO compile above xib into binary plist

$ ibtool --errors --warnings --notices --output-format human-readable-text --compile MainMenu.xib.out MainMenu.xib
$ file MainMenu.xib.out
MainMenu.xib.out: Apple binary property list

@sp1ke23

sp1ke23 commented Sep 1, 2020

Copy link
Copy Markdown
Author

main.m

#import <Cocoa/Cocoa.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
    }
    assert(NSThread.isMainThread);
    return NSApplicationMain(argc, argv);
}

AppDelegate.h

#import <Cocoa/Cocoa.h>

@interface AppDelegate : NSObject <NSApplicationDelegate>

@end

AppDelegate.m

#import "AppDelegate.h"

@interface AppDelegate ()

@property (weak) IBOutlet NSWindow *window;
@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)notification {
    NSLog(@"> %s %@", __func__, notification);

    NSAppleEventManager *appleEventManager = NSAppleEventManager.sharedAppleEventManager;
    [appleEventManager
        setEventHandler:self
        andSelector:@selector(handleQuitEvent:withReplyEvent:)
        forEventClass:kCoreEventClass
        andEventID:kAEQuitApplication];
}

- (void)handleQuitEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
    NSLog(@"> %s %@ %@", __func__, event, replyEvent);
    [NSApp terminate:self];
}

- (void)applicationWillTerminate:(NSNotification *)notification {
    NSLog(@"> %s %@", __func__, notification);
}

- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag {
    NSLog(@"> %s %@ %d", __func__, sender, flag);
    return NO;
}

- (void)alertMenuItemClicked {
    NSAlert *alert = [[NSAlert alloc] init];
    alert.messageText = @"WARNING";
    alert.informativeText = @"You just clicked an alert menu item in Dock.";
    alert.alertStyle = NSAlertStyleCritical;
    [alert runModal];
}

- (nullable NSMenu *)applicationDockMenu:(NSApplication *)sender {
    NSLog(@"> %s %@", __func__, sender);
    NSMenu *menu = [[NSMenu alloc] init];
    (void) [menu addItemWithTitle:@"Alert" action:@selector(alertMenuItemClicked) keyEquivalent:@""];
    return menu;
}

@end

@sp1ke23

sp1ke23 commented Sep 1, 2020

Copy link
Copy Markdown
Author

Info.plist for foobar.app

<?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>BuildMachineOSBuild</key>
	<string>19E224g</string>
	<key>CFBundleDevelopmentRegion</key>
	<string>en</string>
	<key>CFBundleExecutable</key>
	<string>foobar</string>
	<key>CFBundleIdentifier</key>
	<string>com.example.app.foobar</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>foobar</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>1.0</string>
	<key>CFBundleSupportedPlatforms</key>
	<array>
		<string>MacOSX</string>
	</array>
	<key>CFBundleVersion</key>
	<string>1</string>
	<key>DTCompiler</key>
	<string>com.apple.compilers.llvm.clang.1_0</string>
	<key>DTPlatformBuild</key>
	<string>11C29</string>
	<key>DTPlatformVersion</key>
	<string>GM</string>
	<key>DTSDKBuild</key>
	<string>19B90</string>
	<key>DTSDKName</key>
	<string>macosx10.15</string>
	<key>DTXcode</key>
	<string>1130</string>
	<key>DTXcodeBuild</key>
	<string>11C29</string>
	<key>LSMinimumSystemVersion</key>
	<string>10.15</string>
	<key>NSHumanReadableCopyright</key>
	<string>Copyright © 2020 John Doe. All rights reserved.</string>
	<key>NSPrincipalClass</key>
	<string>NSApplication</string>
	<key>NSSupportsAutomaticTermination</key>
	<true/>
</dict>
</plist>

The NSSupportsAutomaticTermination key is used to auto hide dock pin state when last active window of your app is closed, note that the backing app still running.

@sp1ke23

sp1ke23 commented Sep 1, 2020

Copy link
Copy Markdown
Author

HOWTO compile a minimal macOS app foobar.app

$ mkdir -p foobar.app/Contents/MacOS
$ mkdir -p foobar.app/Contents/Resources/Base.lproj
$ clang -x objective-c -Wall -Wextra main.m AppDelegate.m -framework Cocoa -fobjc-arc -fobjc-weak -o foobar.app/Contents/MacOS/foobar
$ ibtool --errors --warnings --notices --output-format human-readable-text --compile foobar.app/Contents/Resources/Base.lproj/MainMenu.nib MainMenu.xib
$ cp Info.plist foobar.app/Contents
$ printf %s 'APPL????' > foobar.app/Contents/PkgInfo

$ tree foobar.app
foobar.app
└── Contents
    ├── Info.plist
    ├── MacOS
    │   └── foobar
    ├── PkgInfo
    └── Resources
        └── Base.lproj
            └── MainMenu.nib

4 directories, 4 files

Sample foobar.app run output

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.4
BuildVersion:	19E224g

$ foobar.app/Contents/MacOS/foobar
2020-09-01 15:35:53.693 foobar[46377:1181130] > -[AppDelegate applicationDidFinishLaunching:] NSConcreteNotification 0x7fc4c6e39af0 {name = NSApplicationDidFinishLaunchingNotification; object = <NSApplication: 0x7fc4c800a450>; userInfo = {
    NSApplicationLaunchIsDefaultLaunchKey = 1;
}}
2020-09-01 15:35:56.139 foobar[46377:1181130] > -[AppDelegate applicationShouldHandleReopen:hasVisibleWindows:] <NSApplication: 0x7fc4c800a450> 0
2020-09-01 15:35:57.604 foobar[46377:1181130] > -[AppDelegate applicationDockMenu:] <NSApplication: 0x7fc4c800a450>
2020-09-01 15:36:00.681 foobar[46377:1181130] > -[AppDelegate applicationShouldHandleReopen:hasVisibleWindows:] <NSApplication: 0x7fc4c800a450> 0
2020-09-01 15:36:01.098 foobar[46377:1181130] > -[AppDelegate applicationDockMenu:] <NSApplication: 0x7fc4c800a450>
2020-09-01 15:36:02.463 foobar[46377:1181130] > -[AppDelegate handleQuitEvent:withReplyEvent:] <NSAppleEventDescriptor: 'aevt'\'quit'{  }> <NSAppleEventDescriptor: null()>

@sp1ke23

sp1ke23 commented Sep 1, 2020

Copy link
Copy Markdown
Author

[1] 46578 killed foobar.app/Contents/MacOS/foobar

$ lldb foobar.app/Contents/MacOS/foobar
(lldb) target create "foobar.app/Contents/MacOS/foobar"
Current executable set to 'foobar.app/Contents/MacOS/foobar' (x86_64).
(lldb) run
Process 46578 launched: '/private/tmp/t/foobar.app/Contents/MacOS/foobar' (x86_64)
2020-09-01 15:41:00.898274+0800 foobar[46578:1184250] > -[AppDelegate applicationDidFinishLaunching:] NSConcreteNotification 0x100429900 {name = NSApplicationDidFinishLaunchingNotification; object = <NSApplication: 0x1004148c0>; userInfo = {
    NSApplicationLaunchIsDefaultLaunchKey = 1;
}}
2020-09-01 15:41:02.667148+0800 foobar[46578:1184250] > -[AppDelegate applicationShouldHandleReopen:hasVisibleWindows:] <NSApplication: 0x1004148c0> 0
2020-09-01 15:41:03.123983+0800 foobar[46578:1184250] > -[AppDelegate applicationDockMenu:] <NSApplication: 0x1004148c0>
2020-09-01 15:41:04.069800+0800 foobar[46578:1184250] > -[AppDelegate applicationShouldHandleReopen:hasVisibleWindows:] <NSApplication: 0x1004148c0> 0
2020-09-01 15:41:04.208703+0800 foobar[46578:1184250] > -[AppDelegate applicationShouldHandleReopen:hasVisibleWindows:] <NSApplication: 0x1004148c0> 0
2020-09-01 15:41:04.368730+0800 foobar[46578:1184250] > -[AppDelegate applicationShouldHandleReopen:hasVisibleWindows:] <NSApplication: 0x1004148c0> 0
2020-09-01 15:41:04.520342+0800 foobar[46578:1184250] > -[AppDelegate applicationShouldHandleReopen:hasVisibleWindows:] <NSApplication: 0x1004148c0> 0
2020-09-01 15:41:05.335217+0800 foobar[46578:1184250] > -[AppDelegate applicationDockMenu:] <NSApplication: 0x1004148c0>
2020-09-01 15:41:08.501825+0800 foobar[46578:1184250] > -[AppDelegate applicationShouldHandleReopen:hasVisibleWindows:] <NSApplication: 0x1004148c0> 0
2020-09-01 15:41:09.110916+0800 foobar[46578:1184250] > -[AppDelegate applicationDockMenu:] <NSApplication: 0x1004148c0>
Process 46578 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGKILL
    frame #0: 0x00007fff68b0c11a libsystem_kernel.dylib`mach_msg_trap + 10
libsystem_kernel.dylib`mach_msg_trap:
->  0x7fff68b0c11a <+10>: retq
    0x7fff68b0c11b <+11>: nop

libsystem_kernel.dylib`mach_msg_overwrite_trap:
    0x7fff68b0c11c <+0>:  movq   %rcx, %r10
    0x7fff68b0c11f <+3>:  movl   $0x1000020, %eax          ; imm = 0x1000020
Target 0: (foobar) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGKILL
  * frame #0: 0x00007fff68b0c11a libsystem_kernel.dylib`mach_msg_trap + 10
    frame #1: 0x00007fff68b0c490 libsystem_kernel.dylib`mach_msg + 60
    frame #2: 0x00007fff2d6cf4e5 CoreFoundation`__CFRunLoopServiceMachPort + 247
    frame #3: 0x00007fff2d6cdfb2 CoreFoundation`__CFRunLoopRun + 1319
    frame #4: 0x00007fff2d6cd42e CoreFoundation`CFRunLoopRunSpecific + 462
    frame #5: 0x00007fff2c314c0d HIToolbox`RunCurrentEventLoopInMode + 292
    frame #6: 0x00007fff2c314925 HIToolbox`ReceiveNextEventCommon + 584
    frame #7: 0x00007fff2c3146c9 HIToolbox`_BlockUntilNextEventMatchingListInModeWithFilter + 64
    frame #8: 0x00007fff2a972509 AppKit`_DPSNextEvent + 883
    frame #9: 0x00007fff2a970d50 AppKit`-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 1352
    frame #10: 0x00007fff2a962a5e AppKit`-[NSApplication run] + 658
    frame #11: 0x00007fff2a934876 AppKit`NSApplicationMain + 777
    frame #12: 0x0000000100000eaf foobar`main + 47
    frame #13: 0x00007fff689cacc9 libdyld.dylib`start + 1
    frame #14: 0x00007fff689cacc9 libdyld.dylib`start + 1
(lldb) ^D

Solution

47,50d46
< 	<key>NSSupportsSuddenTermination</key>
< 	<true/>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment