Claire
2 years ago
3 changed files with 248 additions and 1 deletions
@ -0,0 +1,243 @@ |
|||||||
|
# Setting (and retrieving) user preferences |
||||||
|
|
||||||
|
## Sections |
||||||
|
* [The main application](/wiki/Home) |
||||||
|
* [Windows with Swift](/wiki/Swift-Windows) |
||||||
|
* Setting (and retrieving) user preferences (this page) |
||||||
|
|
||||||
|
## 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. |
||||||
|
|
||||||
|
## 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`](https://developer.apple.com/documentation/foundation/userdefaults/1416603-standard). |
||||||
|
|
||||||
|
```swift |
||||||
|
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`](https://developer.apple.com/documentation/appkit/nsmenuitem) the user selects. We can set the preference based on the contents of [`NSMenuItem.title`](https://developer.apple.com/documentation/appkit/nsmenuitem/1514805-title). |
||||||
|
|
||||||
|
Once we set our user preference, we'll run `updateIcon()`, which will rebuild the icon and menu with the new preferred icon set. |
||||||
|
|
||||||
|
```swift |
||||||
|
@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() |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Adding new icons |
||||||
|
|
||||||
|
<img src="https://abettergeek.com/_media/wiki/git/lunamac/newimages.png" align="left"> |
||||||
|
|
||||||
|
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. |
||||||
|
<br clear="left"/> |
||||||
|
|
||||||
|
## Creating the submenu |
||||||
|
|
||||||
|
<img src="https://abettergeek.com/_media/wiki/git/lunamac/prefmenu.png" align="right"> |
||||||
|
|
||||||
|
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](https://developer.apple.com/documentation/appkit/nsmenu/1518176-additem). This works, but it doesn't give us access to the more advanced capabilities of [`NSMenu`](https://developer.apple.com/documentation/appkit/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`](https://developer.apple.com/documentation/appkit/nsmenu), which will serve as a submenu for the **Icon Style** menu item. |
||||||
|
<br clear="right"/> |
||||||
|
|
||||||
|
```swift |
||||||
|
@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`](https://developer.apple.com/documentation/appkit/nsmenuitem). All three will call our `setPref()` function when clicked. |
||||||
|
|
||||||
|
```swift |
||||||
|
@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`](https://developer.apple.com/documentation/appkit/nsmenuitem/1514804-state) to [`NSControl.StateValue.on`](https://developer.apple.com/documentation/appkit/nscontrol/statevalue/2876355-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`. |
||||||
|
|
||||||
|
```swift |
||||||
|
@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. |
||||||
|
|
||||||
|
```swift |
||||||
|
@objc func buildMenu(key: String = "default") { |
||||||
|
... |
||||||
|
prefMenu.addItem(optDefault) |
||||||
|
prefMenu.addItem(optDarkAlt) |
||||||
|
prefMenu.addItem(optEmoji) |
||||||
|
... |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Now we need to create the [`NSMenuItem`](https://developer.apple.com/documentation/appkit/nsmenuitem) instance for the **Icon Style** menu item, and add it to our main menu object. |
||||||
|
|
||||||
|
```swift |
||||||
|
@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. |
||||||
|
|
||||||
|
```swift |
||||||
|
@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. |
||||||
|
|
||||||
|
## 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. |
||||||
|
|
||||||
|
```swift |
||||||
|
@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. |
||||||
|
|
||||||
|
```swift |
||||||
|
@objc func buildMenu(key: String = "default") { |
||||||
|
... |
||||||
|
// 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" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
... |
||||||
|
} |
||||||
|
``` |
Loading…
issues.context.reference_issue