Controlling First Responder in multi-window Swift Cocoa Applications

In this article, we will build a Cocoa application with one main window and a secondary key window to demonstrate:

  • Using a segue to open the secondary window
  • Use an NSNotification to switch focus between windows and select a different First Responder
  • Examine three different ways of accessing the main window when it is inactive

When building multi-window Cocoa applications, it is important to understand the concepts behind the Cocoa Event Architecture and Window Layering. Apple provides two excellent overviews of these topics:

(Summary: The current first responder in the key window will respond to keyboard, mouse and other events.)

Step 1: Set up the project in Xcode

Create a new Cocoa Application project in Xcode with the settings:

  • Product Name: WhoIsOnFirst
  • Language: Swift
  • Use Storyboards

Step 2: Set up the Storyboard

In the storyboard, use Control-Option-Command-3 to open the Object Library.

To the View Controller add:

  • A push button with the title: Display Child Window
  • A text field with the Placeholder set to "first"
  • A text field with the Placeholder set to "second"

Drag a View Controller object from the Object Library onto the Storyboard.

To the new View Controller add:

  • A push button with the title: "Back to Main and select Second Field"
  • A text field with the Placeholder set to "third"

Step 3: Add two NSViewController subclasses

In Xcode select File > New > File...

Choose Cocoa Class from the OS X > Source group and click Next

Set the Class to MainViewController

Set the Subclass of: to NSViewController

Uncheck the option "Also create XIB file for user interface"

Repeat these steps to create a Class called ChildViewController

Step 4: Connect the subclasses to the two View Controller Scenes

In the Storyboard, for the main window, select View Controller Scene > View Controller.

Use Option-Command-3 to open the Identity Inspector

Change the Class to MainViewController
This will change the name of the View Controller Scene to Main View Controller Scene

Select Main View Controller Scene > Main View Controller > View

Change the identifier to MainWindowContentView

For the child window, select View Controller Scene > View Controller.

Use Option-Command-3 to open the Identity Inspector

Change the Class to ChildViewController
This will change the name of the View Controller Scene to Child View Controller Scene

Step 5: Implement MainViewController.swift

Open MainViewController.swift and change the implementation to:

import Cocoa

class MainViewController: NSViewController {

    @IBOutlet weak var firstField : NSTextField!

    @IBOutlet weak var secondField : NSTextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do view setup here.
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(MainViewController.setFocusBackToThisWindow), name: "whoisonfirst.backToMain", object: nil)
    }

    func setFocusBackToThisWindow() {
        self.view.window?.makeKeyWindow()
        self.view.window?.makeFirstResponder(secondField)
    }

    func setFocusBackToFirstField() {
        self.view.window?.makeFirstResponder(firstField)
    }
}

In this implementation we:

  • Add two outlets for the two Text Fields
  • Add an observer for an NSNotification named "whoisonfirst.backToMain" Note: This example uses the Swift 2.2 method of referencing ObjC selectors. In earlier versions of Swift, you can use: NSNotificationCenter.defaultCenter().addObserver(self, selector: "setFocusBackToThisWindow", name: "whoisonfirst.backToMain", object: nil)
  • Add a function called setFocusBackToThisWindow. This function:
    • Serves as a handler for the "whoisonfirst.backToMain" NSNotification.
    • Makes the main window the key window and makes the second text field the first responder.
  • Add a function called setFocusBackToFirstField. This function:
    • Makes the first text field the first responder.
    • Will be called when the child window is closed.

Step 6: Implement ChildViewController.swift

Open ChildViewController.swift and change the implementation to:

import Cocoa

class ChildViewController: NSViewController {

    @IBAction func backToMain(sender: AnyObject) {
        NSNotificationCenter.defaultCenter().postNotificationName("whoisonfirst.backToMain", object: nil)
    }    
}

In this implementation we add one action that fires the "whoisonfirst.backToMainWindow" NSNotification.

Step 7: Connect the actions, outlets and segues

In the Storyboard, select Main View Controller Scene > Main View Controller.

Use Option-Command-6 to open the Connections Inspector

Connect the firstField and secondField outlets to the first and second Text Fields

Control-drag from the Push Button to the Child View Controller's window and choose Show.

Select Child View Controller Scene > Child View Controller

Use Option-Command-6 to open the Connections Inspector

Connect the backToMain: action to the "Back to Main and select Second Field" Push Button

Step 8: Run the application

Build and Run the application. On launch you should see the window for the Main View Controller with one Push Button and two Text Fields.

Click on the "Display Child Window" button to open the child window.

Click on the "Back to Main and select Second Field" button to make the first window active AND the second text field active.

Discussion:

So far, this implementation is fairly straight-forward. Now we will add a function to ChildViewController overriding the NSViewController function viewWillDisappear(). This function will return focus to the first Text Field of the main window after the child window is closed.

It it important to note that when the main window is not active, calling NSApplication.mainWindow will return a nil, so we need select the window from the windows collection from NSApplication.windows.

In this implementation we will try out three different ways of achieving the desired behaviour:

Option 1 - Getting the first (main) window's view controller and call the setFocusBackToFirstField function on the view controller.

override func viewWillDisappear() {

    // Get the first (main) window's view controller and call a function on the view controller
    let vc = NSApplication.sharedApplication().windows[0].contentViewController as! MainViewController
    vc.setFocusBackToFirstField()
}

This option is simple but relies on the on the main window being at index 0 in the windows array.

Option 2 - Getting the main window's view controller by identifier and call the setFocusBackToFirstField function on the view controller.

override func viewWillDisappear() {

    // Get the main window's view controller by identifier and call a function on the view controller
    let windows = NSApplication.sharedApplication().windows
    for window in windows {
        if (window.contentView?.identifier == "mainWindowContentView") {
            let vc = window.contentViewController as! MainViewController
            vc.setFocusBackToFirstField()
        }
    }  
}

This option is more reliable since it uses the identifier of the window's content view to select the main window.

Option 3 - Getting the main window's view controller by identifier and call makeFirstResponder using a selector

override func viewWillDisappear() {

    // Get the main window's view controller by identifier and call makeFirstResponder using a selector
    let windows = NSApplication.sharedApplication().windows
    for window in windows {
        if (window.contentView?.identifier == "mainWindowContentView") {
            let vc = window.contentViewController as! MainViewController
            window.performSelector(#selector(NSWindow.makeFirstResponder(_:)), withObject: vc.firstField)
        }
    }   
}

This option uses the same technique to select the correct window as Option 2 but by using a selector eliminates the need to use the setFocusBackToFirstField function.

Conclusion

These techniques are especially useful when you are implementing an application with an inspector window. Inspectors often display data that bears a relation to the state of the main windows controls as well as its own data.