added outline to section headers

master
Claire 2 years ago
parent
commit
04941a84c6
  1. 40
      Home.md
  2. 22
      Swift-Windows.md
  3. 10
      User-Prefs.md

40
Home.md

@ -5,7 +5,7 @@
* [Windows with Swift](/wiki/Swift-Windows)
* [Setting (and retrieving) user preferences](/wiki/User-Prefs)
## Introduction
## 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](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.
@ -16,7 +16,7 @@ Swift does things very differently from .NET. It's strongly-typed to an extreme,
And with that, let's look at how the code for *this* project works.
## The Plan
## 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](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:
@ -28,12 +28,12 @@ My needs are minimal:
* About the app
* Quit the app
## Setting up Xcode project
## 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.
### Menu bar icons
### 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.
@ -55,7 +55,7 @@ If all went well, you *should* get no results. If something went wrong, try agai
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.
### Application icon
### 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](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:
@ -89,14 +89,14 @@ If you plan on releasing your app in Apple's App Store, you also need to set up
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
### C. 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"/>
### Removing references to deleted content
### D. Removing references to deleted content
You'll find a default line at the beginning of your `AppDelegate` class:
@ -113,7 +113,7 @@ Delete this line - this is for loading the default application window, which we
Whew. Now that's out of the way, let's get into the fun(ish) stuff.
## Creating a menu bar icon
## IV. 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.
@ -201,7 +201,7 @@ Now we can actually set the icon image (using [NSImage](https://developer.apple.
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
## 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.
@ -209,7 +209,7 @@ Open `Info.plist` and click the plus sign next to **Information Property List**
![](https://abettergeek.com/_media/wiki/git/lunamac/serviceplist.png)
## Building a menu for the 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.
@ -257,7 +257,7 @@ 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
## 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`](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.
@ -308,9 +308,9 @@ func themeListener() {
}
```
## Calculating the right lunar phase
## VIII. Calculating the right lunar phase
### Overview
### A. 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.
@ -326,7 +326,7 @@ Here's the overview:
This is all just basic arithmetic, so let's get into the Swift implementation.
### 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.
@ -389,7 +389,7 @@ Apple does at least provide a useful function here to do the date math for us, a
}
```
## Using the moon phase to build the menu
## 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`](https://developer.apple.com/documentation/swift/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.
@ -466,7 +466,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
```
### Searching the dictionary
### A. 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.
@ -485,7 +485,7 @@ Swift is strongly-typed, so we need to use the same number type for every argume
}
```
## Building the menu
## 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()`](https://developer.apple.com/documentation/swift/dictionary/filter(_:)) with our filter function, `findPhase()`.
@ -581,7 +581,7 @@ Next, we're going to add a menu item for manually updating the moon phase, just
}
```
## Updating `applicationDidFinishLaunching()`
## 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:
@ -596,7 +596,7 @@ func applicationDidFinishLaunching(_ aNotification: Notification) {
}
```
## Updating the lunar phase in the background
## 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.
@ -637,7 +637,7 @@ func applicationDidFinishLaunching(_ aNotification: Notification) {
}
```
## Preventing duplicate menu bar icons
## 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`](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.

22
Swift-Windows.md

@ -6,17 +6,17 @@
* Windows with Swift (this page)
* [Setting (and retrieving) user preferences](/wiki/User-Prefs)
## Introduction
## I. Introduction
I'm just going to preface this by saying: holy hell, making windows work in Xcode and Swift is no small feat.
This project features two windows: one that shows some basic information about the application, and another that shows the application's changelog, which will be loaded from a text file packaged with the application.
## The "About" window
## II. The "About" window
<img src="https://abettergeek.com/_media/wiki/git/lunamac/aboutwindow.png" align="left">
### Building the window
### A. Building the window
Hit **⌘+N** or right-click in the project navigator to add a new file to your project. Select **Window** in the **User Interface** section. Click **Next**, give your new file a name, and click **Create**. My file is named `InfoBox.xib`.
@ -28,13 +28,13 @@ Make sure you've selected the **Window** (not the **View** under the window), an
Under **Title**, enter a display title (mine is "About LunaMac"), and under **Controls**, uncheck the boxes next to **Minimize** and **Resize**. Finally, make sure **Visible at launch** is checked. This refers the launch of the *window*, not the *application*.
### Adding the controls
### B. Adding the controls
To add content to the window, we need to bring up the **Objects Library** - you can find this under **View** > **Libraries** > **Show Library** from the application menu, hit **Shift+⌘+L**, or click the **Libraries** icon in the toolbar of your Xcode window (it looks like a square inside a circle). Search for the word **label**, and drag a **Label** to the window. Change the **Title** attribute to your application name. Drag a **Multi-line Label** to the window, and position it below the label we just created. Change the **Title** attribute to a short summary (or other information) about your application.
The application icon in this window also serves as a button to open the changelog window. Open the **Objects Library** again, and search for and add an **Image Button**. Change the **Image** attribute of this button to the name of your application icon (in my case, `LunaMac`). Resize and position this button to your liking.
### Activating the window
### C. Activating the window
We're going to go back to our main `AppDelegate.swift` file. In your `buildMenu()` function, add a new menu item above the **Quit application** menu item. This will activate a new `showAbout()` function when clicked.
@ -74,7 +74,7 @@ Launch your application, show the "About" window from your icon's menu, and bask
This took awhile to figure out, so maybe I can save you the headache of blindly searching for answers and details.
### Preventing window duplication
### D. Preventing window duplication
Swift follows the MVC model, in which models, views, and controls are separate components of your application. Because of this, we need to use an instance [`NSViewController`](https://developer.apple.com/documentation/appkit/nsviewcontroller), which will give us access to window triggers that fire when the window is opened, closed, changed, etc.
@ -170,7 +170,7 @@ If you look at the source code (XML) view for `InfoBox.xib`, there's now a `<vie
Launch your app, and verify that everything works. Now, when you select "About..." from the status bar icon's menu, if the window's already open, it will display, rather than creating a new instance of the window.
### Adding an action to the button
### E. Adding an action to the button
Recall when we created `InfoBox`, we included an **Image Button** using the application's icon. Now we're going to associate a function with this button. First, we'll create a special kind of function using [`@IBAction`](https://developer.apple.com/documentation/uikit/uibutton) in our `AppDelegate` class. *Note: this documentation relates to UIKit, which we aren't using in this application. The `@IBAction` concept is the same.*
@ -217,7 +217,7 @@ Alternatively, you can `Ctrl`-and-drag from the button's **Button Cell** to the
Now we need to make a changelog dialog, and the code to load it and its contents.
## The Changelog window
## III. The Changelog window
Add a new **Window** document to your project named `Changelog.xib`. Next, add an **Empty** file - this file type can be found in the **Other** section under **MacOS** templates. Name this file `changelog.txt` - we'll load the contents of this file into the changelog window, making it easy to maintain the changelog separate from the application.
@ -243,7 +243,7 @@ Since we're also going to be changing the contents of our **Scrollable Text View
</viewController>
```
### Tracking the window status
### A. Tracking the window status
Displaying the Changelog window also requires a [`Bool`](https://developer.apple.com/documentation/swift/bool) value to track whether it's already open, so add one to your `AppDelegate` class definition in `AppDelegate.swift`.
@ -256,7 +256,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
```
### Handling the view controller
### B. Handling the view controller
Then, add the code to `toggleWindow()` in `ViewController.swift` to handle showing the changelog window, similar to how we handled the info window.
@ -274,7 +274,7 @@ func toggleWindow(boolShow: Bool = false) {
Now you can launch your application and access both the info and changelog windows. We're in the home stretch!
### Loading the changelog file
### C. Loading the changelog file
In our `ViewController` class, we're going to override [`NSViewController.viewDidAppear()`](https://developer.apple.com/documentation/appkit/nsviewcontroller/1434455-viewdidappear) to load and display the contents of `changelog.txt` in our changelog window.

10
User-Prefs.md

@ -5,11 +5,11 @@
* [Windows with Swift](/wiki/Swift-Windows)
* Setting (and retrieving) user preferences (this page)
## Introduction
## 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.
## Setting a user preference
## 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`](https://developer.apple.com/documentation/foundation/userdefaults/1416603-standard).
@ -50,7 +50,7 @@ Once we set our user preference, we'll run `updateIcon()`, which will rebuild th
}
```
## Adding new icons
## III. Adding new icons
<img src="https://abettergeek.com/_media/wiki/git/lunamac/newimages.png" align="left">
@ -59,7 +59,7 @@ Next, we need to add icons for these new icon sets. As you'll see below, the alt
Remember to run `xattr -cr .` after adding new images to your project.
<br clear="left"/>
## Creating the submenu
## IV. Creating the submenu
<img src="https://abettergeek.com/_media/wiki/git/lunamac/prefmenu.png" align="right">
@ -180,7 +180,7 @@ You can run your application now, and test the user setting. Change the setting,
Now we can move on to actually displaying different icons based on the user preference.
## Displaying the user's preferred icon set
## 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).

Loading…
Cancel
Save