5 User Prefs
Claire Davis edited this page 2 years ago

Setting (and retrieving) user preferences

Sections

I. Introduction

Version 1.4 of LunaMac introduces a user preference, which defines the preferred icon style - default, alternate dark, or emoji. After I finished and released this project, I decided I didn't like the dark mode icons as much, because the white part of the icon is actually the dark area of the moon. I also wanted to add back support for the emoji icons, in case some people prefer that to the monochrome icons.

II. Setting a user preference

Handling all this one of the easiest parts of this project, so I can at least give Apple credit for that much. Apple provides a common interface for application preferences, which in MacOS are managed using special XML plist files. Instead of having to write these files directly, you can just use UserDefaults.standard.

class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
    // user settings
    let defaults = UserDefaults.standard
}

We're going to set a single preference named iconPref, which holds a string value. Each value represents an icon theme: default, darkalt, and emoji.

Before we make a submenu for setting the user preference, we're going to make a function that actually handles setting the value in defaults.

This function takes a sender argument, which is the NSMenuItem the user selects. We can set the preference based on the contents of NSMenuItem.title.

Once we set our user preference, we'll run updateIcon(), which will rebuild the icon and menu with the new preferred icon set.

@objc func setPref(sender: NSMenuItem) {
    // sender.title tells us what to do
    switch (sender.title) {
        case "Default" :
            defaults.set("default", forKey: "iconPref")
            break
        case "Alternate Dark" :
            defaults.set("darkalt", forKey: "iconPref")
            break
        case "Emoji" :
            defaults.set("emoji", forKey: "iconPref")
            break
        default:
            break
    }
    
    // rebuild the menu
    updateIcon()
}

III. Adding new icons

Next, we need to add icons for these new icon sets. As you'll see below, the alternate dark mode icons can be reused in a different order for half of the phases. For the crescent and gibbous phases, I created new icons with better light/dark proportion. These icons use the same filename prefix, with a suffix of -alt-dark. The emoji icons, which are PNG representations of Apple's system emoji font, use a suffix of -emoji.

Remember to run xattr -cr . after adding new images to your project.

IV. Creating the submenu

Now we need a menu interface, so the user can select their preferred icon set.

Previously, in buildMenu(), we built our menu by directly invoking NSMenu.addItem. This works, but it doesn't give us access to the more advanced capabilities of NSMenu, like adding submenus. We're going to add a user setting menu item, which points to a submenu of options.

First, we're going to create a new instance of NSMenu, which will serve as a submenu for the Icon Style menu item.

@objc func buildMenu(key: String = "default") {
    ...
	// create menu item to hold submenu
    let prefMenu = NSMenu(title: "Prefs")
    ...
}

Next, we're going to create the three menu items in our submenu using NSMenuItem. All three will call our setPref() function when clicked.

@objc func buildMenu(key: String = "default") {
    ...
    // default
    let optDefault = NSMenuItem(
        title: "Default",
        action: #selector(setPref),
        keyEquivalent: ""
    )
    
    // alternate dark
    let optDarkAlt = NSMenuItem(
        title: "Alternate Dark",
        action: #selector(setPref),
        keyEquivalent: ""
    )
    
    // emoji
    let optEmoji = NSMenuItem(
        title: "Emoji",
        action: #selector(setPref),
        keyEquivalent: ""
    )
    ...
}

You may have noticed in my menu screenshot that the selected theme is indicated with a checkmark. This is done by setting NSMenuItem.state to NSControl.StateValue.on.

This is accomplished with a basic switch case, which sets the state value based on the user's icon preference. If no preference exists, the icon preference is set to default.

@objc func buildMenu(key: String = "default") {
    ...
    // make sure the right menu item is checked
    switch (defaults.string(forKey: "iconPref")) {
        case "default" :
            // standard icons
            optDefault.state = NSControl.StateValue.on
            break
        case "darkalt" :
            // inverted dark mode icons
            optDarkAlt.state = NSControl.StateValue.on
            break
        case "emoji" :
            // emoji icons
            optEmoji.state = NSControl.StateValue.on
            break
        default :
            // default to standard icons
            optDefault.state = NSControl.StateValue.on
            break
    }
    ...
}

Now that we have our menu items all built and set up, we can add them to our submenu object.

@objc func buildMenu(key: String = "default") {
    ...
    prefMenu.addItem(optDefault)
    prefMenu.addItem(optDarkAlt)
    prefMenu.addItem(optEmoji)
    ...
}

Now we need to create the NSMenuItem instance for the Icon Style menu item, and add it to our main menu object.

@objc func buildMenu(key: String = "default") {
    ...
    let prefMenuItem = NSMenuItem(
        title: "Icon Style",
        action: nil,
        keyEquivalent: ""
    )

    statusBarMenu.addItem(prefMenuItem)
    ...
}

The last thing we need to do is add our submenu, prefMenu, to the menu item we just created.

@objc func buildMenu(key: String = "default") {
    ...
    statusBarMenu.setSubmenu(prefMenu, for: prefMenuItem)
    ...
}

You can run your application now, and test the user setting. Change the setting, close the app, relaunch, and your selected icon set should be identified with a checkmark.

Now we can move on to actually displaying different icons based on the user preference.

V. Displaying the user's preferred icon set

At the top of our buildMenu() function, we're going to add some code to the if block used to set the correct icon mode (dark or light).

First, if the user preference is set to emoji, we just need to append -emoji to the filename.

@objc func buildMenu(key: String = "default") {
    ...
    // if iconPref is set to emoji
    if (defaults.string(forKey: "iconPref") == "emoji") {
        suffix += "-emoji"
    }
    ...
}

For the monochrome icons, we just need to make a few small adjustments. Since we want the white parts of the icon to represent the illuminated area of the moon, we can just swap the new and full icons, and also swap the firstq and lastq icons.

For the other four phases (waxing and waning, crescent and gibbous), we're going to append -alt to the filename.

Since we're now setting the value of key2 after the variable has been created, we need to use var instead of let when defining key2.

@objc func buildMenu(key: String = "default") {
    ...
    // if key is new2, rename to new
    var key2 = key.contains("2") ? "new" : key
    ...
    // otherwise use monochrome icons
    else {
        // check for dark mode and set the right image file
        if #available(OSX 10.14, *) {
            let darkMode = UserDefaults.standard.string(forKey: "AppleInterfaceStyle")
            
            if darkMode == "Dark" {
                // if theme is set to dark alt make adjustments
                if (defaults.string(forKey: "iconPref") == "darkalt") {
                    switch (key2) {
                    case "new" :
                        key2 = "full"
                        break
                    case "full" :
                        key2 = "new"
                        break
                    case "firstq" :
                        key2 = "lastq"
                        break
                    case "lastq" :
                        key2 = "firstq"
                        break
                    case "wanc", "wang", "waxc", "waxg" :
                        suffix += "-alt"
                        break
                    default :
                        break
                    }
                }
                
                suffix += "-dark"
            }
        }
    }
    ...
}

Compile your application, and you now have a fully functional, persistent user preference for your application's icon set.