Views and controllers
Relevant resources
Documentation
Sample code - Apple
Sample code - class
Web
UIView
UIView is the base class for almost all visual elements you see on the iPhone. It is very different from its cousin on the Mac, NSView.
In many ways, it behaves like a lightweight wrapper around a Core Animation CALayer. All UIViews are layer-backed, a concept that we will talk about in greater detail when we cover Core Animation. NSViews on the Mac are far more heavy. You can animate up to 50 UIViews at once at 60 FPS on an iPhone (pre-3G S), and 100 at 30 FPS. Having that many NSViews moving around onscreen would significantly slow down even a desktop Mac.
Another difference from the Mac is the fact that UIWindow, the root display object of an iPhone application's interface, inherits from UIView. On the Mac, NSWindow is completely different from NSView.
View geometry
A view's size, position, and shape are determined by a few key properties. These are the frame, bounds, and center of the view.
The frame is a CGRect struct with the following definition:
struct CGRect {
CGPoint origin;
CGSize size;
};
typedef struct CGRect CGRect;
where CGPoint is defined as
struct CGPoint {
CGFloat x;
CGFloat y;
};
typedef struct CGPoint CGPoint;
and CGSize is
struct CGSize {
CGFloat width;
CGFloat height;
};
typedef struct CGSize CGSize;
The two sub-components of the frame let you set the upper-left origin of the view and its overall size. On the iPhone, the coordinate system is structured such that the upper-left corner of the screen is the origin (0,0) with positive values going down and to the right. This is opposite of the coordinate system on the Mac, which tends to start in the lower-left corner of the screen.
The bounds property is also a CGRect, but most of the time you'll just be setting its size. The bounds lets you directly set the size of the view via its size component, which will cause a corresponding change in the view's frame.
The center property lets you change the placement of views by specifying their new center. Like with the bounds property, this will cause the frame to be altered to match. One thing to be careful of is that if your view has an odd width or height, giving it integer values for the X and Y position of its center will cause its origin to be on a non-integer position in X or Y. This leads to a blurry view (something we'll talk more about in the class on 2-D drawing).
Customizing a view without subclassing
The background color of a view can be set using its backgroundColor property, which takes a UIColor argument:
view.backgroundColor = [UIColor redColor];
You can access the layer for a UIView through its layer property. From there, you can fine-tune the appearance of a view without any subclassing. You can add a border and round the corners of a view using code like the following:
view.layer.cornerRadius = 10.0f;
view.layer.borderWidth = 3.0f;
view.layer.borderColor = [[UIColor blackColor] CGColor];
Note that you will need to add
#import <QuartzCore/QuartzCore.h>
to your code to prevent compiler warnings about the CALayer properties you're using here.
These views can also be rotated, scaled, and distorted by using a transform. We will spend more time with transforms when we get to Core Animation, but they are matrices which can hold within them multiple manipulations of your views. For example, a single transform might let you rotate, scale, and apply a perspective effect on your view.
There are two ways to transform a view. First, you can set a 2-D transform directly to a view using its transform property. This property takes a CGAffineTransform value, a Core Graphics structure. We will talk about this in more detail when we get to Quartz 2-D drawing. As an example, to rotate a view by 45 degrees, you could use the following code:
view.transform = CGAffineTransformMakeRotation(45.0f * M_PI / 180.0f);
Transforms specify angles in radians, thus the conversion when creating this transform.
Another way to transform a view is by accessing the transform property of a view's layer. This transform is a 3-D CATransform3D structure native to Core Animation. We will talk about this when we go over Core Animation.
Custom view drawing
Sometimes the items you can use to customize a view simply aren't enough. In those cases, you will want to subclass UIView (or the particular interface element you're extending) to have it perform custom drawing within the view.
It's important to understand how a UIView draws custom content to the screen, because many people new to the platform make bad assumptions about how this works. As mentioned previously, each UIView is backed by a CALayer. This CALayer effectively represents a bitmapped texture stored on the GPU. When you move, animate, or transform a view, it is not being redrawn, its layer is merely manipulated to produce the desired effect. These manipulations can be accelerated in the GPU hardware on the device, so they can be performed quickly.
In contrast, providing new content for the view is an expensive operation, requiring a whole new texture be drawn and uploaded to the GPU. It is for this reason that the iPhone OS tries to minimize the amount of view redrawing that occurs. Ideally, the content for a view will be drawn once and never again, despite the view being moved all over the place, faded in and out, rotated, and scaled.
Custom drawing code for a UIView must be placed within its -drawRect: method. Other methods can do parts of the drawing, but they must be called from within this one method. It is only within here that you will have a valid drawing context for assembling your content to be drawn. Such a method will look something like this:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
// Do your drawing here
}
Before you do your drawing, you will need to call UIGraphicsGetCurrentContext() in order to grab the correct context for your Quartz drawing calls to use. We will describe how to do custom 2-D drawing in the Quartz 2-D drawing class.
When a new view is created, its content will be supplied via -drawRect:. If you want to force a redraw of the view, due to a change in what it represents, or perhaps a change in the view's size, you do so by calling -setNeedsDisplay on the UIView. Note that you never call -drawRect: directly.
Redrawing a view is a costly operation, as explained above, and unnecessary redraws should be kept to a minimum. However, there are times where it is appropriate. For example, if you try to scale up a view using a transform you will see that the view quickly becomes blurry. This is because the transform is merely upscaling the cached bitmap texture of that view. Instead of applying a transform to scale your view, you may wish to redraw the view at the new size so that you can have pixel-sharp graphics. However, you wouldn't want to do this while you are pinch-zooming a view, because the zooming would be jerky. In that case, you could use a transform while zooming, then redraw the content sharply when the user lets go.
View hierarchy
UIWindows and UIViews form a hierarchy in an iPhone interface, which controls how they are displayed and how they are interacted with. At the root of this hierarchy is one UIWindow for your application. This UIWindow can have one or more subviews, and those subviews can have subviews, forming a tree of views to be displayed.
Subviews appear in front of their superview. Subviews are stored in an ordered array within a superview. Views appearing later in the array are placed in front of lower-numbered views. Views can be added as a subview of a particular UIView by using -addSubview:. You don't remove a subview by calling a method on the superview; instead you use the view to be removed's -removeFromSuperview method.
Views can be inserted at a particular location in the array of subviews using –insertSubview:atIndex:, – insertSubview:aboveSubview:, and –insertSubview:belowSubview:. Views can exchange places within the list of subviews using –exchangeSubviewAtIndex:withSubviewAtIndex:.
When placing views, this is done within the coordinate space of the superview. What this means is that if you create a view and place it at (100, 100) within your main window, then add a subview to that view and tell it to be placed at (10, 20), it really will be located at (110, 120) as you look at the main window. Moving a view around will move its subviews to match. To convert from the coordinate space of one view to another, you can use the methods –convertPoint:toView:, –convertPoint:fromView:, –convertRect:toView:, and –convertRect:fromView:.
The sample Views application for this class contains a category on UIView which lets you display a view and all the elements below it in the view hierarchy. This code is based off of the article "View Spelunking Part 1: Exploring subviews and layout" by Erica Sadun.
User interaction
As we will cover in depth when we get to scroll views and custom multitouch event handling, you can perform custom handling of touch events within each of these views. To do so, you can override the methods –touchesBegan:withEvent:, –touchesMoved:withEvent:, –touchesEnded:withEvent:, and –touchesCancelled:withEvent:. Within each of these methods, you can add your own touch-handling code.
However, I recommend placing this touch-handling code within a UIViewController, as it tends to be a better location for this kind of control logic. To do this, you simply need to implement methods with the same signature as those listed above within your UIViewController and not implement them in your UIView subclass, and those actions will then be handled by the controller instead.
Views use the same rules to manage touch events as they do for display. If a view is placed in front of another, it gets a chance to handle the touch event before the one below it does. If it does nothing, or doesn't respond to touches, the touch is then passed on to the view below, and so on. Some views, like UIScrollView do more complex things with touches and their subviews, which we'll talk about in a later class.
UIViewController
Following the Model View Controller design pattern, you don't want to put your control logic within the views in your interface. Instead, you tend to create a UIViewController for each "page" of your application. This UIViewController (or a subclass, like UITableViewController) manages the application-specific control flow for whatever is currently displayed on the iPhone screen.
For UIViewControllers that programmatically generate their display content, you'll need to implement the -loadView method. This method is called the first time a UIViewController's view property is accessed. Within this method, you are expected to create the controller's view, add its subviews and lay them out, and then assign the new view to the view controller's view property.
For view controllers that have their content supplied via NIBs, the -viewDidLoad method is the one most commonly used to perform any additional initialization of views after they have been loaded from the NIB.
The current UIViewController can determine whether or not the interface will rotate if the iPhone is rotated by returning YES or NO from –shouldAutorotateToInterfaceOrientation: in response to particular orientations. If rotation is allowed, your UIViewController can manage layout changes in –willRotateToInterfaceOrientation:duration: and similar methods.
If a low-memory warning has been given to your application (a topic we will cover later when we performance tune for memory usage), your UIViewController will have its –didReceiveMemoryWarning method be triggered. This gives the controller a chance to unload any cached data or anything else that can free up memory. The default behavior also removes the UIViewController's view from memory if it is not currently being displayed. This can lead to problematic behavior if you are not anticipating this case.
Programmatic user interface creation
While it is recommended that you use Interface Builder to create your application's interface, it is possible to construct an iPhone application that programmatically constructs its entire interface. To do this, only a few things in the application need to be changed.
Normally, the main() function kicks off your application by loading a main NIB file. If you want to construct the initial UIWindow and its views yourself, you can replace the line
int retVal = UIApplicationMain(argc, argv, nil, nil);
with
int retVal = UIApplicationMain(argc, argv, nil, @"ViewsAppDelegate");
where you replace the last argument with the name of your application delegate class. Within that application delegate, you'll need to construct the interface in the -applicationDidFinishLaunching: delegate method, including initializing the root UIWindow object and placing the first view within that.
Finally, you will need to remove the reference to the main NIB file's base name from the application's Info.plist.
Note that this won't net you a huge performance win. In Matt Gallagher's benchmarks, he only observed a maximum of a 20% decrease in application loading time by going with a programmatic interface (and sometimes a small performance win in going that way).
Accessibility
Starting with iPhone OS 3.0, Apple has made it possible for visually impaired users to work with the iPhone 3G S (and newer), and with the third generation iPod touch, using VoiceOver technology. VoiceOver provides screen reading of the iPhone interface.
There are many good reasons why you'd want to make your iPhone application accessible. By making your application accessible, you are making it usable to a very vocal group of users, who will be more likely to tell others about it. But beyond that, it's just the right thing to do.
Default user interface elements tend to be accessible out of the box, but if you wish to make your custom views work with VoiceOver, you either have to provide a few methods or set properties on your view. For an example of setting accessibility properties:
[view setIsAccessibilityElement:YES];
[view setAccessibilityTraits:UIAccessibilityTraitImage];
[view setAccessibilityLabel:NSLocalizedString(@"Red view", nil)];
[view setAccessibilityHint:NSLocalizedString(@"This is just a red view", nil)];
This sets the view to be a VoiceOver-responding element, and has it be handled like an image. We also provide a text label and hint for this element that will be read by VoiceOver. Rather than directly specify a string constant, the NSLocalizedString() macro has been used. This macro replaces the specified string constant with a version that has been localized into the user's default language, if you've specified one in an appropriate lookup table (see the Internationalization Programming Topics guide for more).
To test the accessibility of your application within the Simulator, you can enable the Accessibility Inspector. This is found within the Settings application, under General | Accessibility. Once you turn it on, a hovering panel will appear over your interface. You can activate and deactivate this panel by clicking on the X in its upper left. When active, it will display the text that VoiceOver will see for each element you click on. However, this is no match for VoiceOver running on the actual device, which requires an iPhone 3G S or later, or one of the latest iPod touches.
Cut, copy, and paste
As of OS 3.0, the iPhone now has an elegant system for doing cut, copy, and paste between applications. This system works transparently with standard user interface elements, like text fields. However, you may have custom views for which you want to enable cut, copy, and paste as well.
To enable this, you will need to make the responsible view controller the first responder. There is a glitch in iPhone OS 3.0 where if you send the -becomeFirstResponder message to your view controller when it first appears, this message won't be registered. To work around this, you can use code like the following:
[self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0.3];
In order for the view controller to be able to become first responder, you need to override the following method:
- (BOOL)canBecomeFirstResponder
{
return YES;
}
You then need to implement -canPerformAction:withSender: in your view controller to identify which actions your view supports. For example,
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (action == @selector(cut:))
return NO;
else if (action == @selector(copy:))
return YES;
else if (action == @selector(paste:))
return NO;
else if (action == @selector(select:) || action == @selector(selectAll:))
return NO;
else
return [super canPerformAction:action withSender:sender];
}
reports that this view controller only supports the copy action, and none of the others. When the menu appears, the only option shown will be Copy.
You will need to implement a -copy: method to handle this action in your controller:
- (void)copy:(id)sender
{
// Copy to the pasteboard here
}
To display the cut, copy, and paste menu, you would use code like the following:
UIMenuController *copyMenuController = [UIMenuController sharedMenuController];
[copyMenuController setTargetRect:viewRect inView:mainView];
[copyMenuController setMenuVisible:YES animated:YES];
This displays the menu and centers on viewRect, which we assume is the frame of your view in the coordinate system of the mainView view.
You may wish to perform custom highlighting of your view or other actions when the menu appears. To do that, make your view controller an observer of the UIMenuControllerWillShowMenuNotification and the UIMenuControllerWillHideMenuNotification.