Setting (and retrieving) user preferences
Sections
- The main application
- Windows with Swift
- Setting (and retrieving) user preferences (this page)
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.