v1.4 bugfixes

master
Claire 2 years ago
parent
commit
b005118d6e
  1. 124
      Home.md

124
Home.md

@ -1,4 +1,9 @@
# Code Notebook
## Sections
* The main application (this page)
* [Windows with Swift](/wiki/Swift-Windows)
## 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.
@ -88,6 +93,21 @@ To configure your project to use the `Icon.icns` file you imported, edit the exi
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"/>
### Removing references to deleted content
You'll find 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.
Whew. Now that's out of the way, let's get into the fun(ish) stuff.
## Creating a menu bar icon
@ -105,55 +125,67 @@ func applicationDidFinishLaunching(_ aNotification: Notification) {
}
```
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).
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.
```swift
@objc func buildMenu() {
// get system-wide menu bar
let statusBar = NSStatusBar.system
...
}
```
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.
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.
First, we need [`NSStatusBar`](https://developer.apple.com/documentation/appkit/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`](https://developer.apple.com/documentation/appkit/nsstatusitem), and a menu for our icon using [`NSMenu`](https://developer.apple.com/documentation/appkit/nsmenu). The `title` property of [`NSMenu`](https://developer.apple.com/documentation/appkit/nsmenu) won't display in the UI.
```swift
@NSApplicationMain
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.
You probably also noticed a default line at the beginning of your `AppDelegate` class:
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`](https://developer.apple.com/documentation/appkit/nsstatusitem/1534224-squarelength) constant with [`NSStatusBar.statusItem()`](https://developer.apple.com/documentation/appkit/nsstatusbar/1532895-statusitem).
```swift
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
...
@IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
...
// set icon size to square
statusBarItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
}
```
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. 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()`](https://developer.apple.com/documentation/appkit/nsmenu/1518234-removeallitems).
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.
```swift
@objc func buildMenu() {
statusBarMenu.removeAllItems()
}
```
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.
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.
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.
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.
Finally, we can actually set the icon image (using [NSImage](https://developer.apple.com/documentation/appkit/nsimage)) on our menu bar item.
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.
```swift
```swift
@objc func buildMenu(key: String = "default") {
...
// set icon size to square
statusBarItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
}
```
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](https://developer.apple.com/documentation/appkit/nsimage)) on our menu bar item.
```swift
@objc func buildMenu(key: String = "default") {
...
// set the icon filename
let imgName = key + ".png"
@ -176,8 +208,6 @@ Open `Info.plist` and click the plus sign next to **Information Property List**
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") {
...
@ -601,4 +631,48 @@ func applicationDidFinishLaunching(_ aNotification: Notification) {
}
```
## 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`](https://developer.apple.com/documentation/foundation/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`](https://developer.apple.com/documentation/appkit/nsmenudelegate) events: [`menuWillOpen()`](https://developer.apple.com/documentation/appkit/nsmenudelegate/1518156-menuwillopen) and [`menuDidClose()`](https://developer.apple.com/documentation/appkit/nsmenudelegate/1518167-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()`](https://developer.apple.com/documentation/appkit/nsmenu/1518234-removeallitems) call in `buildMenu()`.
```swift
@objc func buildMenu(key: String = "default") {
...
statusBarMenu.delegate = self
...
}
```
This is an [`NSMenuDelegate`](https://developer.apple.com/documentation/appkit/nsmenudelegate), which means we need to update our class definition to inherit this type.
```swift
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.](https://developer.apple.com/documentation/swift/using-delegates-to-customize-object-behavior/) 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()`](https://developer.apple.com/documentation/foundation/nstimer/1415405-invalidate).
```swift
@objc func menuWillOpen(_ menu: NSMenu) {
lunaTimer.invalidate()
}
```
When the menu closes, the `timer` should be restarted, using our `startTimer()` function.
```swift
@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](/wiki/Swift-Windows).
Loading…
Cancel
Save