loading...

Creating a global configurable shortcut for MacOS apps in Swift

mitchartemis profile image Mitch Stanley Updated on ・10 min read

I recently released my first MacOS app and after a lot of trial and error, discovered that there are not enough Swift for MacOS tutorials! Consider this my first contribution to the cause 🙂.

Shameless plug: This tutorial came about from my work on Snipbar, a MacOS app I’ve been working on for my shell command app Snipline. If you work with shell commands and servers, or SQL then I’d love it if you checked it out.

By the end of this tutorial you will have

  • Set up an app with two windows: a Main window and Preferences window
  • Created a button from the Main window that links to the Preferences window.
  • Installed and configured HotKey via Carthage.
  • Set up a simple UI for configuring a global keyboard shortcut that opens your Main window.

Here’s a preview of how the app works

The finished app

This tutorial uses Xcode 10.2 and Swift 5.

Creating the app windows

First things first, let’s create a new Mac project in Xcode, I called mine GlobalConfigKeybind. We need to make sure “MacOS” and “Cocoa App” are selected. In the second panel, make sure “Use Storyboards” is selected.

With the app created we need to create the Preferences window. We can do this by going to Main.storyboard, clicking the Library button, searching for Window View Controller and then drag a new Window Controller next to our Main window.

Selecting the Window Controller from the Library

The layout of the two window view controllers

Linking the main app window to the preferences window

Let’s create a button on the Main view controller and set it up so that when it’s pressed it shows the preferences window.

Press the library button, search for Push Button and drag it into the Main view controller.

Select the button and go to the Attributes inspector. Change the title to say Preferences.

How the Main Window looks

Now we have the button but we need to make it do something. Hold ctrl while click and dragging from the button to the new Preferences window. The Preferences window will become highlighted. Release the mouse and select the Show action segue.

Now when clicked the button will open the Preferences window. Before we test it though, let’s make sure that when the button is pressed more than once it only opens one window. Click on the Window and in the Attributes inspector change Presentation to Single.

At this point, if we run the app and press the Preferences button the new window will show. Hooray!

Installing HotKey

HotKey is a Swift package that wraps around the Carbon API for dealing with global hot keys.

We’ll use Carthage to install it but If you prefer SPM or CocoaPods feel free to use that instead.

First, make sure you have Carthage installed, following their installation instructions if needed. Then in Xcode, create a new Empty file and call it Cartfile. Make sure it’s in the base of the project, if you’ve accidentally saved it in the wrong place, make sure to drag it below the GlobalConfigKeybind area with the blue page icon.

Inside that file add the following and save it.

github "soffes/HotKey"

Location of the Cartfile

We need to install HotKey from the Terminal. To do this go to the project directory and run carthage update && carthage build --platform MacOS.

Back in Xcode link the new HotKey binary to our app.

Click the GlobalConfigKeybind with the blue page icon, select the app Target and click the + icon under Embedded Binaries. Click Add Other and navigate to the root directory of your project, then go to Carthage > Build > Mac > highlight HotKey.framework and click Open.

When prompted select Copy items if needed.

Embedding HotKey binary

Creating the Keybind options interface.

In the Main.storyboard drag two Push Buttons onto the Preferences window. Give the second button a title of Clear, set the State to disabled. For the first button set the title to "Set Shortcut" in the attributes inspector.

Adding the key bind configuration functionality

We need to create three new Cocoa class files. Make sure that XIB is not being created.

  • PreferencesViewController which needs to be a subclass of NSViewController.
  • PreferencesWindowController which needs to be a subclass of NSWindowController.
  • MainWindow which needs to be a subclass of NSWindow.

Once made, set the class for each in the Main.storyboard. One for the Preferences Window and one for the Preferences View Controller.

The MainWindow needs to be set on the first app window. This will be used later on when we need to target the window to bring to the front. Notice in the below screenshot that Window is highlighted, not Window Controller.

Inside of PreferencesWindowController add the following code:

//
//  PreferencesWindowController.swift
//  GlobalConfigKeybind
//
//  Created by Mitch Stanley on 27/01/2019.
//

import Cocoa

class PreferencesWindowController: NSWindowController {

    override func windowDidLoad() {
        super.windowDidLoad()
    }

    override func keyDown(with event: NSEvent) {
        super.keyDown(with: event)
        if let vc = self.contentViewController as? PreferencesViewController {
            if vc.listening {
                vc.updateGlobalShortcut(event)
            }
        }
    }
}

