Swift Development with Cocoa (2015)

Chapter 19. Nonstandard Apps

For the majority of this book, we’ve talked about GUI applications designed to run on either OS X or iOS. These applications receive user input via the mouse, keyboard, or touchscreen, display information via the screen, and are launched by double-clicking them on OS X or tapping them on iOS.

However, not every piece of software that you write is a traditional app. In some cases, you might want to create something that the user doesn’t need to interact with—for example, a background application that automatically downloads files from the Internet. Another case where you don’t want to build a traditional app is when you want to create a preference pane, which the user can access via the System Preferences application.

In this chapter, you’ll learn how to build apps for OS X that don’t fit the mold of standard applications. Specifically, you’ll learn how to build command-line tools (which don’t use a GUI), system preference panes, and applications that add an item to the system-wide menu bar. Finally, you’ll learn how to make apps on iOS that can use more than one screen.

NOTE

This chapter mostly applies to OS X only—on iOS, you can only build apps that display a graphical interface interface, and command-line tools and daemons aren’t supported.

The only exception to this is iOS Apps with Multiple Windows.

Command-Line Tools

The simplest possible application on OS X is a command-line tool. This kind of app never presents a GUI to the user, but instead sends and receives input and output via the command line.

The command line is a common traditional user interface for interacting with the system, and it is often the foundation that graphical user interfaces build on top of. OS X, as a Unix-based system, offers a command-line interface that your applications can use.

In fact, the command line has been involved in many of the apps that you’ve been developing in this book. Whenever you use NSLog or println to log some text, that text goes to the command line. (Xcode redirects it so that you can view it in the IDE, but if you were to launch the app via the Terminal, you’d see it there.)

To demonstrate how to build a command-line tool with Swift, we’ll create a simple app that prints text out to the command line:

1.    Create a new command-line tool project named CommandLine.

Xcode will create a command-line application that uses the Foundation framework and is written in Swift.

NOTE

There are several types of command-line apps, which vary by the framework that your code uses. If you use Foundation, you’ll be writing Swift. If you create a Core Foundation application, you’ll write C.

2.    Replace the main method in main.swift with the following code:

3.  import Foundation

4.   

5.  for var i = 10; i > 0; i-- {

6.      NSLog("%i green bottles, standing on the wall", i);

7.      NSLog("%i green bottles, standing on the wall", i);

8.      NSLog("And if one green bottle should accidentally fall");

9.      NSLog("There'll be %i green bottles, standing on the wall\n\n", i-1);

}

10.Test the application by running it and noting what gets shown in the log.

11.Then test the application in the Terminal. Open the Terminal application, and in Xcode, scroll down to the Products group and open the folder. You’ll see the CommandLine application. Drag it onto Terminal’s icon in the Dock and watch the program run.

Preference Panes

For the most part, your applications should show their preferences inside the apps themselves. For example, most apps that have preferences you can change have a Preferences window, accessible via the main menu (or by pressing ⌘-,).

However, some software doesn’t present a traditional interface where the preferences can be displayed—for example, background applications or device drivers. In these cases, you create preference panes, which are small programs hosted by the System Preferences application.

Preference panes are designed to allow the user to control features that affect the entire system (as far as the user is concerned). For example, when you install drivers for a graphics tablet, the features the drivers provide apply to all applications, which means that the drivers don’t have an app to show UI in. To allow the user to configure how it works, therefore, the drivers provide a preference pane.

NOTE

Preference panes are only available on OS X. On iOS, you use Settings Bundles, which are basically files that describe what settings to show to the user. You don’t write any code to display them. These settings are then available to the user using the NSUserDefaults system, which is discussed in Preferences.

How Preference Panes Work

A preference pane is not a separate application, but is instead a bundle of code loaded by the System Preferences application. The bundle contains code and whatever resources it needs (such as images, nib files, etc.); when the preference pane is installed, System Preferences displays it as an icon in the main window. When the user selects the preference pane’s icon, the bundle is loaded, its main nib is displayed, and your code begins running.

NOTE

The preference pane bundle stays in memory after the user switches to another pane, until the System Preferences application exits.

Because your preference pane is a bundle that’s loaded by another application, accessing resources via NSBundle’s pathForResource(ofType:) method or NSUserDefaults won’t work the same way as in your applications. This is because these methods access the application’s bundle and preference domain, not your bundle and preference domain. If you want to set preferences, you need to specifically tell NSUserDefaults which domain the preferences should be set in.

