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:
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?
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.
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 ...
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.setActivationPolicy(.regular)
app.setActivationPolicy(.accessory)
window.hidesOnDeactivate = false
window.isReleasedWhenClosed = false
Tested on M1 Pro. Here is how it looks.
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