The keyDown:with method triggers any time a key is pressed while this window is active. We only want to do this when the configuration button is pressed so we use an if statement that checks the listening state in the Preferences View Controller (We’ll go into more detail on this shortly).

Inside of PreferencesViewController add this code.

//
//  PreferencesWindowController.swift
//  GlobalConfigKeybind
//
//  Created by Mitch Stanley on 27/01/2019.
//

import Cocoa
import HotKey
import Carbon

class PreferencesViewController: NSViewController {

    @IBOutlet weak var clearButton: NSButton!
    @IBOutlet weak var shortcutButton: NSButton!

    // When this boolean is true we will allow the user to set a new keybind.
    // We'll also trigger the button to highlight blue so the user sees feedback and knows the button is now active.
    var listening = false {
        didSet {
            if listening {
                DispatchQueue.main.async { [weak self] in
                    self?.shortcutButton.highlight(true)
                }
            } else {
                DispatchQueue.main.async { [weak self] in
                    self?.shortcutButton.highlight(false)
                }
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Check to see if the keybind has been stored previously
        // If it has then update the UI with the below methods.
        if Storage.fileExists("globalKeybind.json", in: .documents) {
            let globalKeybinds = Storage.retrieve("globalKeybind.json", from: .documents, as: GlobalKeybindPreferences.self)
            updateKeybindButton(globalKeybinds)
            updateClearButton(globalKeybinds)
        }

    }

    // When a shortcut has been pressed by the user, turn off listening so the window stops listening for keybinds
    // Put the shortcut into a JSON friendly struct and save it to storage
    // Update the shortcut button to show the new keybind
    // Make the clear button enabled to users can remove the shortcut
    // Finally, tell AppDelegate to start listening for the new keybind
    func updateGlobalShortcut(_ event : NSEvent) {
        self.listening = false

        if let characters = event.charactersIgnoringModifiers {
            let newGlobalKeybind = GlobalKeybindPreferences.init(
                function: event.modifierFlags.contains(.function),
                control: event.modifierFlags.contains(.control),
                command: event.modifierFlags.contains(.command),
                shift: event.modifierFlags.contains(.shift),
                option: event.modifierFlags.contains(.option),
                capsLock: event.modifierFlags.contains(.capsLock),
                carbonFlags: event.modifierFlags.carbonFlags,
                characters: characters,
                keyCode: UInt32(event.keyCode)
            )

            Storage.store(newGlobalKeybind, to: .documents, as: "globalKeybind.json")
            updateKeybindButton(newGlobalKeybind)
            clearButton.isEnabled = true
            let appDelegate = NSApplication.shared.delegate as! AppDelegate
            appDelegate.hotKey = HotKey(keyCombo: KeyCombo(carbonKeyCode: UInt32(event.keyCode), carbonModifiers: event.modifierFlags.carbonFlags))
        }

    }

    // When the set shortcut button is pressed start listening for the new shortcut
    @IBAction func register(_ sender: Any) {
        unregister(nil)
        listening = true
        view.window?.makeFirstResponder(nil)
    }

    // If the shortcut is cleared, clear the UI and tell AppDelegate to stop listening to the previous keybind.
    @IBAction func unregister(_ sender: Any?) {
        let appDelegate = NSApplication.shared.delegate as! AppDelegate
        appDelegate.hotKey = nil
        shortcutButton.title = ""

        Storage.remove("globalKeybind.json", from: .documents)
    }

    // If a keybind is set, allow users to clear it by enabling the clear button.
    func updateClearButton(_ globalKeybindPreference : GlobalKeybindPreferences?) {
        if globalKeybindPreference != nil {
            clearButton.isEnabled = true
        } else {
            clearButton.isEnabled = false
        }
    }

    // Set the shortcut button to show the keys to press
    func updateKeybindButton(_ globalKeybindPreference : GlobalKeybindPreferences) {
        shortcutButton.title = globalKeybindPreference.description
    }

}

Two properties and two methods need to be hooked up here.

  • register:sender needs to be connected with the shortcut button.
  • unregister:sender needs to be connected to the clear shortcut button.
  • clearButton property needs to be connected to the clear shortcut button.
  • shortcutButton needs to be connected to the shortcut button.

This is quite long a long file but each method is commented. As a general overview, it’s letting one button listen and update the app shortcut and another button will clear that shortcut.

Create a new file, GlobalKeybindPreferences.swift. This will be a struct that holds the shortcut state. This includes modifiers and keys that are pressed. It also has a handy computed property called description which is used in PreferencesViewController to set the shortcut button text to look like ⌃⌘K.

//
//  GlobalKeybindPreferences.swift
//  GlobalConfigKeybind
//
//  Created by Mitch Stanley on 07/04/2019.
//
struct GlobalKeybindPreferences: Codable, CustomStringConvertible {
    let function : Bool
    let control : Bool
    let command : Bool
    let shift : Bool
    let option : Bool
    let capsLock : Bool
    let carbonFlags : UInt32
    let characters : String?
    let keyCode : UInt32

    var description: String {
        var stringBuilder = ""
        if self.function {
            stringBuilder += "Fn"
        }
        if self.control {
            stringBuilder += "⌃"
        }
        if self.option {
            stringBuilder += "⌥"
        }
        if self.command {
            stringBuilder += "⌘"
        }
        if self.shift {
            stringBuilder += "⇧"
        }
        if self.capsLock {
            stringBuilder += "⇪"
        }
        if let characters = self.characters {
            stringBuilder += characters.uppercased()
        }
        return "\(stringBuilder)"
    }
}

In AppDelegate.swift we need to listen for the shortcut if it exists and pull the MainWindow to the front.

We can see in applicationDidFinishLaunching:aNotification we check if the globalKeybind.json file exists, if it does, set the HotKey to what we have stored.

The computed property hotKey checks if hotKey is not nil, and then adds a keyDownHandler. In this closure, we loop through all the windows we have open (It’s possible that the Preferences window is also open, otherwise we could use first). When the MainWindow is found we bring it to the front with makeKeyAndOrderFront and makeKey.

//
//  AppDelegate.swift
//  GlobalConfigKeybind
//
//  Created by Mitch Stanley on 07/04/2019.
//

import Cocoa
import HotKey
import Carbon

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {



    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application
        if Storage.fileExists("globalKeybind.json", in: .documents) {

            let globalKeybinds = Storage.retrieve("globalKeybind.json", from: .documents, as: GlobalKeybindPreferences.self)
            hotKey = HotKey(keyCombo: KeyCombo(carbonKeyCode: globalKeybinds.keyCode, carbonModifiers: globalKeybinds.carbonFlags))
        }
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }

    public var hotKey: HotKey? {
        didSet {
            guard let hotKey = hotKey else {
                return
            }

            hotKey.keyDownHandler = { [weak self] in
                NSApplication.shared.orderedWindows.forEach({ (window) in
                    if let mainWindow = window as? MainWindow {
                        print("woo")
                        NSApplication.shared.activate(ignoringOtherApps: true)
                        mainWindow.makeKeyAndOrderFront(self)
                        mainWindow.makeKey()
                    }
                })

            }
        }
    }
}

Finally, the last piece of the puzzle, create a new file called Storage.swift. We’ll use this awesome class created by Saoud M. Rizwan which can be found in more detail here. This class makes working with local JSON storage very simple and I encourage you to read the blog post to understand how it works.

import Foundation

public class Storage {

