Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 281
P i Avatar asked Jun 10 '15 17:06

P i


3 Answers

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.

like image 69
mondaugen Avatar answered Oct 23 '22 23:10

mondaugen


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 ...

like image 27
guest Avatar answered Oct 24 '22 01:10

guest


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.

Status icon and status menu Opened window App menu when window is open

like image 2
Juve Avatar answered Oct 24 '22 01:10

Juve