Windows and Window Controllers in OS X Tutorial

Gabriel Miro

Windows250x250

Windows are the “containers” for all the UI associated with all OS X apps. They define the area on the screen that the app is currently responsible for, and allow users to interact using a well-understood multi-tasking paradigm. OS X apps fall into one of the following categories:

  • Single-window utility like Calculator
  • Single-window library-style “shoebox” like Photos
  • Multi-window document-based like TextEdit

Regardless of which category an app falls into, nearly every OS X app makes use of MVC (Model-View-Controller), a core design pattern.

In Cocoa, a window is an instance of the NSWindow class, and the associated controller object is an instance of the NSWindowController class. In a well-designed app, you typically see a one-to-one relationship between a window and its controller. The model layer varies according to your app type and design.

In this windows and window controllers in OS X tutorial, you’ll create BabyScript, a multi-window document-based app inspired by TextEdit. In the process, you’ll learn about:

  • Windows and window controllers
  • The document architecture
  • NSTextView
  • Modal windows
  • The menu bar and menu items

Prerequisites

This tutorial is aimed at beginners. Having said that, it requires basic knowledge of the following topics:

  • Swift
  • Xcode, and in particular, storyboards
  • Creating a simple Mac (OS X) app

If you’re not familiar with any of the above, you might want to brush up with some other tutorials on this site:

Getting Started

Launch Xcode, and choose File / New / Project…. Select OS X / Application / Cocoa Application, and click Next.

1-CocoaApp

In the next screen, fill out the fields as indicated below, but enter your own name (or superhero alias) instead of mine.

2-XcodeTemplate

Click Next and save your project.

Build and run, and you will see:

3-FirstWindow

To open more documents, select File / New. All the documents are positioned in the same place, so you’ll only see the top document when you click and drag them around. It’s not a desirable effect, so add fixing this to your to-do list, but don’t dive in yet.

4-Open-Many

You can also use the Windows menu to bring windows to the front.

5-Bring-ToFront

Documents: Under the Hood

Now you’ve seen it in action, let’s take a few minutes to see how it works.

Document Architecture

A document is a container for data in memory that you can view in a window. Eventually, it can be written to or read from a disk or iCloud. Programmatically speaking, a document is an instance of the NSDocument class that acts as the controller for the data objects—aka model—associated with the document.

The other two major classes in the document architecture are NSWindowcontroller and NSDocumentController. These are the roles of each primary class:

  • NSDocument: Creates, presents and stores document data
  • NSWindowController: Manages a window in which a document is displayed
  • NSDocumentController: Manages all of the document objects in the app

Visuals are nice too, so here’s a chart that shows how the classes work together:
DocArchitecture

Disabling Document Saving and Opening

The document architecture also provides the saving/opening mechanism for documents.

In Document.swift, you’ll find the empty implementation of dataOfType, for writing, and readFromData for reading. Saving and opening documents is outside the scope of this tutorial, so you’ll make some changes to prevent confusing behavior.

In Document.swift, remove autosavesInPlace:

  override class func autosavesInPlace() -> Bool {
    return true
  }

Now you’ll disable all menu items related to opening and saving, but before you do, notice that all the functionality you would expect is already there. For example, select File / Open and the finder dialog box, including controls, sidebar, toolbar etc., is there:

OpenDialog

When it has no action defined, a menu item is rendered useless. The same disabling effect happens when there is no object in the responder chain that responds to the selector associated with the action.

Hence, you’ll disconnect actions that are defined for the menu items you need to disable.

no-saving-for-you

In the storyboard, select File / Open in Main Menu in the Application Scene.

Select the Connections Inspector and click Open. As you can see, it connects to the first responder via the openDocument selector, aka the first object to respond to this selector in the responder chain. Delete this connection by clicking on the x as shown below:

TargetAction

Repeat this step for Save, Save As and Revert to Saved.

Build and Run. Toggle the Open menu and check that it looks like this:

7-OpenMenu

Window Position

When you run BabyScript, the window opens near the left edge, but somewhat below the center of the screen.

Why does it choose this location?

Go to the storyboard, and in the outline view select Window, and then select the Size Inspector. Run BabyScript – or bring it to the front – and you should see the following screen:

5-WindowSizeInspector

Entering numeric values for the X and Y under Initial Position is one way to set the window’s position. You can also set it graphically by dragging the gray rectangle just below.