Preference Domains

Imagine that two applications exist, both of which set a preference called favoriteColor. These applications are by different authors and use the preference in different ways, so each assumes that it’s the only one using the favoriteColor preference.

To prevent preferences colliding, OS X and iOS separate preferences by domain. When you use NSUserDefault’s setValue(forKey:) and valueForKey methods (and related methods like setBool(forKey:)), it assumes that the preference domain you want to work in is the one with the same name as your application’s bundle identifier.

So, to go back to our two example applications, as long as each has a different bundle identifier—and it should, because Apple won’t allow it into the App Store unless a unique one is set—the two applications will set and retrieve preferences in their own, separate domains.

When you’re building a preference pane, however, the bundle identifier of the application is that of System Preferences. This means that calling methods like boolForKey won’t retrieve the settings you want. To solve this problem, you indicate to NSUserDefaults exactly which preference domain you want to work with.

To retrieve the preferences for a specific domain, you use the NSUserDefaults class’s persistentDomainForName method. This method takes an NSString containing the name of the domain, and returns an NSDictionary containing all of the keys and values stored in that domain’s preferences.

To set the preferences for this domain, you use NSUserDefaults’s setPersistentDomain(forName:) method. This works in much the same way: it takes an NSDictionary containing settings to apply, and an NSString containing the name of the domain to set.

This means that, instead of working with preferences on an individual basis, you work with a dictionary that contains all of the settings. When you set the values for a domain, you replace all of the settings at once.

For example, imagine that you want to work with the preferences for the domain com.oreilly.MyAmazingApplication.

To get the preferences as a mutable dictionary (so that you can modify it later), you do this:

let domainName = "com.oreilly.MyAmazingApplication"

var preferences =

    NSUserDefaults.standardUserDefaults().persistentDomainForName(domainName)

You can then modify that dictionary as you like, like so:

preferences["isChecked"] = true

When you’re done, you set the preferences for the domain by passing in the dictionary:

NSUserDefaults.standardUserDefaults().setPersistentDomain(preferences,

    forName: domainName)

Building a Sample Preference Pane

We’ll now build a preference pane that displays a single checkbox, which we’ll store in the domain com.oreilly.MyAmazingApplication.

1.    Create a new preference pane application for OS X. You’ll find the template in the System Plug-in section.

Name the project PreferencePane.

2.    Create the interface. Open PreferencePane.xib. This is the nib file that contains the view that will be shown when the preference pane is selected.

Drag in a checkbox and make its label read whatever you like.

3.    Make the File’s Owner of the nib file use the PreferencePane class. By default, the nib file created as part of the project template does not set the File’s Owner object to use the main class of the project. We’ll change that first.

Select the File’s Owner in the Interface Builder, and open the Identity Inspector.

Change the class from NSPreferencePane to PreferencePane (your class).

4.    Connect the interface to the code. Open PreferencePane.swift in the assistant.

Control-drag from the checkbox into PreferencePane’s interface. Create an outlet called checkbox.

5.    Add the code that loads the current preference. We’ll first add the code that loads the current value of the setting and turns the checkbox on or off. To do this, replace the mainViewDidLoad method in PreferencePane.swift with the following code (this method is run when the preference pane finishes loading):

6.      override func mainViewDidLoad()  {

7.          var preferences = NSUserDefaults.standardUserDefaults()

8.              .persistentDomainForName(domainName)

9.   

10.        if let checked = preferences?["isChecked"] as? NSNumber {

11.            switch checked {

12.            case true:

13.                self.checkbox.state = NSOnState

14.            default:

15.                self.checkbox.state = NSOffState

16.            }

17.        }

18. 

    }

19.Add the code that sets the preference when the pane is closed.

Add the following method to PreferencePane.swift (this method is called after the preference pane has stopped being shown by the user—such as when the System Preferences pane quits or the user clicks the Back, Forward, or Show All button):

    override func didUnselect()  {

        var preferences = NSUserDefaults.standardUserDefaults()

            .persistentDomainForName(domainName) as? [String: AnyObject]

        // persistentDomainForName might return nil, because this might

        // be the first time we've ever tried to save the preferences.

        // If this is the case, set 'preferences' to be an empty

        // dictionary, so that it can be used.

        if preferences == nil {

            preferences = [:]

        }

        // Store the info in the dictionary

        switch self.checkbox.state {

        case NSOnState:

            preferences?["isChecked"] = true

        default:

            preferences?["isChecked"] = false

        }

        // Store the dictionary in NSUserDefaults

        NSUserDefaults.standardUserDefaults()

            .setPersistentDomain(preferences!, forName: domainName)

    }

