11 Home
claire edited this page 1 year ago

Code Notebook

Sections

I. 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. 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.

II. 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, 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

III. 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.

A. 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:

xattr -cr .

To double-check the results of this command, you can run:

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. Make sure to clean your build folder (under the Product menu) after running xattr, or you'll continue to get build errors.

B. 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 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:

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.

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).

C. Removing extraneous files

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).

D. Removing references to deleted content

You'll find a default line at the beginning of your AppDelegate class:

@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.

Whew. Now that's out of the way, let's get into the fun(ish) stuff.

IV. Creating a menu bar icon

I started with this tutorial (archive). 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 method of 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.

func applicationDidFinishLaunching(_ aNotification: Notification) {
	// build the icon and menu
	buildMenu()
}

My original code instantiated the system menu bar and our application's status icon in my buildMenu() function. This technically works, but I discovered it came with some annoying bugs - duplicate icons, and the icon's position in the status area didn't persist if it was moved - every icon update moved the icon back to the end of the status area.

To solve these bugs, we're going to handle our declarations in the class definition, rather than in any specific function. This ensures our objects persist in memory, even when our application is running in the background.

First, we need NSStatusBar, which refers to the status area of the system menu bar (where your clock and system indicators like battery and wifi live). In Windows parlance, this is called the system tray. We'll create a new item for this status bar using NSStatusItem, and a menu for our icon using NSMenu. The title property of NSMenu won't display in the UI.

class AppDelegate: NSObject, NSApplicationDelegate {
    // get system-wide menu bar
    let statusBar = NSStatusBar.system

	// make sure applet stays in memory
	var statusBarItem: NSStatusItem?
    
    // create a menu for the icon
    let statusBarMenu = NSMenu(title: "LunaMac Menu")
	...
}

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.

To prevent issues with icon duplication in the menu bar, we're going to connect our statusBarItem to our statusBar instance at runtime, rather than in any custom function.

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 the NSStatusItem.squareLength constant with NSStatusBar.statusItem().

Note that this should be the first thing your applicationDidFinishLaunching() function calls. This ensures the menu bar icon is drawn immediately, even if the application isn't finished loading. I discovered that putting this assignment at the end of the load function caused a noticeable lag between launching the application and displaying the icon.

func applicationDidFinishLaunching(_ aNotification: Notification) {
	// set icon size to square
	statusBarItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
	...
}

Now we can go back to our buildMenu() function. The very first thing we need to do is empty our statusBarMenu, in case it contains any existing menu items. This is straightforward using NSMenu.removeAllItems().

@objc func buildMenu() {
	statusBarMenu.removeAllItems()
}

To assign an icon image to statusBarItem, we need a way of knowing which moon phase to display, so we'll add a string argument to buildMenu(), which will contain a unique keyword for the current phase.

My app also displays a specific icon if the lunar phase calcuation fails for some reason, so the default icon name, default, is also the default value of this argument.

By setting a value for our argument in the function declaration, the argument becomes optional. Note that there are no positional arguments in Swift; every argument must be named when passed to a function.

@objc func buildMenu(key: String = "default") {
	...
}

MacOS doesn't require file extensions (and never has). I do most of my work in Windows, so I always make sure my files include the correct extension. You don't need to add the .png to the icon name if your files don't include the extension.

Now we can actually set the icon image (using NSImage) on our menu bar item.

@objc func buildMenu(key: String = "default") {
	...
	// 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.

V. 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.

VI. 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.

@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.

@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() method of NSApp.

// quit application
@objc func quitApp() {
	NSApp.terminate(self)
}

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.

VIII. 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. 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.

@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 that fires when the system them is changed between dark and light.

The extension block goes outside your AppDelegate class definition.

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.

func themeListener() {
	DistributedNotificationCenter.default.addObserver(
		self,
		selector: #selector(updateIcon),
		name: .AppleInterfaceThemeChangedNotification,
		object: nil
	)
}

VIII. Calculating the right lunar phase

A. Overview

I can't take credit for figuring out the math on this. I was lucky to find this excellent PHP implementation (archive), which lays out the math in detail.

Here's the overview:

  1. Use the lunar cycle constant 29.53058770576 (LunarDays), 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 baseDate.
  3. Calculate the time (in seconds) between the user date and baseDate as totalSecs.
  4. If the result is positive, it's valid, so calculate the modulus (remainder) of totalSecs divided by lunarSecs, which will return the total 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.

B. 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 current system date and time with Date. This is always in UTC.

// 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() object and pass that to an instance of a Calendar() object.

@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() to get the total seconds between our full moon baseline and the current system time.

@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
		...
	}
	...
}