6-QuartzCoordinates

Note: The origin of a visual object (window, view, control, etc.) in Cocoa is the lower-left corner. Values increase as you go up and to the right in the coordinate system.

In contrast, many graphic environments, especially with iOS, the origin is in the upper-left, and values increase going down and to the right.

Suppose that the desired opening position for your window is 200 points offset horizontally and vertically from the top-left. You can set this with Xcode in the window’s Size Inspector or do it programmatically.

Set the Window’s Position with Interface Builder

It’s a safe bet that users will launch BabyScript on various screen sizes. Since your app doesn’t have a crystal ball to see what screen size it will open on at compile time, Xcode uses a virtual screen size and uses a concept similar to Auto Layout to determine the window position at run time.

To set the position, you’ll work with the X and Y values under Initial Position and the two drop-down menus.

Go to Initial Position to set window’s opening position in terms of screen coordinates. Enter 200 for both X and Y and select Fixed from Left and Fixed from Bottom in the upper and lower drop-downs, respectively. This sets the window’s origin at 200 points offset in both the x and y directions.

Build, run and you should see:

Follow these steps to pin the window to the upper-left corner:

  1. Drag the gray rectangle in the preview to the top-left of the virtual screen – this changes the initial position.
  2. Enter 200 for X and for Y, enter the maximum value minus 200, in this case 557.
  3. Select Fixed from Top in the lower drop-down.

The right side of the image below also shows what you should enter and where:

Window757557

Note: OS X remembers window positions between app launches. In order to see the changes you made, you need to actually close the app window – not just rebuild and run.

Close the window(s), and then build and run.

10-Window200x200Xcode

Set the Window’s Position Programmatically

Now you’ll accomplish the same task you did with Interface Builder, but this time you’ll do it programmatically.

The reason to take the “hard way” is two-fold. First, you’ll walk away with a better understanding of NSWindowController. Second, it’s a more flexible and straightforward approach.

At run time, the app will perform the final positioning of the window once it knows the screen size.

In the Project Navigator select the BabyScript group, then select File / New / File... From the dialog that pops up, select OS X / Source / Cocoa Class and click Next.

Create a new class called WindowController and make it a subclass of NSWindowController. The checkbox for XIB should be unchecked, and the Language should be Swift.

11-WindowController

Choose a location to save the new file. Once done, you’ll see a new file named WindowController.swift appear in the group BabyScript.

Go to the storyboard, and in Outline View select Window Controller from the Window Controller Scene. Choose the Identity Inspector, and from the Class drop-down select WindowController.

12-WindowController-2

When windowDidLoad is called the window will have completed loading from the storyboard, so any configuration you do will override the settings in the storyboard.

Open WindowController.swift and replace windowDidLoad with the following:

  override func windowDidLoad() {
    super.windowDidLoad()
    if let window = window, screen = window.screen {
      let offsetFromLeftOfScreen: CGFloat = 20
      let offsetFromTopOfScreen: CGFloat = 20
      let screenRect = screen.visibleFrame
      let newOriginY = CGRectGetMaxY(screenRect) - window.frame.height
        - offsetFromTopOfScreen
      window.setFrameOrigin(NSPoint(x: offsetFromLeftOfScreen, y: newOriginY))
    }
  }

This logic positions the window’s top-left corner 20 points offset in both the x and y directions from the top-left of the screen.

As you can see, NSWindowController has a window property and NSWindow has a screen property. You use these two properties to access the geometry of the window and the screen.

After ascertaining the height of the screen, your window’s frame is subtracted along with the desired offset. Remember the Y value increases as you move upwards on the screen.

visibleFrame excludes the areas taken by the dock and menu bar. If you don’t take this into account, you might end up with the dock obscuring part of your window.

When you enable dock and menu hiding, visibleFrame may still be smaller than frame, because the system retains a small boundary area to detect when to show the dock.

Build and run. The window should sit 20 points in each direction from the screen’s top-left corner.

Cascading Windows

To further improve your windows’ position, you’ll introduce Cascading Windows, meaning an arrangement of windows that overlap one another while leaving the title bar for each window visible.

Add the following below the definition of WindowController in WindowController.swift:

  required init?(coder: NSCoder) {
    super.init(coder: coder)
    shouldCascadeWindows = true
  }

You’re setting the shouldCascadeWindows property of NSWindowController to true by overriding the required init method of NSWindowController.