20.Test the application.

Build the preference pane by pressing ⌘-B or choosing Build from the Product menu.

Launch the System Preferences application.

Open the Products group in the project navigator. Drag the PreferencePane.prefPane file onto the System Preferences application in the Dock. System Preferences will ask how you want to install the preference pane.

Play around with the preference pane. If you check the checkbox, quit System Preferences, and come back to your preference pane, the checkbox will remain checked.

NOTE

You can’t test preference panes like you can other applications, because preference panes aren’t run like normal applications. Instead, you build the application and load it into the System Preferences application.

Status Bar Items

Another example of applications that don’t present themselves with traditional GUIs are applications that exist as status items—items that live in the top-right corner of the screen. OS X has a number of built-in applications that live like this, such as the volume changer and clock.

Status items can display any text, image, or view when clicked, and can display either a menu or a custom view. You create a status item by asking the system’s status bar to create an NSStatusItem for you; you then customize the status item by setting its title text, image, or view, and providing it with an NSMenu or other view to display when it’s clicked.

NOTE

You must keep a reference to the NSStatusItem object that you get from the NSStatusBar class. If you don’t, the object will be released from memory and removed from the status bar.

Status items allow you to work with an application’s features without requiring that the application be the foreground application. For example, Twitter for Mac shows a status item while the application is running that changes color when new messages arrive.

You can also create an application that only displays a status item. Such applications are generally background utility apps such as Dropbox, which use the status item to indicate the app’s current status and provide an easy way to access basic settings, as well as a means to access a more complete settings UI for controlling the application.

If you’re writing an application that only shows a status item, you likely don’t want to show the dock icon. To implement this, set the Application is agent (UIElement) value in the application’s Info.plist file to YES, and the app will not show a dock icon.

Building a Status Bar App

We’ll now demonstrate how to build a status bar application that doesn’t show a dock icon:

1.    Create a Cocoa application named StatusItem.

2.    Create the interface. This application will have neither a menu bar nor a window to show. The only UI will be the status item.

Open MainMenu.xib and delete both the main menu and the main window.

Drag in an NSMenu. It will contain three items—delete the second and third.

Make the single menu item’s label read Quit.

3.    Connect the interface to the code. Open AppDelegate.swift in the assistant.

Control-drag from the menu into AppDelegate, and create a new outlet called menu.

Control-drag from the Quit menu item into AppDelegate, and create an action named quit.

4.    Next, we’ll create the status item and prepare it. We’ll also add the code that gets run when the Quit menu item is chosen.

We’ll also need to add a property that stores the NSStatusItem object. Without this variable, the status item would be removed from memory, and therefore the status item would disappear immediately after it was added.

Update AppDelegate.swift so that it looks like the following code:

import Cocoa

class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet var window: NSWindow!

    @IBOutlet var menu: NSMenu!

    var statusItem : NSStatusItem!

    @IBAction func quit(sender: AnyObject) {

        NSApplication.sharedApplication().terminate(nil)

    }

    func applicationDidFinishLaunching(aNotification: NSNotification?) {

        // Make a status bar that has variable length

        // (as opposed to being a standard square size)

        // -1 to indicate "variable length"

        statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-1)

        // Set the text that appears in the menu bar

        statusItem.title = "My Item"

        // Set the menu that should appear when the item is clicked

        statusItem.menu = self.menu

        // Set if the item should change color when clicked

        statusItem.highlightMode = true

    }

}

5.    Finally, we’ll make the application not show a dock icon. The status item will remain visible no matter which application is currently active, so there’s always a way to access it.

To do this, you modify the application’s Info.plist file and indicate that it’s an agent. “Agent” is Apple’s term for a background application that doesn’t present a dock icon.

Select the project at the top of the project navigator. Open the Info tab at the top of the main editor.

Add a new entry into the Application is agent (UIElement) property list and set the value of this entry to YES.

6.    Run the application. Nothing will appear in the dock, but the word “Test” will appear at the top of the screen in the menu bar. You can open this menu and choose to quit the app.

iOS Apps with Multiple Windows

Sometimes, you might want to run your iOS app on more than one screen. For example, you might want to use the built-in touchscreen to receive input from the user and display the results on a television. Without the touch sensor or any of the other iOS device hardware, any second screen will be for output only, but this doesn’t mean it isn’t useful.

