Class UIViewController

We will create a custom view controller. The Notification Center project was able to detect a change of orientation; now we will also able to make the text run in the correct direction.

A view under the control of an iOS 7 view controller occupies the full screen; see the Transition Guide. I tried to turn this off with the edgesForExtendedLayout property of the view controller, but the view persisted in being 568 pairs of pixels high. Fortunately, however, the iOS 7 status bar is transparent. I merely started the text in the text view with an empty line to prevent any text from being written behind the status bar.

Note that the frame and bounds properties disagree in the landscape orientations.

Source code in Portrait.zip

  1. main.m
  2. Class PortraitAppDelegate
  3. Class ViewController
  4. Class View

Create the project

To create class ViewController, highlight the Portrait folder in the Project Navigator.
File → New → File…
Choose a template for your new file: Objective-C class
Next
Class: ViewController
Subclass of: UIViewController
Uncheck With XIB for user interface.
Next
Create

To create class View, highlight the Portrait folder in the Project Navigator.
File → New → File…
Choose a template for your new file: Objective-C class
Next
Class: View
Subclass of: UITextView
Uncheck With XIB for user interface.
Next
Create

To allow the app to run in the upside down orientation, select the name of the project in the Xcode Project Navigator. In the big center pane of Xcode, scroll down to
Deployment Info → Deice Orientation
and check all four checkboxes. Add the supportedInterfaceOrientations method to the view controller. See Handling View Rotations.

MVC: model-view-controller

This app redraws itself by calling the drawRect: method of the View whenever the iPhone is moved to a new orientation: portrait, landscape, portrait upside down, etc. The machinery that calls drawRect: belongs to a new object called the view controller, or controller for short. The controller has a property called interfaceOrientation. Whenever the controller senses that this property has changed, the controller will ensure that someone calls the drawRect: method of the view that is controlled by the controller.

In the future, I promise that the controller will respond to changes in something much more substantial than interfaceOrientation, which is one of the controller’s own properties. The controller will respond to changes in a separate object called the model, which can be a formidable repository of data. When the data in the model changes, the controller will see to it that drawRect: is called.

Data will also flow in the opposite direction. The controller will sense when the buttons and sliders in the view have received data from the user. The controller will then transmit this data to the model. For the present, however, the flow of data is only one way, from the controller down to the view.

We saw an example where a little yellow view was added to a big white view. We could have provided a view controller for that big white view but not for the little yellow view. A little view that has been added to another view does not have a view controller of its own.

The application delegate creates the view controller.

Instead of creating a view and a window, this app delegate creates a window and a view controller. The view controller will create the view. In fact, the controller will be the view’s lifelong guardian angel. At any given time, a controller controls only one view.

The application delegate has a propery variable named window that points to the window. The window has a property named rootViewController that points to the controller. The controller has a property named view that points to the view. Make a diagram of this on a piece of paper.

The view controller creates the view.

The controller’s view is created by the loadView method of the controller. loadView creates the view and stores the address of the view into the view property of the controller. The code in loadView is familiar code that until now has been in application:didFinishLaunchingWithOptions:.

loadView is called automatically the first time the value of the controller’s view property is used. For example, the method application:didFinishLaunchingWithOptions: could have triggered a call to loadView with the following mention of viewController.view. loadView would have been called immediately before the value of viewController.view was used as an argument of addSubview:.

		ViewController *viewController = [[ViewController alloc] initWithNibName: nil bundle: nil];
		[self.window addSubview: viewController.view];

Instead of the above two statements, application:didFinishLaunchingWithOptions: accomplished the same thing with the following statement. See the documentation for rootViewController.

		self.window.rootViewController = [[ViewController alloc] initWithNibName: nil bundle: nil];

The controller and the view have pointers to each other.

The view controller and the view contain instance variables or properties that point to each other. The view property of the ViewController object, inherited from class UIViewController, points down to the View object. Conversely, the viewController instance variable of the View object points up to the ViewController object. This instance variable allows the View object to send messages to the ViewController object. It has to be a __weak pointer (two underscores) to allow the view to die before the view controller. Otherwise, the view and the view controller will keep each other alive forever and we’d have a memory leak.

In our view controller, the interfaceOrientation propery has one of the four possible values of the data type UIInterfaceOrientation. Just between you and me, this data type is basically an int, and the four values are basically 1, 2, 3, 4.

The output from drawRect:

The initial orientation is portrait.

The View’s superview is the window. The frame and bounds of the window remain serenely unaffected by the change in orientation.