Build and run the app, and then open five windows. Your screen should look a little bit more friendly:

13-CascadingWindows

Make BabyScript a Mini Word Processor

Now comes the most exiting part of this tutorial. With just two little lines of code and the addition of an NSTextView control to your window’s contentView, you can add functionality that will blow your mind!

The Content View

Upon creation, a window automatically creates two views: an opaque frame view with a border, title bar, etc., and a transparent content view accessible via the window’s contentView property.

The content view is the root of the view hierarchy of a window, and you can replace the default with a custom view. Note that to position the content view, you must use the setContentView method of NSWindow — you can’t position it with the standard setFrame method of NSView.

ContentView

Note: If you’re an iOS developer, please note that in Cocoa, NSWindow is NOT a subclass of NSView. In iOS, UIWindow is a special subclass, of UIView. UIWindow itself is the root of the view hierarchy, and it’s simply playing the role of the content view.

Add the Text View

Remove the text field that says “Your document contents here” from the contentView in the storyboard, by selecting it and pressing delete.

To create the new NSTextField that will form the main part of your UI follow these instructions:

  1. In the storyboard, open the Object Library.
  2. Search for nstextview.
  3. Drag Text View and drop it on the content view.
  4. Resize the text view so its inset is 20 points on each side from the content view.
  5. In the Outline View, select Bordered Scroll View. Note that the text view is nested in the Clip View, which is nested inside a scroll view.
  6. Select the Size Inspector. Enter 20 for X and Y, 440 for Width and 230 for Height

15-TextViewCreation

Build and run — you should see the following:

16-EmptyText

Look at that friendly, blinking text insertion point inviting you to enter your text! Start your manifesto, or just keep it simple with “Hello World”, and then select the text. Copy it with File / Copy or command – C, and then paste several times, just to get a feeling for the app.

Explore the Edit and Format menu to get the idea what’s available. You might have noticed that the Font / Show Fonts is disabled. You’re going to enable it now.

Enable the Font Panel

In the storyboard, go to the main menu, click on the Format menu, then on Font, then follow with a click on Show Fonts.

Go to the Connections Inspector and you’ll see that no actions are defined for this menu item. This explains why the menu item is disabled, but what do you connect it to?

Apparently, the action is already defined in the code imported indirectly by Xcode, you just need to make the connection.

Right-click Show Fonts and drag it to the First Responder in the Application Scene, and then release the mouse. A small window with a scrollable list of all the actions defined will pop up. Look for and select orderFrontFontPanel. You can also start typing it to find it more quickly.

17-ConnectFontPanel

Now, take a look at the Connections Inspector with Show Fonts selected. You’ll see the menu is now connected to orderFrontFontPanel of the first object in the responder chain that responds to this selector.

Build and run the app, then enter some text and select it. Choose Format / Font / Show Fonts to open the fonts panel. Play with the vertical slider on the right side of the font panel, and observe how the text size changes in real time.

18-FontPanel

Wait, but you didn’t enter yet a single line of code regarding the text view, yet you have the power to change the size. You’re amazing!

Word Processing...Like A Boss

Initialize the Text View with Rich Text

To see the full power of the app, download some formatted text from here, and use it as the initial text for the text view.

Open it with TextEdit, select all and copy it to the clipboard. Go to the storyboard, select the Text View, then Attributes Inspector and paste the text into the Text Storage field.

20-RichText

Build and run, and you should see:

21-EditMe

Use Auto Layout

You do have the ability to scroll text that doesn’t fit the current window, but try to resize the window.

Oops! The text view does not resize with the window.

TextNoGrow

It’s a simple fix with Auto Layout.

Note: Auto Layout assists both you in both Cocoa and iOS with your app’s UI. It creates a set of rules that define the geometric relationship between the elements, and you define these relationships in terms of constraints.

With Auto Layout, you create a dynamic interface that responds appropriately to changes in screen size, window size, device orientation and localization.

There’s more to it than that, but for the sake of this tutorial, all you need to do is follow the few simple steps below — you can learn more about Auto Layout later. Here are a couple of good tutorials to check out: Beginning Auto Layout Tutorial in iOS 7, Part 1 and Part 2.

In the storyboard’s Outline View, select Bordered Scroll View, and click on the Pin button at the bottom-right of the canvas.

Click on each of the four little red bar constraints; the broken faded red will turn to solid red. Click at the bottom on the button that reads Add 4 Constraints.

