Update 'Home'

master
Claire 2 years ago
commit
c50e78e4e3
  1. 601
      Home.md

601
Home.md

@ -0,0 +1,601 @@
# Code Notebook
## Introduction
I recently picked up a very cheap MacBook Air circa 2015. I'm disinterested in running any version of MacOS beyond 10.14, and as time goes on and more and more popular apps drop support for Mojave, I figured I might try my hand at writing apps for my Mac.
In 2014, Apple introduced a new programming language called [Swift](https://www.apple.com/swift/). This is a high-level language using *managed code*, meaning the language runs on top of a framework, very similar to Microsoft's .NET. This also makes it possible to use C libraries (including C++ and Objective-C) in your Swift projects, which can be pretty useful depending on what you're doing.
In nearly forty years on this planet, I have yet to understand C++. It's too abstract and mathematical for the way my mind works. .NET, on the other hand, was very easy for me to learn on my own and use to create useful applications. This is really Apple's central goal with Swift: lowering the barriers for new programmers to start building apps for Apple's ecosystem.
Swift does things very differently from .NET. It's strongly-typed to an extreme, and it seems Swift cannot inherently handle null (aka "nil") values. If you're looking for a cross-platform language that's relatively easy to learn and implement, .NET is still superior. Apple is trying hard to position Swift as a cross-platform language, but its paradigms and standards will be foreign to anyone who's used to developing for Windows and Linux.
And with that, let's look at how the code for *this* project works.
## The Plan
I'm one of those people who cares about the moon phases. I like to know what phase the moon is in at a glance. I found [this project on GitHub](https://github.com/ColdBio/Moonu-Phases), but it requires Python - and enough knowledge of Python to actually get it to work. I fiddled with it for a day or so, and decided to instead try my hand at writing my own tool.
My needs are minimal:
* A menu bar icon that graphically shows the phase of the moon
* No dock or menu bar required; the application is driven by its icon
* The icon should expose a menu:
* The name of the current moon phase
* About the app
* Quit the app
## Setting up Xcode project
I'll be using Xcode 10.3 and Swift 5.0.1, on a Mac running MacOS 10.14.6. We'll start with a blank Swift project. In Xcode, create a new project, and use the **Cocoa App** project template under **MacOS**. Give your project a name and publisher details, and make sure **Language** is set to **Swift**, and **Use Storyboards** is ***unchecked***. Storyboarding is a rather complex Apple-designed means of creating UIs and attaching behavior (backend code) to those UI elements. Since the main application is a status bar icon, we're building the initial UI programmatically, so we don't need to use this feature.
Before we get into the code, let's get the application's icons out of the way, since it's not exactly a straightforward procedure.
### Menu bar icons
LunaMac v1 used emoji for the menu bar icon. v2 introduced handmade icons, with support for both light and dark MacOS themes. This is where we run into one of Apple's fun quirks: you can't package an app for distribution if any of its files contain extended MacOS file attributes, but Xcode doesn't strip these attributes when you import files with MacOS attributes.
I put all my icons in a folder named `png` and added the folder and its contents to my project (**File** > **Add files to [Your Project]...**). Make sure you check the option to **Copy items if needed**, and select **Create groups** under **Added folders:**. Once you've added whatever resources to your project, fire up Terminal so we can make sure those MacOS attributes are gone.
Navigate to your project folder and run the following, which will strip extended attributes from all files and folders under your current directory. We'll use `xattr` to strip the extended attributes:
```bash
xattr -cr .
```
To double-check the results of this command, you can run:
```bash
xattr -lr .
```
If all went well, you *should* get no results. If something went wrong, try again, and maybe try with `sudo`, to circumvent any permissions issues. These extended attributes will prevent codesigning entirely, which is a *mandatory* step to packaging a MacOS application for release. If you get an error like `resource fork, Finder information, or similar detritus not allowed` during archiving your project for release, try using `xattr` to strip your project of any leftover attributes.
### Application icon
Apple loves its ultra-high-res icons, so you're going to need a set of icon images (generally transparent PNGs or TIFFs). [This StackOverflow thread](https://stackoverflow.com/questions/12281309/where-is-the-iconset-command-line-tool-iconutil-located) goes into some detail regarding icon dimensions and PPI. If you want pixel-perfect icons of all dimensions and densities, you'll want to manually create the following files:
| file | dimensions | PPI |
| --------------------- | ------------ | --- |
| `icon_16x16.png` | 16x16 | 72 |
| `icon_16x16@2x.png` | 32x32 | 144 |
| `icon_32x32.png` | 32x32 | 72 |
| `icon_32x32@2x.png` | 64x64 | 144 |
| `icon_128x128.png` | 128x128 | 72 |
| `icon_128x128@2x.png` | 256x256 | 144 |
| `icon_256x256.png` | 256x256 | 72 |
| `icon_256x256@2x.png` | 512x512 | 144 |
| `icon_512x512.png` | 512x512 | 72 |
| `icon_512x512@2x.png` | 1024x1024 | 144 |
**Important Note:** Your icon images *must* use the names listed here. Anything else will cause Apple's icon generator utility to fail.
I didn't save at different PPIs, however. I'm just using images of the correct pixel dimensions. Put all these files in a directory named `Icon.iconset`. Fire up Terminal, navigate to the parent folder of this directory, and run:
```bash
iconutil -c icns Icon.iconset/
```
This is a utility that ships with Xcode, and it will generate an `Icon.icns` file, which you can import into your Xcode project.
<img src="https://abettergeek.com/_media/wiki/git/lunamac/appicon.png" align="right" width="400">
If you plan on releasing your app in Apple's App Store, you also need to set up the blank `AppIcon` hiding under the `Assets.xcassets` pseudo-directory in your Xcode project. If you *don't* see an `AppIcon` listed here, right-click in the empty pane (or hit the plus sign) and navigate to **App Icons & Launch Images** > **New MacOS App Icon**. You'll have to drag-and-drop each image from your original `Icon.iconset` directory into the empty spots in the `AppIcon`.
To configure your project to use the `Icon.icns` file you imported, edit the existing `Info.plist` file. Add an `Icon file` key if one doesn't already exist, and set the value to Icon (no file extension required).
<br clear="right"/>
### Removing extraneous files
<img src="https://abettergeek.com/_media/wiki/git/lunamac/mainmenu2.png" align="left">
This project runs out of the systemwide menu bar, which means we don't need the default window and main menu that are part of `MainMenu.xib`. Open this file, and before we do anything else, drag the lefthand pane of the view so you see the full tree, rather than a handful of nondescript icons. Now you can see both `Main Menu` and `Window` in the list of objects. Select each and delete it by hitting backspace (or delete).
<br clear="left"/>
Whew. Now that's out of the way, let's get into the fun(ish) stuff.
## Creating a menu bar icon
I started with [this tutorial](https://8thlight.com/blog/casey-brant/2019/05/21/macos-menu-bar-extras.html) ([archive](https://archive.ph/bNVdG)). We need to start with kicking off the creation of the menu when the application launches.
Swift relies on the use of core methods which alert your code to various system and application events. Code that's executed at launch lives under the [`applicationDidFinishLaunching`](https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428385-applicationdidfinishlaunching) method of [`NSApplicationDelegate`](https://developer.apple.com/documentation/appkit/nsapplicationdelegate).
Any time the user chooses to update the icon from its menu, I'm going to rebuild the menu with new information. This means I need a separate function that actually handles building the menu, and I'll call that function from my application launch method.
```swift
func applicationDidFinishLaunching(_ aNotification: Notification) {
// build the icon and menu
buildMenu()
}
```
We're first going to instantiate an instance of the MacOS system-wide menu bar's status area (it's called the system tray in Windows), using [`NSStatusBar`](https://developer.apple.com/documentation/appkit/nsstatusbar).
```swift
@objc func buildMenu() {
// get system-wide menu bar
let statusBar = NSStatusBar.system
...
}
```
Next we're going to create a new item for this status bar using [`NSStatusItem`](https://developer.apple.com/documentation/appkit/nsstatusitem). However, because of how variables and objects are scoped in Swift, we need to declare this status bar item in our class, rather than in a specific method. This will keep the status bar item in memory as long as the application is running.
```swift
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
// make sure applet stays in memory
var statusBarItem: NSStatusItem?
...
}
```
The `?` has to do with Swift's paradigm of variable "wrapping" and "unwrapping." Suffice it to say, Swift doesn't directly handle null (nil) values. It's up to you to handle any null instances. Using `!` is unsafe, and a null value will cause your application to crash if you don't handle the exception. Using `?` is safe and enforces null handling.
You probably also noticed a default line at the beginning of your `AppDelegate` class:
```swift
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
...
@IBOutlet weak var window: NSWindow!
...
}
```
Delete this line - this is for loading the default application window, which we already removed from our project's `MainMenu.xib`, since we don't need it.
Now we can go back to our `buildMenu()` function. We're going to start working with `statusBarItem`. We need a way of knowing which moon phase to display, so we'll add an argument that takes a string. My app displays a specific icon if the lunar phase calcuation fails for some reason, so the default icon name is also the default value of this argument.
Since this application just shows a single icon in the menu bar, we don't need a variable-length icon area, so we'll use [`NSStatusItem.squareLength`](https://developer.apple.com/documentation/appkit/nsstatusitem/1534224-squarelength) constant.
MacOS doesn't require file extensions (and never has). I do most work in Windows, so I always make sure my files include the extension. You don't need to add the `.png` to the icon name if your files don't include the extension.
Finally, we can actually set the icon image (using [NSImage](https://developer.apple.com/documentation/appkit/nsimage)) on our menu bar item.
```swift
@objc func buildMenu(key: String = "default") {
...
// set icon size to square
statusBarItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
// set the icon filename
let imgName = key + ".png"
// set the icon image
statusBarItem?.button?.image = NSImage(named: imgName)
}
```
We have an icon! If you want to test your application now, we have enough code for it to do something. Hit the play button in Xcode, and in a moment, you'll see your menu bar icon, as well as the name of your application in the menu bar. Since nothing is set up to actually *do* anything yet, hit the stop button in Xcode to terminate your running application.
## Hiding the dock icon and application menu
We deleted the regular application menu bar, but the app still loaded the menu bar. You might have noticed it also appears in the dock when running. Since this application is just going to run from the menu bar's status area, we can disable the icon and regular menu bar.
Open `Info.plist` and click the plus sign next to **Information Property List** to add a new entry. Find (or paste into the box) **Application is agent (UIElement)**, and set the value to **YES**. If you rerun your project, you can now see it no longer loads a standard application menu, and there is no dock icon.
![](https://abettergeek.com/_media/wiki/git/lunamac/serviceplist.png)
## Building a menu for the icon
Since we have no regular application menu in this project, we need to add a menu to our menu bar icon. To get started, we'll just add a "Quit" menu item.
We'll create a new instance of [`NSMenu`](https://developer.apple.com/documentation/appkit/nsmenu). The title won't display in the UI. Then we'll associate our existing `statusBarMenuItem` with this `statusBarMenu`.
```swift
@objc func buildMenu(key: String = "default") {
...
// create a menu for the icon
// this is invisible
let statusBarMenu = NSMenu(title: "LunaMac Menu")
// add the menu to the menu bar icon
statusBarItem?.menu = statusBarMenu
...
}
```
Now we can add some menu items! We'll start with an entry to quit the application using [NSMenu.addItem](https://developer.apple.com/documentation/appkit/nsmenu/1518176-additem).
```swift
@objc func buildMenu(key: String = "default") {
...
statusBarMenu.addItem(
withTitle: "Quit LunaMac",
action: #selector(AppDelegate.quitApp),
keyEquivalent: ""
)
...
}
```
This is where Swift starts to veer a bit from what you might be accustomed to with other programming and scripting languages. Swift abstracts access to your function in a way that prevents passing arguments to that function. This means you need a separate function to handle every menu item.
We're going to call this one `quitApp`, and that's what the function needs to do. This is straightforward: just call the [`terminate()`](https://developer.apple.com/documentation/appkit/nsapplication/1428417-terminate) method of [`NSApp`](https://developer.apple.com/documentation/appkit/nsapp/).
```swift
// quit application
@objc func quitApp() {
NSApp.terminate(self)
}
```
<img src="https://abettergeek.com/_media/wiki/git/lunamac/quitmenu.png" align="right">
Run your application, and you should now have a menu that works!
You might be wondering how I have a dark mode-compatible menu bar icon. It's a perfect segue into the more advanced aspects of this project.
<br clear="right">
## Automatically setting the dark mode icon
To automatically choose the correct icon for light or dark themes, we need to look at a system-wide variable under [`UserDefaults`](https://developer.apple.com/documentation/foundation/userdefaults). This doesn't exist prior to MacOS 10.14, so we'll wrap it in an OS version check just to be safe.
The easiest way to manage light and dark icons is to just append a suffix to your dark icons, like `-dark`. This suffix is appended to the filename string passed to `statusBarItem` in our `buildMenu()` function.
```swift
@objc func buildMenu(key: String = "default") {
...
var suffix = ""
// 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" {
suffix += "-dark"
}
}
// set image filename
let imgName = key + suffix + ".png"
statusBarItem?.button?.image = NSImage(named: imgName)
...
}
```
Now that we can change the icon based on the theme, we need to add a listener for the system-wide [Notification](https://developer.apple.com/documentation/foundation/notification/) that fires when the system them is changed between dark and light.
The `extension` block goes **outside** your `ApplicationDelegate` class definition.
```swift
extension Notification.Name {
static let AppleInterfaceThemeChangedNotification = Notification.Name("AppleInterfaceThemeChangedNotification")
}
```
To enable this listener, we need a function we'll call from `applicationDidFinishLaunching()`. Mine is named `themeListener()`. As you can see in the `selector` property of the listener, we're calling the same `updateIcon()` function called when the application launches.
```swift
func themeListener() {
DistributedNotificationCenter.default.addObserver(
self,
selector: #selector(updateIcon),
name: .AppleInterfaceThemeChangedNotification,
object: nil
)
}
```
## Calculating the right lunar phase
### Overview
I can't take credit for figuring out the math on this. I was lucky to find [this excellent PHP implementation](https://minkukel.com/en/various/calculating-moon-phase/) ([archive](https://archive.ph/hjXlv)), which lays out the math in detail.
Here's the overview:
1. Use the lunar cycle constant `29.53058770576` and calculate its duration in seconds as `lunarSecs`.
2. Convert date of first full moon in 2000 (January 6 at 18:14 UTC) to unix timestamp as `lunarSecs`.
3. Calculate `totalSecs` which is the time (in seconds) between the user date and the moment of the first new moon in 2000
4. If the result is positive, it's valid, so calculate the modulus (remainder) of `totalSecs` divided by `lunarSecs` , to know the amount of seconds that have passed in the current cycle as `currSecs`.
5. Divide `currSecs` by `lunarSecs` to find the current position in the lunar cycle as `currentFrac`.
6. Multiply `currFrac` by `lunarDays` to get this value in days as `currDays`.
7. Find the correct moon phase based on the value of `currDays`.
This is all just basic arithmetic, so let's get into the Swift implementation.
### Implementation
Since we're going to want this app to stay updated automatically, the math is going into a function named `updateIcon()`. This is the `key` we're going to pass to the `buildMenu()` function we already built.
First, we're going to get the curren system date and time with [Date](https://developer.apple.com/documentation/foundation/date). This is always in UTC.
```swift
// update menu bar icon
@objc func updateIcon() {
// current system time in UTC
let sysDate = Date()
...
}
```
So we can perform date math with this value, we need a date object representing our full moon baseline of January 6, 2000 at 6:14 PM UTC. Annoyingly, this can't be done with a one-liner in Swift. Instead, we have to create a [`DateComponents()`](https://developer.apple.com/documentation/foundation/calendar/2293176-datecomponents) object and pass that to an instance of a [`Calendar()`](https://developer.apple.com/documentation/foundation/calendar) object.
```swift
@objc func updateIcon() {
...
// create date object from baseDate
// date components
var bdc = DateComponents()
bdc.year = 2000
bdc.month = 1
bdc.day = 6
bdc.hour = 18
bdc.minute = 14
// create date object
let ucal = Calendar(identifier: .gregorian)
let baseDate = ucal.date(from: bdc)
...
}
```
Now we can do the math. Note the use of `truncatingRemainder()` instead of the standard modulo operator, `%`. This is another fun Swift quirk: floats and doubles have different available methods. Additionally, Swift functions **require** explicitly naming each argument passed to the function. There are no positional arguments.
Apple does at least provide a useful function here to do the date math for us, as opposed to the old school way of doing integer math with UNIX epoch time. We just have to use [`Date.timeIntervalSince()`](https://developer.apple.com/documentation/foundation/nsdate/1410206-timeintervalsince) to get the total seconds between our full moon baseline and the current system time.
```swift
@objc func updateIcon() {
...
// days in a lunar cycle
let lunarDays = 29.53058770576
// seconds in a lunar cycle
let lunarSecs = lunarDays * (24 * 60 * 60)
// calculate seconds between sysDate and baseDate
// if baseDate is nil, default to current system time
let totalSecs = sysDate.timeIntervalSince(baseDate ?? Date())
if (totalSecs.sign != .minus) {
// a positive number is valid
// % calculates seconds of current cycle
let currSecs = totalSecs.truncatingRemainder(dividingBy: lunarSecs)
let currFrac = currSecs / lunarSecs
let currDays = currFrac * lunarDays
...
}
...
}
```
## Using the moon phase to build the menu
Now that we have `currDays`, we need to find the right moon phase. I created a [`Dictionary`](https://developer.apple.com/documentation/swift/dictionary) (an unordered array of key-value pairs) named `phaseArray` in my `ApplicationDelegate` class containing all the information I need to calculate the correct phase and display the right icon.
A few notes:
* I originally used emoji to build my menu, and left that info in my dictionary after I switched to icons.
* There are two **New Moon** records, because of when the New Moon actually begins in the lunar cycle.
* The dictionary is an array of [`String`](https://developer.apple.com/documentation/swift/string) keys, the value of each being a [`Dictionary`](https://developer.apple.com/documentation/swift/dictionary) of [`String`](https://developer.apple.com/documentation/swift/string) keys paired to [`NSString`](https://developer.apple.com/documentation/foundation/nsstring) values. This is to enable converting the values to numbers when necessary.
```swift
class AppDelegate: NSObject, NSApplicationDelegate {
...
// static array of possible lunar values
let phaseArray:[String:[String:NSString]] = [
"new" : [
"nice" : "New Moon",
"start" : "0",
"end" : "1",
"icon" : "🌑"
],
"waxc" : [
"nice" : "Waxing Crescent",
"start" : "1",
"end" : "6.38264692644",
"icon" : "🌒"
],
"firstq" : [
"nice" : "First Quarter",
"start" : "6.38264692644",
"end" : "8.38264692644",
"icon" : "🌓"
],
"waxg" : [
"nice" : "Waxing Gibbous",
"start" : "8.38264692644",
"end" : "13.76529385288",
"icon" : "🌔"
],
"full" : [
"nice" : "Full Moon",
"start" : "13.76529385288",
"end" : "15.76529385288",
"icon" : "🌕"
],
"wang" : [
"nice" : "Waning Gibbous",
"start" : "15.76529385288",
"end" : "21.14794077932",
"icon" : "🌖"
],
"lastq" : [
"nice" : "Last Quarter",
"start" : "21.14794077932",
"end" : "23.14794077932",
"icon" : "🌗"
],
"wanc" : [
"nice" : "Waning Crescent",
"start" : "23.14794077932",
"end" : "28.53058770576",
"icon" : "🌘"
],
"new2" : [
"nice" : "New Moon",
"start" : "28.53058770576",
"end" : "29.53058770576",
"icon" : "🌑"
],
"default" : [
"nice" : "Failed to calculate.",
"icon" : "🌙"
]
]
...
}
```
### Searching the dictionary
To find the current phase, we're going to use the [`Dictionary.filter()`](https://developer.apple.com/documentation/swift/dictionary/filter(_:)) method of `phaseArray`. This method can take a function to filter for the correct entry, and will return a dictionary of the same type as the original one.
We'll look at the filter function first. This pair of conditionals will return the phase for which `currDays` falls between the `start` and `end` values. This should always return a single [`Dictionary`](https://developer.apple.com/documentation/swift/dictionary) entry.
Swift is strongly-typed, so we need to use the same number type for every argument. The function must return a [`Bool`](https://developer.apple.com/documentation/swift/bool), which tells [`Dictionary.filter()`](https://developer.apple.com/documentation/swift/dictionary/filter(_:)) whether to include the entry or skip it.
```swift
@objc func findPhase(start: Double, end: Double, curr: Double) -> Bool {
if ((curr >= start) && (curr <= end)) {
return true
} else {
return false
}
}
```
## Building the menu
Back in our `updateIcon()` function, we're first going to create a new dictionary to hold our filter results. Then we're going to populate this dictionary by calling [`Dictionary.filter()`](https://developer.apple.com/documentation/swift/dictionary/filter(_:)) with our fitler function, `findPhase()`.
```swift
@objc func updateIcon() {
...
if (totalSecs.sign != .minus) {
var newArr = [String:[String:NSString]]()
newArr = phaseArray.filter{
findPhase(
start: $0.value["start"]?.doubleValue ?? 0,
end: $0.value["end"]?.doubleValue ?? 0,
curr: currDays
)
}
...
}
...
}
```
We can make use of [`Dictionary.first()`](https://developer.apple.com/documentation/swift/dictionary/first(where:)), since our array should only have one element. If something went wrong and the filter returned nothing, we'll default to "default".
With this key value, we can populate the menu via the `buildMenu()` function we already created.
```swift
@objc func updateIcon() {
...
if (totalSecs.sign != .minus) {
...
let theKey = newArr.first?.key ?? "default"
buildMenu(key: theKey)
...
}
...
}
```
Finally, we'll close our `if` conditional and display the default menu if the lunar phase calculation failed to return a positive number.
```swift
@objc func updateIcon() {
...
if (totalSecs.sign != .minus) {
...
} else {
// if dateDiff is negative, something went wrong.
buildMenu()
}
}
```
Now that we have our dictionary of lunar phases, we can display the friendly name in our menu. Head back to `buildMenu()` for some updates.
First, if we got a moon phase, we're going to add the friendly phase name to our menu, along with a separator using [`NSMenuItem.separator()`](https://developer.apple.com/documentation/appkit/nsmenuitem/1514838-separator). This menu item isn't going to do anything, so the `action` property is set to `nil`.
Since the text for this menu item might be null, we have to use "safe unwrapping" with the `??` syntax. This is essentially shorthand for `if(nil){...}` and is called the [**nil-coalescing operator**](https://docs.swift.org/swift-book/LanguageGuide/BasicOperators.html#ID72) in Swift.
```swift
@objc func buildMenu(key: String = "default") {
...
let topTxt = phaseArray["default"]?["nice"] as String? ?? ""
// add menu items to the menu
if (!topTxt.isEmpty) {
statusBarMenu.addItem(
withTitle: phaseArray[key]?["nice"] as String? ?? topTxt,
action: nil,
keyEquivalent: ""
)
}
statusBarMenu.addItem(NSMenuItem.separator())
...
}
```
Next, we're going to add a menu item for manually updating the moon phase, just in case something goes wrong with launch or automatic updates. This menu item will call `updateIcon()`, and we'll follow it with another separator.
```swift
@objc func buildMenu(key: String = "default") {
...
statusBarMenu.addItem(
withTitle: "Update moon phase",
action: #selector(AppDelegate.updateIcon),
keyEquivalent: ""
)
statusBarMenu.addItem(NSMenuItem.separator())
...
}
```
## Updating `applicationDidFinishLaunching()`
Now that `updateIcon()` calls `buildMenu()`, we need to take a quick detour back to `applicationDidFinishLaunching()` to change our `buildMenu()` call to `updateIcon()`. Your launch function should look like this now:
```swift
func applicationDidFinishLaunching(_ aNotification: Notification) {
// build the icon and menu
updateIcon()
// listen for theme changes (dark and light)
themeListener()
}
```
## Updating the lunar phase in the background
This application is meant to run silently in the background, with just one unobtrusive menu bar icon. For the icon to stay updated, we need to add a timer that reruns `updateIcon()` regularly.
We're going to accomplish this with a [`Timer`](https://developer.apple.com/documentation/foundation/timer). This is where Swift implements something pretty interesting that I don't believe is available in .NET - the system loop.
MacOS makes available a common system loop for running timer functions, which allows the OS to manage the timer to prevent overconsumption of resources (CPU and memory). This is called [`RunLoop.current.add`](https://developer.apple.com/documentation/foundation/runloop/mode/1408609-common.).
Our timer will run every ten seconds and run `updateIcon()`, which recalculates the moon phase and rebuids the menu.
We need to declare our `timer` in the class definition, so it remains in memory outside of the timer function.
```swift
class AppDelegate: NSObject, NSApplicationDelegate {
...
var lunaTimer = Timer()
...
}
```
We'll set the value of this timer in our `startTimer()` function.
```swift
// start the timer for auto-updating the icon
func startTimer() {
lunaTimer = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(updateIcon), userInfo: nil, repeats: true)
RunLoop.current.add(lunaTimer, forMode: RunLoop.Mode.common)
}
```
Finally, we'll add a reference to `startTimer()` in `applicationDidFinishLaunching()`:
```swift
func applicationDidFinishLaunching(_ aNotification: Notification) {
...
// start the the timer for keeping the icon updated automatically
startTimer()
}
```
The second half of this project notebook is focused on window management - LunaMac includes an **About** window, as well as a second window that displays the changelog, loaded from a text file. You can read all about that in [the next section](/wiki/SwiftWindows).
Loading…
Cancel
Save