    fileprivate init() { }

    enum Directory {
        // Only documents and other data that is user-generated, or that cannot otherwise be recreated by your application, should be stored in the <Application_Home>/Documents directory and will be automatically backed up by iCloud.
        case documents

        // Data that can be downloaded again or regenerated should be stored in the <Application_Home>/Library/Caches directory. Examples of files you should put in the Caches directory include database cache files and downloadable content, such as that used by magazine, newspaper, and map applications.
        case caches
    }

    /// Returns URL constructed from specified directory
    static fileprivate func getURL(for directory: Directory) -> URL {
        var searchPathDirectory: FileManager.SearchPathDirectory

        switch directory {
        case .documents:
            searchPathDirectory = .documentDirectory
        case .caches:
            searchPathDirectory = .cachesDirectory
        }

        if let url = FileManager.default.urls(for: searchPathDirectory, in: .userDomainMask).first {
            return url
        } else {
            fatalError("Could not create URL for specified directory!")
        }
    }


    /// Store an encodable struct to the specified directory on disk
    ///
    /// - Parameters:
    ///   - object: the encodable struct to store
    ///   - directory: where to store the struct
    ///   - fileName: what to name the file where the struct data will be stored
    static func store<T: Encodable>(_ object: T, to directory: Directory, as fileName: String) {
        let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)