PinTextView

Build and run, and watch how both the window and text view resize together:

22-AutoLayoutFixed

Show the Ruler by Default

To show the ruler automatically when a window opens, you’ll need an IBOutlet in the code. Select Format / Text / Show Ruler from the menus. In ViewController.swift, add one line into the viewDidLoad method toggleRuler, and add an IBOutlet above the method as shown below:

  @IBOutlet var text: NSTextView!
 
  override func viewDidLoad() {
    super.viewDidLoad()
    text.toggleRuler(nil)
  }

Now you’ll connect the text view to the view controller in the storyboard.

In the storyboard, right-click on the ViewController, hold and drag into the text view until it highlights, and then release the mouse. A small window with the list of Outlets will show itself. Select the text outlet:

23-TextConnect

Build and run, and now the window by default shows the ruler by default:

RulerShowing

So just as I promised, with two lines of code and the storyboard, you have created a mini word processor – Chapeau, Apple!

Modal Windows

You can make a window run in a modal fashion. The window still uses the app’s normal event loop, but input is restricted to the modal window.

There are two ways to utilize a modal window. You’ll call the runModalForWindow method of NSApplication. This approach monopolizes events for the specified window until it is gets a request to stop, which you can invoke by stopModal, abortModal or stopModalWithCode.

For this case, you’ll use stopModal. The other way, called a modal session, is not covered by this tutorial.

Add a Word Count Window

You’ll add a modal window that counts words and paragraphs in the active window. It has to be modal because it’s associated with a specific window and a specific state.

 

From the Object Library, drag a new window controller to the canvas. This creates two new scenes: a window controller scene and a view controller scene:

NewScenes

Select Window from the new window controller scene and use the Size Inspector to set its width to 300 and height to 150. Select View from the new view controller scene and resize it to match the window:

WordCountReduced

Since Word Count is a modal, having the close, minimize and resize buttons in its title bar would be bizarre, and a violation of HIG (Apple’s Human Interface Guidelines).

For the Close button, it would also introduce a serious bug because clicking the button will close the window, but won’t call stopModal. So, the app will forever stay in a “modal state”.

Removing Buttons from a Modal

In the storyboard, select the Word Count window and choose Attributes Inspector. Uncheck Close, Minimize and Resize. Also change the Title to Word Count.

WordCountAppearance

Now you’ll add four label controls and a push button from the Object Library to the contentView of the Word Count window.

Select the Attributes Inspector. Change the labels’ titles to Word Count, Paragraph Count, 0 and 0 respectively. Also change the alignment for the two 0 labels to right justified. Change the push button title to OK.

WordCountFields

Next on the list is creating a subclass for the Window Count ViewController. Select File / New / File.., choose OS X / Source / Cocoa Class. In the Choose Options dialog, enter WordCountViewController in the Class field and NSViewController in the Subclass of field.

WordCountViewController

Click Next and create the new file. Confirm that WordCountWindowControll.swift is now in the project navigator.

Go to the storyboard. Select the proxy icon for the view controller in the view controller scene for word count. Open the Identity Inspector, and select WordCountViewController from the Class drop-down. Note how the name on the canvas and the Outline View changed from the generic name to Word Count View Controller.

SetWCViewController

Create the Count Labels

Now you’ll create outlets for the two labels that show the count values — the two 0 labels. Under the class definition for WordCountViewController.swift, add the following:

  @IBOutlet weak var wordCount: NSTextField!
  @IBOutlet weak var paragraphCount: NSTextField!

In the storyboard, right-click on the proxy icon for the word count view controller, drag over the top-most 0 label and release when the control highlights. From the Outlets list that pops up, select wordCount.

Repeat the same for the lower 0 label, but this time select paragraphCount. Check for each of the labels in the Connections Inspector that the Outlets are connected like this:

WordCountConnected

In a few moments, you’ll add code to programmatically load the word count window controller. This requires that it have a storyboard ID. Select the window controller of the word count window from the storyboard. Select the Identity Inspector, and in Storyboard ID enter Word Count Window Controller:

WCControllerStoryboardId

Show Me the Modal