The Macintosh origin is in the lower left corner of the screen, with the Y axis pointing up. The iPhone origin is in the upper left corner of the screen, with the Y axis pointing down. The CTM agrees with the view’s bounds. The CTM for Portrait and PortraitUpsideDown means “flip it upside down, double the coördinates, and add 640 to the y.” The CTM for LandscapeLeft and LandscapeRight means “flip it upside down, double the coördinates, and add 1136 to the y.”

Thinsg to try

  1. Even though the view controller says that the view’s frame should only be as big as the applicationFrame, the view occupies the entire window. This causes the status bar to appear in front of the top part of the view. To prevent the words in the view from apearing behind the status bar, drawRect: started the format string with the newline character "\n". A better way to accomplish this would be to remove the newline and insert the following initialization code into the initWithFrame:controller: method of the view.
    		//Draw no text in the part of the UITextView
    		//that lies behind the status bar.
    		CGFloat height = [UIApplication sharedApplication].statusBarFrame.size.height;
    
    		self.textContainerInset = UIEdgeInsetsMake(
    			height,
    			self.textContainerInset.left,
    			self.textContainerInset.bottom,
    			self.textContainerInset.right
    		);
    

  2. To make the controller redraw the view for all four possible orientations, the method supportedInterfaceOrientations should
    	return UIInterfaceOrientationMaskPortrait
    		| UIInterfaceOrientationMaskLandscapeLeft
    		| UIInterfaceOrientationMaskLandscapeRight
    		| UIInterfaceOrientationMaskPortraitUpsideDown;
    
    or, more briefly,
    	return UIInterfaceOrientationMaskAll;
    
    To make it redraw only for portrait and portrait upside down, you can say
    	return UIInterfaceOrientationMaskPortrait
    		| UIInterfaceOrientationMaskPortraitUpsideDown;
    
    See all the possible values for the UIInterfaceOrientationMask.

  3. This app recognizes only four possible orientations. For an app that recognizes two more, face up and face down, see the notification center example. For an app that recognizes angles of tilt (e.g., 30°), see accelerometer.

  4. The drawRect: method of class UIView is called when the orientation of the view changes. The layoutSubviews method of class UIView is also called. In class View, change the name of the drawRect: method to layoutSubviews and remove the unused argument rect.

  5. It might be easier to create this project with a different Xcode template.
    Choose a template for your new project: Single View Application (instead of Empty Application)

  6. Put some information into the ViewController’s tab bar item. Add the file star.png to the Supporting Files folder in the Project Navigator, checking the checkbox for “Copy items into destination group’s folder (if needed).” In ViewController.m,
    - (id) initWithNibName: (NSString *) nibNameOrNil bundle: (NSBundle *) nibBundleOrNil {
    	self = [super initWithNibName: nibNameOrNil bundle: nibBundleOrNil];
    	if (self) {
    		// Custom initialization
    		self.title = @"Title";
    		self.tabBarItem.image = [UIImage imageNamed: @"star.png"];
    		self.tabBarItem.badgeValue = @"b";
    	}
    	return self;
    }
    
    Run the app and note that the tab bar item is not yet visible.

    Now put a tab bar controller over the the ViewController. Initialize the tab bar controller in application:didFinishLaunchingWithOptions:. A tab bar controller usually has an array of five or so controllers under it, but this tab bar controller has an array of only one controller. Our other arrays were created with arrayWithObjects:; this tiny array is created with arrayWithObject:.

    	// Override point for customization after application launch.
    	self.window.rootViewController = [[UITabBarController alloc] init];
    
    	((UITabBarController *)self.window.rootViewController).viewControllers = [NSArray arrayWithObject:
    		[[ViewController alloc] initWithNibName: nil bundle: nil]
    	];
    
    The viewController’s tab bar item should now be visible. (For a row of tab bar items, see the app store.) A badge of more than one character will block part of the star.


  7. Put some information into the viewController’s navigation item. In the initWithNibName:bundle: method of class ViewController, insert the following code. A naviagtion item has room for a more elaborate title than a tab bar item.
    	self.navigationItem.prompt = @"Welcome to";
    	self.navigationItem.title = @"More Elaborate Title";
    
    	self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
    		initWithBarButtonSystemItem: UIBarButtonSystemItemDone
    		target: nil
    		action: NULL
    	];
    
    Run the app and note that the navigation item is not yet visible.

    Now put a navigation controller over the ViewController. Initialize the navigation controller in application:didFinishLaunchingWithOptions:.

    	self.window.rootViewController = [[UINavigationController alloc]
    		initWithRootViewController: [[ViewController alloc]
    			initWithNibName: nil
    			bundle: nil
    		]
    	];
    
    The viewController’s navigation item should now be visible. We conclude that the navigation bar is 74 pixels (or pixel pairs) high, because 20 + 74 + 386 = 420.