A window on an iOS device is represented by a UIWindow object. Inside this window are two important properties. The rootViewController holds the root view controller to be displayed; in the case of a standard iOS app, this will be the inital view controller from the storyboard, and in the case of an external window, it can be anything you wish. The second important property is the screen, which represents the actual physical screen on which the window is going to be displayed. The screen has a bounds property, which holds its size, and also has additional properties such as brightness, meaning that you can customize the second window to a degree.

To demonstrate how to use a second window in your iOS apps, we’ll create a demo app with two different view controllers—one for the device and another for the external monitor:

1.    Create a new single view iPhone application and call it MultipleWindows.

2.    Create the interface. Open the Main.storyboard and add a second view controller to the storyboard. There is no need to hook it up to the inital view controller, but do make sure it looks different so you can see it on the second window later.

Select the new view controller and open the Identity Inspector, and set the Storyboard ID to secondWindowVC.

3.    Connect to the new window. Replace the AppDelegate.swift file with the following:

4.  import UIKit

5.   

6.  @UIApplicationMain

7.  class AppDelegate: UIResponder, UIApplicationDelegate {

8.   

9.      var window : UIWindow!

10.    var secondWindow : UIWindow!

11. 

12.    func application(application: UIApplication!,

13.        didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {

14. 

15.        // Register to be notified of when screens connect or disconnect

16.        var notificationCenter = NSNotificationCenter.defaultCenter()

17. 

18.        notificationCenter.addObserver(self,

19.            selector: Selector("screenDidConnect:"),

20.            name: UIScreenDidConnectNotification,

21.            object: nil)

22.        notificationCenter.addObserver(self,

23.            selector: Selector("screenDidDisconnect:"),

24.            name: UIScreenDidDisconnectNotification,

25.            object: nil)

26. 

27.        // We're in the middle of starting up. If the system already has

28.        // multiple screens, set up the second one!

29.        if UIScreen.screens().count >= 2 {

30.            var secondScreen = UIScreen.screens()[1] as UIScreen

31.            self.setupScreen(secondScreen)

32.        }

33. 

34.        return true

35.    }

36. 

37.    // Given a screen, prepare and display the view

38.    // controller for the screen.

39.    func setupScreen(screen : UIScreen) {

40. 

41.        // If we already have a second window, do nothing

42.        if self.secondWindow != nil {

43.            return;

44.        }

45. 

46.        // Create a window to display on this screen

47.        self.secondWindow = UIWindow(frame: screen.bounds)

48.        self.secondWindow.screen = screen

49.        self.secondWindow.hidden = false

50. 

51.        // Create a view controller to show in the window

52.        var storyboard = UIStoryboard(name: "Main", bundle: nil)

53.        var viewController = storyboard

54.            .instantiateViewControllerWithIdentifier("secondWindowVC")

55.                as UIViewController

56. 

57.        // Show the view controller in the window

58.        self.secondWindow.rootViewController = viewController

59. 

60.    }

61. 

62.    // Called when a screen connects

63.    func screenDidConnect(notification: NSNotification) {

64. 

65.        // Get the screen from the NSObject

66.        var screen = notification.object as UIScreen

67. 

68.        // Attempt to set it up

69.        self.setupScreen(screen)

70.    }

71. 

72.    // Called when a screen disconnects

73.    func screenDidDisconnect(notification: NSNotification) {

74. 

75.        // Get the screen from the NSObject

76.        var screen = notification.object as UIScreen

77. 

78.        // If we have a second window, and it uses this window...

79.        if self.secondWindow?.screen == screen {

80. 

81.            // ... remove it!

82.            self.secondWindow = nil

83.        }

84. 

85.    }

86. 

}

This code is registering to be notified when a second monitor is being connected or disconnected from the device. When it detects a connection, it creates a new UIWindow object to hold that screen and then it adds the second view controller from the storyboard onto that window.

When it detects a window being disconnected, it simply clears out the window so it doesn’t take up memory.

Now if you run the app and then plug in a second monitor to your device, you should see the second view controller you created in the storyboard appearing while the initial view controller is showing on the device.

It’s worth noting that if you start the application and the window is already connected to the device, you won’t receive a notification. To find out what screens are connected when the app launches, you can query the screens property of the UIScreen class, which is a list of all screens currently attached to the device.