Now for the basic logic to show the modal window. In the document window’s view controller, find and select ViewController.swift add the code below under viewDidLoad:

  @IBAction func showWordCountWindow(sender: AnyObject) {
 
    // 1
    let storyboard = NSStoryboard(name: "Main", bundle: nil)
    let wordCountWindowController = storyboard.instantiateControllerWithIdentifier("Word Count Window Controller") as! NSWindowController
 
    if let wordCountWindow = wordCountWindowController.window, textStorage = text.textStorage {
 
      // 2
      let wordCountViewController = wordCountWindow.contentViewController as! WordCountViewController
      wordCountViewController.wordCount.stringValue = "\(textStorage.words.count)"
      wordCountViewController.paragraphCount.stringValue = "\(textStorage.paragraphs.count)"
 
      // 3
      let application = NSApplication.sharedApplication()
      application.runModalForWindow(wordCountWindow)
    }
  }

Take it step-by-step:

  1. Instantiate the word count window controller, using the storyboard ID you specified before.
  2. Set the values retrieved from the text view in the word count window count outlets
  3. Show the word count window modally

Note: In step two, you passed data between two view controllers. This is similar to what you’d usually do in a prepareForSegue method when a segue is involved in the transition. Since showing a modal window is done directly with a call to runModalForWindow and there’s no segue involved, you pass the data just before the call.

Go Away, Modal

Now you’ll add code to dismiss the word count window. In WordCountViewController.swift, add the following method below the paragraphCount outlet:

  @IBAction func dismissWordCountWindow(sender: NSButton) {
    let application = NSApplication.sharedApplication()
    application.stopModal()
  }

This is an IBAction that should be invoked when the user clicks OK on the word count window.

Go to the storyboard, right-click on OK, then hold and drag to the proxy icon of the word count view controller. Release the mouse and select dismissWordCountWindow: from the presented list:

ConnectOK

Add UI to Invoke It

The only thing left to present the window is adding the UI to invoke it. Go to the storyboard, and in the Main Menu click Edit. From the Object Library, drag a Menu Item to the bottom of the Edit menu. Select the Attributes Inspector and set the title to Word Count.

WCMenu

Take a moment to create a keyboard shortcut by entering command – K as the key equivalent.

Now you’ll connect the new menu item to the showWordCountWindow method in ViewController.swift.

Go to the storyboard, right-click on the Word Count menu item, hold and drag over First Responder in the application scene. Select showWordCountWindow from the list.

ConnectWCMenuItem

Note: You might wonder why you connected the menu item to the first responder, but not directly to showWordCountWindow. It’s because the document view’s main menu and view controller are in different storyboard scenes, and as such, can’t be connected directly.

Build and run the app, select Edit / Word Count, and voila, the word count window presents itself.

WordCountFinal

Click OK to dismiss the window.

Where To Go From Here?

Here is the final version of BabyScript.

You covered a lot of ground in this windows and window controllers for OS X tutorial! But it’s just the tip of the iceberg as far as what you can do with windows and window controllers.

You covered:

  • The MVC design pattern in action
  • How to create a multi-window app
  • Typical app architecture for OS X apps
  • How to position and arrange windows with Interface Builder and programmatically
  • Using Auto Layout to resize a view with its window
  • Using modal windows to display additional information

And more!

I strongly recommend that you explore the huge amount of documentation provided by Apple in El Capitan’s Mac Developer Library. In particular, reference the Window Programming Guide.

For better understanding of Cocoa app design and how it works with the types of apps mentioned at the beginning, check out the Mac App Programming Guide. This document also extends on the concept of multi-window document-based apps, so you’ll find ideas to keep improving BabyScript there.

I look forward to hearing your ideas, experiences and any questions you have in the forums below!

Gabriel Miro

There is a long list of Operating Systems, languages, technologies, companies (large and small) I have worked with. The short version is as follows. Today I am an indie iOS an OS X developer. Started back in the seventies as a system programmer for main frames. I am programming for the Mac since 1984 when the Mac was born. Between 1984-1998 I was the co-owner of a Software House, specialising in multi-lingual word processors and desktop publishing applications for the Mac. Was a freelancer for a few years, then joined IBM as a Senior Engineer in development. After leaving IBM, I returned to my old love, i.e. development for the Apple platforms.

I love cats, photography, cooking, reading, music (listening and playing).

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

Swift Team

... 15 total!

iOS Team

... 34 total!

Android Team

... 15 total!

macOS Team

... 11 total!

Apple Game Frameworks Team

... 12 total!

Unity Team

... 11 total!

Articles Team

... 12 total!

Resident Authors Team

... 15 total!