IX. 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 (an unordered array of key-value pairs) named phaseArray in my AppDelegate 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 keys, the value of each being a Dictionary of String keys paired to NSString values. This is to enable converting the values to numbers when necessary.
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" : "🌙"
		]
	]
	...
}

A. Searching the dictionary

To find the current phase, we're going to use the 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 entry.

Swift is strongly-typed, so we need to use the same number type for every argument. The function must return a Bool, which tells Dictionary.filter() whether to include the entry or skip it.

@objc func findPhase(start: Double, end: Double, curr: Double) -> Bool {
	
	if ((curr >= start) && (curr <= end)) {
		return true
	} else {
		return false
	}
}

X. 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() with our filter function, findPhase().

We can use the special variable $0 to access the array element currently running through our findPhase() filter function. Since our function takes Double numbers as arguments, we'll convert the string value in phaseArray to a Double - this is why we used NSString instead of String as the values in phaseArray.

@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(), 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.

@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.

@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(). 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 in 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.

 @objc func buildMenu(key: String = "default") {
	...
	statusBarMenu.addItem(
		withTitle: "Update moon phase",
		action: #selector(AppDelegate.updateIcon),
		keyEquivalent: ""
	)
	
	statusBarMenu.addItem(NSMenuItem.separator())
	...
}

XI. 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:

func applicationDidFinishLaunching(_ aNotification: Notification) {
	...
	// build the icon and menu
	updateIcon()

	// listen for theme changes (dark and light)
	themeListener()
}

XII. 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. 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.

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.

class AppDelegate: NSObject, NSApplicationDelegate {
	...
	var lunaTimer = Timer()
	...
}

We'll set the value of this timer in our startTimer() function.

// 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():

func applicationDidFinishLaunching(_ aNotification: Notification) {
	...
	
	// start the the timer for keeping the icon updated automatically
	startTimer()
}

XIII. Preventing duplicate menu bar icons

Version 1.3 of LunaMac includes a small, but annoying bug - if you are using the icon's menu when the timer turns over and reruns the menu script, a second icon will appear in the status area, until you click outside the menu to close it. This is because our Timer is still running when the menu is open, and if it turns over when the status icon is in use, a second icon will appear, and when you close the menu, the first icon disappears from the status area.

This can be easily addressed by adding functions for two NSMenuDelegate events: menuWillOpen() and menuDidClose().

To use these, we need to assign a delegate for our menu, which will be itself. This should go right after your NSMenu.removeAllItems() call in buildMenu().

@objc func buildMenu(key: String = "default") {
	...
	statusBarMenu.delegate = self
	...
}

This is an NSMenuDelegate, which means we need to update our class definition to inherit this type.

class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
	...
}

Delegates in Swift are handlers that make it possible for you to interact with UI elements when certain events are triggered. You can read some of Apple's documentation on delegation here. This is very different from how Windows software development generally works, in which form elements have unique object names which can be directly referenced in your code.

When the menu is open, the timer should stop. We can accomplish this using NSTimer.invalidate().

@objc func menuWillOpen(_ menu: NSMenu) {
	lunaTimer.invalidate()
}

When the menu closes, the timer should be restarted, using our startTimer() function.

@objc func menuDidClose(_ menu: NSMenu) {
	startTimer()
}

You should no longer see any menu or icon duplication issues, and your application icon should persist in a set location in your system menu bar.

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.