Life in Technicolor
iOS MacColor is a fundamental and important aspect of app development. It helps identify brand, evokes emotion, and can even help us determine things like...
6 min read
Mar 25, 2019
So you’re ready to start writing your first macOS application. The project has been created in Xcode (Cocoa app, no storyboards, Swift), and sifting through the generated project structure you happen upon the AppDelegate
file. This might be your first time writing a computer program, or perhaps this is your first on an Apple platform. You may even be giving macOS a look after years of iOS development. No matter what path you traveled to make it to this point, the following line in AppDelegate
is our subject for today:
@IBOutlet weak var window: NSWindow!
On macOS, you need to have a good understanding of NSWindow
, because you’ll be dealing with it quite a bit. NSWindow
provides two basic functions: a screen area to add content such as views and controls, and a mechanism to relay input events. Event handling is a topic worthy of its own discussion, so for this article, we’re going to focus on creating, sizing, and moving windows.
We’re going to get started by creating a window in code, then exploring and adjusting from there. This may seem like a waste of time when we have Interface Builder available to us. It’s not. Interface Builder is a fabulous tool designed to save you time on repetitive tasks. However, there are two main drawbacks I find with it. First, it rarely provides access to all of the customization points available. Second, if the only way you’ve interacted with a class is through IB, you can potentially have a large gap in your understanding.
But enough about that, let’s get something up on the screen.
Before we write any code we have a small amount of cleanup to do. Start by removing the window IBOutlet in your app delegate, then delete the window object in MainMenu.xib. If you build and run now nothing will show onscreen. In applicationDidFinishLaunching
, add the following code:
let contentRect = NSRect(x: 0, y: 0, width: 800, height: 600)
let window = NSWindow(contentRect: contentRect, styleMask: .borderless, backing: buffered, defer: false)
window.orderFront(nil)
Now when you run, you’ll see a rectangle showing up at the bottom left of your screen. I know, you probably expected a bit more, like rounded corners, the stop lights (close, minimize, and full screen), a shadow, etc. Let’s start by adding the stop lights. Just above our window initializer, define a styleMask
local variable:
let styleMask: NSWindow.StyleMask = [.borderless, .closable, .miniaturizable, .resizable]
Use the new styleMask constant in the window initializer and run. Huh, still not what we expected. If we look at the docs for NSWindow.StyleMask
, we have a clue as to why this might be. The .borderless
option, “displays none of the usual peripheral elements”. In order to get the stop lights to show up the way we expect, we need to replace .borderless
with .titled
. This has the additional benefit of providing the other elements we expect for a window—the rounded corners, shadow, etc.
It’s a fair bet to say a good majority of the windows you encounter are of the titled variety. So why even have the borderless option? Well, if you want a window with a non-traditional shape, borderless would be the way to go since it’s the only way to truly get rid of the title bar. A more practical example would be the floating status messages you see when Xcode finishes building your project or running a test suite.
There’s one additional thing to be aware of after switching to a titled window. If the origin you provide for contentRect
would result in the window being occluded by the dock, the window will be moved to compensate.
Next up, let’s take a look at the different options we have for sizing.
There are two levels you can size windows at. The first is rather obvious—the window itself. The second is the content level. There are several properties with similar names between these two levels, and they are: aspect ratio, min and max size, and resize increments.
Of the available initializers for NSWindow, none of them allow you to set the frame of the window itself. Instead, you’ll be specifying either a rect, or a view controller for the content. This is a small detail and can cause unintended behavior in the future.
Let’s say your window needs to have the same aspect ratio as the screen. Easy enough, right? You can grab an instance of the main screen, and set the aspect ratio to the width and height of the screen. So before we order the window front add the following:
if let screen = NSScreen.main {
window.aspectRatio = NSSize(width: screen.frame.width, height: screen.frame.height)
}
When you run the app you’ll see the same thing as before, which is to say not the aspect ratio we set on the window. You can confirm this by watching what happens when you start to resize the window. The window will first snap to the aspect ratio set earlier.
The initial reaction at this point might be to adjust the width and height passed to the initializer. Rather than go that route, let’s use the visual debugger with the code we already have. When you inspect the window you will see the height isn’t 600 like we specified. So how do you ensure the window size is what you expect before you display it? There are two ways to tackle this.
The first way would be to include one additional option in our styleMask: .fullSizeContentView
. Going this route opts us in to layer-backing, and requires a little more when it comes to layout in the form of needing to use contentLayoutRect
or contentLayoutGuide
to ensure our content isn’t obscured by the title bar. The second option would be to set the frame of our window. This second approach will override whatever we passed in for contentRect
. So which way should you go? I tend to go with setting the window’s frame, mainly because for simple operations I get animation for free by passing true to the animate parameter of the setFrame
method.
There’s one last thing I want to cover about NSWindow
right now—movement.
Moving a window comes mostly for free, but it’s still worth addressing, since there are a few points to consider. So let’s start by addressing what I mean by mostly. We’re going to head back to the beginning where we were creating a borderless window.
Out of the box windows are moved by clicking in the title bar and then dragging the window to the new destination. Since there is no title bar in a borderless window, you need to set the isMovableByWindowBackground
property to true.
One last thing on the movement front. I’ve seen a lot of unnecessary window centering code over the years. Please make sure before you write code to center a window you look at the existing center()
method. While the name isn’t truly representative of what it does, the docs are clear as to how it behaves and why, and well, it’s certainly easier to write than centerHorizontallyAndJustSlightlyHigherThanCenterVertically()
.
Ok, this wraps up the intro to NSWindow. There’s still all sorts of cool stuff to cover, but this is a good foundation to start with.
Color is a fundamental and important aspect of app development. It helps identify brand, evokes emotion, and can even help us determine things like...