        let encoder = JSONEncoder()
        do {
            let data = try encoder.encode(object)
            if FileManager.default.fileExists(atPath: url.path) {
                try FileManager.default.removeItem(at: url)
            }
            FileManager.default.createFile(atPath: url.path, contents: data, attributes: nil)
        } catch {
            fatalError(error.localizedDescription)
        }
    }

    /// Retrieve and convert a struct from a file on disk
    ///
    /// - Parameters:
    ///   - fileName: name of the file where struct data is stored
    ///   - directory: directory where struct data is stored
    ///   - type: struct type (i.e. Message.self)
    /// - Returns: decoded struct model(s) of data
    static func retrieve<T: Decodable>(_ fileName: String, from directory: Directory, as type: T.Type) -> T {
        let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)

        if !FileManager.default.fileExists(atPath: url.path) {
            fatalError("File at path \(url.path) does not exist!")
        }

        if let data = FileManager.default.contents(atPath: url.path) {
            let decoder = JSONDecoder()
            do {
                let model = try decoder.decode(type, from: data)
                return model
            } catch {
                fatalError(error.localizedDescription)
            }
        } else {
            fatalError("No data at \(url.path)!")
        }
    }

    /// Remove all files at specified directory
    static func clear(_ directory: Directory) {
        let url = getURL(for: directory)
        do {
            let contents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
            for fileUrl in contents {
                try FileManager.default.removeItem(at: fileUrl)
            }
        } catch {
            fatalError(error.localizedDescription)
        }
    }

    /// Remove specified file from specified directory
    static func remove(_ fileName: String, from directory: Directory) {
        let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)
        if FileManager.default.fileExists(atPath: url.path) {
            do {
                try FileManager.default.removeItem(at: url)
            } catch {
                fatalError(error.localizedDescription)
            }
        }
    }

    /// Returns BOOL indicating whether file exists at specified directory with specified file name
    static func fileExists(_ fileName: String, in directory: Directory) -> Bool {
        let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)
        return FileManager.default.fileExists(atPath: url.path)
    }
}

And that’s it. Try running the app, bringing up preferences, setting a shortcut, bring some other apps infront of it and press the shortcuts to test it out. Not only this, but if you close the Main window and then press the key bind the app should re-open.

There are a few things that could be improved, such as checking if the shortcut is valid or already used by the system but that's a quest for another day.

Discussion

pic
Editor guide
Collapse
aegerborder profile image
Patrick

As a beginner I must say that this is a great and in depth tutorial, Very nice! But...

"register:sender needs to be connected with the shortcut button.
unregister:sender needs to be connected to the clear shortcut button.
clearButton property needs to be connected to the clear shortcut button.
shortcutButton needs to be connected to the shortcut button."

Sorry but... what? :D

There's one button and there's one text field. Can't figure out which to connect with which action/outlet because you're calling them all "button"

There's screenshots for all things but this one. Would be incredibly great to know what I'm doing wrong.

Collapse
mitchartemis profile image
Mitch Stanley Author

Hi Patrick,

Thank you for reading and for the kind words!

Sorry for the confusion. There's a mistake in the tutorial. Instead of using a text field and a button I'd recommend two buttons. One is the clear button, and the other is the "set" button.

You may also wish to set default text on the set button so the user knows to click it. You can do that from the storyboard title button property, as well as updating the unregister method

shortcutButton.title = "Set Shortcut"
Collapse
aegerborder profile image
Patrick

Hey Mitch,

thank you for your reply which explained a lot and now all makes sense, haha :D

If I may grab another minute off you:
How would you go on if there was no MainWindow? I have an existing app and I have the MainStoryboard wired to a ViewController. Like this: thepracticaldev.s3.amazonaws.com/i...

I want this one to pop up when hitting the shortcut. I've tried a few things but nothing worked so far :-/

Thanks again and best regards
Patrick

Thread Thread
mitchartemis profile image
Mitch Stanley Author

No problem Patrick :)

Can you show me a screenshot of your Storyboard?

Usually a view controller will have window or something else that it is used in.

One of the exceptions is if you're making a menubar app, in which case you can use NSPopover in your AppDelegate

class AppDelegate: NSObject, NSApplicationDelegate {
    // ...

    let popover = NSPopover()

    // ...

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        popover.contentViewController = MenuViewController.freshController()

        eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in
            if let strongSelf = self, strongSelf.popover.isShown {
                print("is shown")
                strongSelf.closePopover(sender: event)
            }

    }
    // ...
    @objc func togglePopover(_ sender: Any?) {
        if popover.isShown {
            closePopover(sender: sender)
        } else {
            showPopover(sender: sender)
        }
    }
    func showPopover(sender: Any?) {
        if let button = statusItem.button {
            popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
        }
        eventMonitor?.start()
    }
   func closePopover(sender: Any?) {
        popover.performClose(sender)
    }
    public var hotKey: HotKey? {
        didSet {
            guard let hotKey = hotKey else {
                return
            }

            hotKey.keyDownHandler = { [weak self]  in
                if let strongSelf = self {
                    strongSelf.togglePopover(nil)
                }
            }
        }
    }

This code is a little messy (copied it from an old project) but hopefully it is enough to give you an idea of how it works.

Thread Thread
aegerborder profile image
Patrick

Once again: thank you! I guess I'm coming closer to that goal, hehe. Indeed it's a menu bar app. I ran through a "Swift on sundays" session by Paul Hudson and that's why there's a lot of stuff in the AppDelegate.swift and I don't know exactly how to merge both your code and the code that's already there.

Here's the MainStoryboard: thepracticaldev.s3.amazonaws.com/i...

And here's my AppDelegate. Tried to understand that Markdown stuff here but didn't succeed :(

import Cocoa
import HotKey
import Carbon

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        if Storage.fileExists("globalKeybind.json", in: .documents) {

            let globalKeybinds = Storage.retrieve("globalKeybind.json", from: .documents, as: GlobalKeybindPreferences.self)
            hotKey = HotKey(keyCombo: KeyCombo(carbonKeyCode: globalKeybinds.keyCode, carbonModifiers: globalKeybinds.carbonFlags))
        }

        statusItem.button?.title = "⅀"

        statusItem.button?.target = self

        statusItem.button?.action = #selector(showSettings)

    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }

    @objc func showSettings() {

        let storyboard = NSStoryboard(name: "Main", bundle: nil)

        guard let vc = storyboard.instantiateController(withIdentifier: "ViewController") as? NSViewController else {
            fatalError("ViewController in Storyboard nicht gefunden.")
        }

        guard let button = statusItem.button else {

            fatalError("Button des Status-Items konnte nicht gefunden werden")
        }


        let popoverView = NSPopover()
        popoverView.contentViewController = vc
        popoverView.behavior = .transient
        popoverView.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)

    }


    public var hotKey: HotKey? {
        didSet {
            guard let hotKey = hotKey else {
                return
            }

            hotKey.keyDownHandler = { [weak self] in
                NSApplication.shared.orderedWindows.forEach({ (window) in
                    if let mainWindow = window as? MainWindow {
                        print("woo")
                        NSApplication.shared.activate(ignoringOtherApps: true)
                        mainWindow.makeKeyAndOrderFront(self)
                        mainWindow.makeKey()
                    }
                })

            }
        }
    }


}
Thread Thread
mitchartemis profile image
Mitch Stanley Author

Is the settings window supposed to be a popover as well? Or is that just a separate window that opens when the setting button is clicked?

I would try

1) Adding a popover instance to the AppDelegate
2) Add the main view controller instance to the new popover in the applicationDidFinishLaunching method

Something like this (This is untested!)

