Using NSPageController in a Mac OS app

These notes show how to setup an NSPageController in a Mac OS app Swift project, to allow the user to switch between two or more pages of content.

The idea is to have several NSViewController instances, each with their corresponding user interface xib, to be configured with the NSPageController to provide different user interface items.

Then the pages can be switched by a swipe action on a compatible touchpad, or with buttons and keyboard shortcuts, programmatically.

Configure the project

Setup a new Cocoa Application project, using the Swift language and without using Storyboards.

Creating Controllers

Next create at least a pair of NSViewController subclasses by creating a new file and picking Cocoa Class; configure these new classes as subclasses of NSViewController and select to generate the corresponding user interface XIB file.

Then put your custom interface in the two XIB files that XCode has created, here’s an example just for test.

Controller 1 UI Controller 2 UI

Setting up the main inteface

Now in the interface file where the NSPageController is to be deployed (the default screen in the example is MainMenu.xib) drop a Custom View: this will be used by the NSPageController to perform animation and the proper screen grabs to provide the slider functionality.

Also drop four buttons in the user interface: previous, next, page1 and page2, in order to provide a simple navigation toolbar to test the functions of the Page Controller.

Next add to the Objects of the user interface two NSViewController objects and a NSPageController.

Main UI and controllers

The two NSViewControllers will have to be configured as having the two custom classes of the controllers created before, and the Nib Name of the XIB user interfaces created along them. These properties can be configured in the inspector of each controller object, so that the controller and user interfaces will be automatically instantiated when the application is loaded.

The NSPageController can be wired up connecting the view outlet to the custom view in the main user interface, and by connecting the navigateBack and navigateForward actions to the Prev and Next buttons in the UI.

NSPageController outlets

Code

There is also some code necessary for the page controller to work. In the application controller class (AppDelegate.swift if using the default).

We first need to connect the three controller instances from Interface Builder to outlets in the code to be referenced later.

We also need to make the AppDelegate class conform to the NSPageControllerDelegate protocol.

 1 class AppDelegate: NSObject, 
 2         NSApplicationDelegate, 
 3         NSPageControllerDelegate {
 4 
 5     @IBOutlet weak var theController1: Controller1!
 6     @IBOutlet weak var theController2: Controller2!
 7     @IBOutlet weak var thePageController: NSPageController!
 8 
 9     //...
10 }

The NSController works by managing an ordered list of objects, that can be navigated back and forth. Its delegate is asked for the controller corresponding to the particular object to be displayed.

In this test case the list of objects can be an array of string corresponging to the pages that need to be displayed in the application, so that when the delegate is asked for a particular string it will be able to respond with the appropriate controller.

So when the application is instantiated we first set the AppDelegate as the delegate of the page controller, and set the arrangedObjects property to a list of page description strings.

1 func applicationDidFinishLaunching(aNotification: NSNotification) {
2         
3         thePageController.delegate = self
4         thePageController.arrangedObjects =
5             ["page1" , "page2"]
6     }

The first delegate method to implement is the identifierForObject. This method is made to map the array of arranged objects with their identifier, that will be passed again to our delegate to select a controller. Since in our simple example we dont need controllers that can manage more than one object, we can simply return back the received object, which was a string from the arrangedObjects array.

1 func pageController(
2     pageController: NSPageController, 
3     identifierForObject object: AnyObject 
4 ) -> String {
5     
6         return String(object)
7     }

The second delegate method is the one we use to return back the proper controller for each received identifier: this simply maps the controller instances declared into interface builder to the returned controller according to the identifier string.

 1 func pageController(
 2     pageController: NSPageController, 
 3     viewControllerForIdentifier identifier: String
 4 ) -> NSViewController {
 5         
 6     switch identifier {
 7     case "page1":
 8         return theController1
 9     case "page2":
10         return theController2
11     default:
12         return NSViewController()
13     }
14 }

The last delegate method is needed to inform the page controller that a transition is finished. This is used to inform the class that the user controller has configured the corresponding view to be displayed, and that the screenshot produced by the page controller to animate can be replaced with the actual view.

1 func pageControllerDidEndLiveTransition(
2     pageController: NSPageController) {
3     
4     pageController.completeTransition()
5 }

Running the app

At this point the app can be built and run: the next and prev buttons will allow switching pages from the interface, and multitouch enabled trackpads or mice will be able to send a swipe action to move across the panels.

The rendered graphics however is not the most polished: if we click the next and previous buttons the views overlap with each other when the animation is performed:

change page with buttons

And even with a swiping gesture the same overlapping between the two views can be seen. A quick solution would be to make the back of the individual views opaque by adding a view that stretches for the whole area and has a not transparent color.

change page with swipe

Some more types of pagination effects are available in the properties of the NSPageController: it can be configured

Setting pages programmatically

In order to change the pages the controller property selectedIndex can be changed to the index of the page, and the corresponding view will be immediately switched, without animation.

To make the controller animate the change the following helper method can be used to get the animator of the page controller, issue the change and have it completed when the animation has been performed.

Then in the event handlers of the buttons Page 1 and Page 2 we can simply call the following function with the proper index.

1 func changePage( page : Int ){
2   NSAnimationContext.runAnimationGroup(
3     {_ in self.thePageController.animator().selectedIndex = page},
4      completionHandler: 
5         {_ in self.thePageController.completeTransition()})
6 }

Conclusion

The NSPageController can be configured with multiple view controllers to paginate between them. It has however some problems that are not quickly manageable:

The above project is available for download here, it can be used as a baseline to use the NSPageController in a Mac OS app, and as a starting point to overcome the little problems found with this straightforward implementation.

Comments