'Display window on OSX using Swift without XCode or NIB
Disclaimer: I'm attempting the following exercise because I think it will be instructive. I'm interested in how it might be done. So please don't be too hasty to jump in with "This is the wrong way to do it, you should never do it like this!"
Working from the commandline with my favourite text editor, I would like to construct a minimal Swift program that displays a window.
It's a GUI/Cococa hello world, if you like.
In the same spirit, I want to avoid NIB.
So, No XCode, No NIB.
I would like to:
- compile it with the swift compiler
- create a #! swift script which runs using the Swift interpreter
If I can do both of those things I will feel my feet on the ground and be much more at ease upgrading to Xcode.
I tried the following:
window.swift
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet weak var window: NSWindow!
let newWindow = NSWindow(contentRect : NSScreen.mainScreen()!.frame
, styleMask : NSBorderlessWindowMask
, backing : NSBackingStoreType.Buffered
, defer : false)
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
newWindow.opaque = false
newWindow.movableByWindowBackground = true
newWindow.backgroundColor = NSColor.whiteColor()
newWindow.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}
However, attempting to run this from the command line fails:
[email protected] ~ /Users/pi/dev/macdev:
⤐ swift window.swift
window.swift:3:1: error: 'NSApplicationMain' attribute cannot be used in a
module that contains top-level code
@NSApplicationMain
^
window.swift:1:1: note: top-level code defined in this source file
import Cocoa
^
✘
What's the correct way to eliminate the error?
Solution 1:[1]
Make a file TestView.swift (like this):
import AppKit
class TestView: NSView
{
override init(frame: NSRect)
{
super.init(frame: frame)
}
required init?(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
var colorgreen = NSColor.greenColor()
override func drawRect(rect: NSRect)
{
colorgreen.setFill()
NSRectFill(self.bounds)
let h = rect.height
let w = rect.width
let color:NSColor = NSColor.yellowColor()
let drect = NSRect(x: (w * 0.25),y: (h * 0.25),width: (w * 0.5),height: (h * 0.5))
let bpath:NSBezierPath = NSBezierPath(rect: drect)
color.set()
bpath.stroke()
NSLog("drawRect has updated the view")
}
}
Make a file TestApplicationController.swift (like this):
import AppKit
final class TestApplicationController: NSObject, NSApplicationDelegate
{
/// Seems fine to create AppKit UI classes before `NSApplication` object
/// to be created starting OSX 10.10. (it was an error in OSX 10.9)
let window1 = NSWindow()
let view1 = TestView(frame: NSRect(x: 0, y: 0, width: 1000, height: 1000))
func applicationDidFinishLaunching(aNotification: NSNotification)
{
window1.setFrame(CGRect(x: 0, y: 0, width: 1000, height: 1000), display: true)
window1.contentView = view1
window1.opaque = false
window1.center();
window1.makeKeyAndOrderFront(self)
// window1.backgroundColor = view1.colorgreen
// window1.displayIfNeeded()
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}
Make a file main.swift (like this):
//
// main.swift
// CollectionView
//
// Created by Hoon H. on 2015/01/18.
// Copyright (c) 2015 Eonil. All rights reserved.
//
import AppKit
let app1 = NSApplication.sharedApplication()
let con1 = TestApplicationController()
app1.delegate = con1
app1.run()
The last file must not be renamed, main.swift is apparently a special name for swift (otherwise the example will not compile).
Now, enter this (to compile the example):
swiftc -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk TestView.swift TestApplicationController.swift main.swift
Run the code by entering:
./main
It shows a main window (centered) with a nice green collor and a yellow rectangle within it). You can kill the app by entering Control-C.
Note that this is a swift compilation not an interpreter running, so you have a native app.
Note that -sdk and the path to MacOSX10.11.sdk is essential (otherwise the code will not compile).
Note also that this compilation depends on the latest Xcode distribution, so update MacOSX10.11.sdk to MacOSX10.10.sdk or whatever is in the SDKs directory.
It took a while to find this out ...
Solution 2:[2]
Porting this code from objective-c to Swift, you get
import Cocoa
let nsapp = NSApplication.shared()
NSApp.setActivationPolicy(NSApplicationActivationPolicy.regular)
let menubar = NSMenu()
let appMenuItem = NSMenuItem()
menubar.addItem(appMenuItem)
NSApp.mainMenu = menubar
let appMenu = NSMenu()
let appName = ProcessInfo.processInfo.processName
let quitTitle = "Quit " + appName
let quitMenuItem = NSMenuItem.init(title:quitTitle,
action:#selector(NSApplication.terminate),keyEquivalent:"q")
appMenu.addItem(quitMenuItem);
appMenuItem.submenu = appMenu;
let window = NSWindow.init(contentRect:NSMakeRect(0, 0, 200, 200),
styleMask:NSTitledWindowMask,backing:NSBackingStoreType.buffered,defer:false)
window.cascadeTopLeft(from:NSMakePoint(20,20))
window.title = appName;
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps:true)
NSApp.run()
Save it as minimal.swift, compile
swiftc minimal.swift -o minimal
and run
./minimal
You will get an empty window and a menu bar with a menu named like the application and a quit button.
Why it works exactly, I don't know. I'm new to Swift and Cocoa programming, but the linked website explains a bit.
Solution 3:[3]
Based on the other solutions I wrote a recent version of a single file app.
Requirements: Swift 5.6, Command Line Tools, no XCode, no XIB, no Storyboard.
One file, one class, run it with swift app.swift
.
file: app.swift
import AppKit
class App : NSObject, NSApplicationDelegate {
let app = NSApplication.shared
let name = ProcessInfo.processInfo.processName
let status = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let window = NSWindow.init(
contentRect: NSMakeRect(0, 0, 200, 200),
styleMask: [.titled, .closable, .miniaturizable],
backing: .buffered,
defer: false
)
override init() {
super.init()
app.setActivationPolicy(.accessory)
window.center()
window.title = name
window.hidesOnDeactivate = false
window.isReleasedWhenClosed = false
let statusMenu = newMenu()
status.button?.title = "?"
status.menu = statusMenu
let appMenu = newMenu()
let sub = NSMenuItem()
sub.submenu = appMenu
app.mainMenu = NSMenu()
app.mainMenu?.addItem(sub)
}
@IBAction func activate(_ sender:Any?) {
app.setActivationPolicy(.regular)
DispatchQueue.main.async { self.window.orderFrontRegardless() }
}
@IBAction func deactivate(_ sender:Any?) {
app.setActivationPolicy(.accessory)
DispatchQueue.main.async { self.window.orderOut(self) }
}
private func newMenu(title: String = "Menu") -> NSMenu {
let menu = NSMenu(title: title)
let q = NSMenuItem.init(title: "Quit", action: #selector(app.terminate(_:)), keyEquivalent: "q")
let w = NSMenuItem.init(title: "Close", action: #selector(deactivate(_:)), keyEquivalent: "w")
let o = NSMenuItem.init(title: "Open", action: #selector(activate(_:)), keyEquivalent: "o")
for item in [o,w,q] { menu.addItem(item) }
return menu
}
func applicationDidFinishLaunching(_ n: Notification) { }
func applicationDidHide(_ n: Notification) {
app.setActivationPolicy(.accessory)
DispatchQueue.main.async { self.window.orderOut(self) }
}
}
let app = NSApplication.shared
let delegate = App()
app.delegate = delegate
app.run()
The 64 lines of code above provide the following features and fixes over the previous solutions:
- app menu is visible and usable
- status menu can be clicked and is usable
- both menus have working key keybindings
- dock item appears when window is open:
app.setActivationPolicy(.regular)
- dock item hides when window is closed:
app.setActivationPolicy(.accessory)
- window is preserved on close and reused on open:
window.hidesOnDeactivate = false window.isReleasedWhenClosed = false
Tested on M1 Pro. Here is how it looks.
Solution 4:[4]
Pass the -parse-as-library
flag to swiftc
:
swiftc -parse-as-library window.swift
That said, you'll just end up running into a second error when you do this:
2015-06-10 14:17:47.093 window[11700:10854517] No Info.plist file in application bundle or no NSPrincipalClass in the Info.plist file, exiting
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Ignis Incendio |
Solution 2 | mondaugen |
Solution 3 | Juve |
Solution 4 | Lily Ballard |