import Cocoa
import HotKey
import Carbon

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)

    // Add mainPopover and eventMonitor as an instance variable
    let mainPopoverView = NSPopover()
    var eventMonitor: EventMonitor?

    func applicationDidFinishLaunching(_ aNotification: Notification) {

        if Storage.fileExists("globalKeybind.json", in: .documents) {

            let globalKeybinds = Storage.retrieve("globalKeybind.json", from: .documents, as: GlobalKeybindPreferences.self)
            hotKey = HotKey(keyCombo: KeyCombo(carbonKeyCode: globalKeybinds.keyCode, carbonModifiers: globalKeybinds.carbonFlags))
        }

        statusItem.button?.title = "⅀"

        statusItem.button?.target = self

        statusItem.button?.action = #selector(showSettings)

        // Fresh controller instance - use your view controller here
        mainPopoverView.contentViewController = MenuViewController.freshController()

        // Check if mouse is clicked outside of menu - if it is then close the popover (optional)
        eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in
            if let strongSelf = self, strongSelf.mainPopoverView.isShown {
                print("is shown")
                strongSelf.closePopover(sender: event)
            }
        }
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }

    // Add in the toggling methods
    @objc func togglePopover(_ sender: Any?) {
        if mainPopoverView.isShown {
            closePopover(sender: sender)
        } else {
            showPopover(sender: sender)
        }
    }

    func showPopover(sender: Any?) {
        if let button = statusItem.button {
            mainPopoverView.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
        }
        eventMonitor?.start()
    }

    func closePopover(sender: Any?) {
        mainPopoverView.performClose(sender)
        eventMonitor?.stop()
    }



    @objc func showSettings() {

        let storyboard = NSStoryboard(name: "Main", bundle: nil)

        guard let vc = storyboard.instantiateController(withIdentifier: "ViewController") as? NSViewController else {
            fatalError("ViewController in Storyboard nicht gefunden.")
        }

        guard let button = statusItem.button else {

            fatalError("Button des Status-Items konnte nicht gefunden werden")
        }


        let popoverView = NSPopover()
        popoverView.contentViewController = vc
        popoverView.behavior = .transient
        popoverView.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)

    }


    public var hotKey: HotKey? {
        didSet {
            guard let hotKey = hotKey else {
                return
            }

            // Toggle popover 
            hotKey.keyDownHandler = { [weak self] in
                if let strongSelf = self {
                    strongSelf.togglePopover(nil)
                }
            }
        }
    }
}

Mildly related, I recommend reading this Ray Wenderlich tutorial on menu bar apps, it helped me understand them better - raywenderlich.com/450-menus-and-po...

Thread Thread
aegerborder profile image
Patrick

Thank you! I guess this is far above my skill level. I'm already out on "event monitor" and "instance variable", "fresh controller instance". Funny thing: the moment you sent the reply I was on that exact same site of Ray Wenderlich to find out how to use an image as menu bar icon instead of that mathematical "sum" icon I had used before. I think I will keep learning stuff and get back to this in about 5 years, haha :D

But again: thank you very much. It's doesn't happen too often that people just offer their time to help out in this huge form. And: it shows that there will be a solution in the future when I'm able to understand these things and alter the necessary "things" (Objects? Items? I have no idea at all! :D)

Thread Thread
mitchartemis profile image
Mitch Stanley Author

My pleasure Patrick,

The trick is to keep pushing yourself to try different things in your code. The fact that you're already doing this is a great start.

I hope you continue to pursue Swift. It's truly a wonderful language, especially with the new SwiftUI stuff that's arrived recently!

Thread Thread
aegerborder profile image
Patrick

Thanks. And... I'm a fool. I stopped reading the page at "Bet you’re feeling smarter already!" with that little fella flexing. Thought "OK, page end" and quit. I reopened the site to re-read a detail I had missed and saw that there's an entire "Event Monitoring" part following that little dude. NOW I know what you were talking about in the comments of your code.

Well... time for an awkward confession: I'm learning Swift since late 2017 and I even have a few iOS Apps on the App Store. But I am totally new to that thing on macOS. It's very similar in the basics but there're many differences in actions and objects and members of objects etc.
I like Swift a lot. I always like things that show your progress immediately (or tell you what or THAT something went wrong). And as a Mac and iPhone user I can do things for myself. Things I need or always wanted. Yes, it's great. And the day will come when this super small app is done :D

Thanks for motivation - I'll keep practicing :)

Thread Thread
mitchartemis profile image
Mitch Stanley Author

Yeah MacOS is a completely different beast! and unfortunately it's really hard to find good tutorials and resources for it which makes it that much harder to build for.

Not only that, you'll often have to rely on old APIs that are really awful to write/read. iOS is much nicer in that sense!

Glad you've got the event monitoring stuff figured out 😀

Thread Thread
aegerborder profile image
Patrick

Not only figured that out. I got the shortcut thing AND the window/popup issues sorted out and finally both up and running now. And that's only because of your help. Thank you. Again and again :)
There are only 2-3 little things left.
Another good thing is that not everything is different from iOS. I just learned that UserDefaults work the exact same way. At least this was a hattrick scored within seconds, hehe.
Phew... by the way: if you don't understand any things I write feel free to ask. Just a German digging in pale memories of English lessons from over 25 years ago, haha.

Thread Thread
mitchartemis profile image
Mitch Stanley Author

That's great to hear! And yeah being able to share code between iOS and MacOS is really handy :).

Don't worry your English seems pretty good to me, I didn't even noticed until the screenshot you sent had German in it, haha :D.