Views and the View Hierarchy
Views make up the user interface of an application. Each view maintains an image that represents it. For example, a
UIButton’s image is a rounded rectangle with a title in the center. A UILabel’s image is just text. When something
about a view changes, like a UILabel’s text property or a UIButton’s title, the view’s image is redrawn so that
these changes become visible on screen.
Another subclass of UIView is UIWindow. Every application has exactly one instance of UIWindow that serves as the
container for all the views in the application. The window is created when the application launches. Open
HypnosisterAppDelegate.m and find the application:didFinishLaunchingWithOptions: method.
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
In application:didFinishLaunchingWithOptions:, the template added code that creates a UIWindow object and
sends it the message makeKeyAndVisible. This puts the window on the screen, and once a window is on the screen,
you can add other views to it.
When a view is added to a window, it is said to be a subview of the window. Views that are subviews of the window
can also have subviews, and the result is a hierarchy of view objects. A view will appear on the screen if and only if
its been added to this hierarchy – either directly as a subview of the window or as a subview of another view that has
been added to the window. Thus, the window is the root of the view hierarchy.
When the screen is redrawn (which happens after an event is processed), the window’s image is drawn to the screen.
Then, all of the subviews of the window draw their images to the screen, then the subviews of the subviews draw
their images, and so on
Creating a Custom View
Let’s create an instance of HypnosisView, set its backgroundColor (a property inherited from UIView), and add the
HypnosisView to the view hierarchy.
Open HypnosisterAppDelegate.m. At the top of this file, import the header file for HypnosisView.
#import "HypnosisterAppDelegate.h"
#import "HypnosisView.h"
@implementation HypnosisterAppDelegate
Locate the method application:didFinishLaunchingWithOptions: near the top of
HypnosisterAppDelegate.m. In this method, create an instance of HypnosisView and add it as a subview of the
window.
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
CGRect viewFrame = CGRectMake(160, 240, 100, 150);
HypnosisView *view = [[HypnosisView alloc] initWithFrame:viewFrame];
[view setBackgroundColor:[UIColor redColor]];
[[self window] addSubview:view];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
This HypnosisView instance is a
subview of the UIWindow. When you add a view as a subview of another view, the inverse relationship is
automatically established: the HypnosisView’s superview is the UIWindow. (To avoid a retain cycle, the superview
property is declared as __unsafe_unretained.)
Also, notice that the console says something about applications expecting to have a root view controller. You can
ignore this. It doesn’t hurt anything, and it will make sense after the next chapter.
Every view instance has a frame rectangle. A view’s frame specifies the view’s size and position relative to its
superview. A frame is represented by a CGRect structure and contains the members origin and size (Figure 6.6).
These members are also structures. The origin is of type CGPoint and contains two float members: x and y. The
size is of type CGSize and has two float members: width and height. (A structure is not an Objective-C object,
so you can’t send it messages, and you don’t declare it as a pointer.)
Thus, a view is always a rectangle.
Create another instance of HypnosisView in HypnosisterAppDelegate.m and add it to the window in a different
position and with a different size and background color.
[[self window] addSubview:view];
CGRect anotherFrame = CGRectMake(20, 30, 50, 50);
HypnosisView *anotherView = [[HypnosisView alloc] initWithFrame:anotherFrame];
[anotherView setBackgroundColor:[UIColor blueColor]];
[[self window] addSubview:anotherView];
self.window.backgroundColor = [UIColor whiteColor];
也就是说UIWindow 下可以不止一个UIView
The drawRect: Method
By default,
drawRect: does nothing, but UIView subclasses override this method to perform custom drawing.
When you override drawRect:, you issue drawing instructions that create the image for instances of your UIView
subclass. These drawing instructions come from the Core Graphics framework, which is automatically added to an
application target when you create a new project.
Before a view is sent the message drawRect:, a drawing context is automatically created and set as the “current
context.”
- (void)drawRect:(CGRect)dirtyRect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
}
The type CGContextRef is defined as CGContext * – a pointer to a CGContext. CGContext is a structure that
represents a drawing context. (The suffix Ref makes it easy to distinguish between
A view’s image is the same size as it appears on the screen, i.e., the same size as the view’s frame. The frame
describes the view’s size relative to the view’s superview. However, the view shouldn’t have to know about its
superview until it gets to compositing its image to the screen. So, there is a separate CGRect property of UIView
named bounds that gives the view its size independent of its superview. In HypnosisView.m, get the bounds
rectangle in drawRect: after you get a pointer to the drawing context.
- (void)drawRect:(CGRect)dirtyRect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect bounds = [self bounds];
}
The drawing operations you perform on the CGContextRef must fall within the bounds rectangle; otherwise, they
will be clipped to that rectangle. Let’s draw a circle in the center of the bounds rectangle. Add the following code to
drawRect:.
- (void)drawRect:(CGRect)dirtyRect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect bounds = [self bounds];
// Figure out the center of the bounds rectangle
CGPoint center;
center.x = bounds.origin.x + bounds.size.width / 2.0;
center.y = bounds.origin.y + bounds.size.height / 2.0;
// The radius of the circle should be nearly as big as the view
float maxRadius = hypot(bounds.size.width, bounds.size.height) / 4.0;
// The thickness of the line should be 10 points wide
CGContextSetLineWidth(ctx, 10);
// The color of the line should be gray (red/green/blue = 0.6, alpha = 1.0);
CGContextSetRGBStrokeColor(ctx, 0.6, 0.6, 0.6, 1.0);
// Add a shape to the context - this does not draw the shape
CGContextAddArc(ctx, center.x, center.y, maxRadius, 0.0, M_PI * 2.0, YES);
// Perform a drawing instruction; draw current shape with current state
CGContextStrokePath(ctx);
}
Notice that a view’s backgroundColor is always drawn regardless of what drawRect: does. Typically, you set the
backgroundColor of a view to clear so that only drawRect:’s results show. In HypnosisterAppDelegate.m,
remove the code that sets the background color of each of these views.
HypnosisView *view = [[HypnosisView alloc] initWithFrame:viewFrame];
[view setBackgroundColor:[UIColor redColor]]; //remove
[[self window] addSubview:view];
CGRect anotherFrame = CGRectMake(20, 30, 50, 50);
HypnosisView *anotherView = [[HypnosisView alloc] initWithFrame:anotherFrame];
[anotherView setBackgroundColor:[UIColor blueColor]]; //remove
Then, in HypnosisView.m, override initWithFrame: to set the background color of every HypnosisView to clear.
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// All HypnosisViews start with a clear background color
[self setBackgroundColor:[UIColor clearColor]]; //clear color
}
return self;
}
While the bounds origin is typically (0, 0) and the size is typically equal to the frame’s size, this is sometimes
not the case. Thus, when drawing, you should never use constant numbers. Instead, use a combination of the origin
and size of the bounds to specify the coordinates that you draw to.
Core Graphics
The functions and types that begin with CG come from the Core Graphics framework, a C language API for 2D
drawing. The hub of the Core Graphics framework is CGContextRef: all other Core Graphics functions and types
interact with a drawing context in some way, and then the context creates an image.
Let’s look at the Core Graphics functions you used in the implementation of HypnosisView’s drawRect: method.
You set up the drawing state with CGContextSetLineWidth and then set its color using
CGContextSetRGBStrokeColor.
Next, you added a path to the context using CGContextAddArc. A path is a collection of points that forms a shape
and can form anything from a square to a circle to a human outline. There are a number of Core Graphics functions
like CGContextAddArc that you can use to add a path to a context. (Search the documentation for CGContextRef to
find them.)
After a path has been added to a context, you can perform a drawing operation. There are three drawing operations:
1CGContextStrokePath will draw a line along the path.
2CGContextFillPath will fill the shape made by the path.
3CGContextClip will limit future drawing operations to the area defined by the path. For example, drawing a
square to a context that has been clipped to a circle of the same size will result in drawing a circle.
After a drawing operation, the current path is removed from the context. Thus, to draw more than one circle, you
have to add a path to the context for each circle. In HypnosisView.m, modify drawRect: to draw a number of
concentric circles.
UIKit Drawing Additions
There are a number of Foundation and UIKit classes that can work with a CGContextRef. One such class is
UIColor. An instance of UIColor represents a color and can be used to set the current drawing color of the context.
In HypnosisView.m, replace the line of code that changes the stroke color with one that uses UIColor’s
colorWithRed:green:blue:alpha: method.
CGContextSetLineWidth(ctx, 10);
CGContextSetRGBStrokeColor(ctx, 0.6, 0.6, 0.6, 1.0); //remove
[[UIColor colorWithRed:0.6 green:0.6 blue:0.6 alpha:1] setStroke];
Build and run the application again. The HypnosisView will look the same as before.
There are a number of prepared UIColor instances you can use for common colors. Change this line of code again:
[[UIColor colorWithRed:0.6 green:0.6 blue:0.6 alpha:1] setStroke];//remove
[[UIColor lightGrayColor] setStroke];
Build and run again. The color of the circles should be about the same, but the code is much simpler.
NSString has the ability to draw to a CGContextRef. Sending the message drawInRect:withFont: to an
NSString will draw that string in the given rectangle with the given font to the current context. In HypnosisView.m,
add the following code to the end of drawRect:.
for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) {
CGContextAddArc(ctx, center.x, center.y,
currentRadius, 0, M_PI * 2.0, YES);
CGContextStrokePath(ctx);
}
// Create a string
NSString *text = @"You are getting sleepy.";
// Get a font to draw it in
UIFont *font = [UIFont boldSystemFontOfSize:28];
CGRect textRect;
// How big is this string when drawn in this font?
textRect.size = [text sizeWithFont:font];
// Let's put that string in the center of the view
textRect.origin.x = center.x - textRect.size.width / 2.0;
textRect.origin.y = center.y - textRect.size.height / 2.0;
// Set the fill color of the current context to black
[[UIColor blackColor] setFill];
// Draw the string
[text drawInRect:textRect
withFont:font];
}
Let’s spice it up with a shadow. Add the following code in HypnosisView.m.
[[UIColor blackColor] setFill];
// The shadow will move 4 points to the right and 3 points down from the text
CGSize offset = CGSizeMake(4, 3);
// The shadow will be dark gray in color
CGColorRef color = [[UIColor darkGrayColor] CGColor];
// Set the shadow of the context with these parameters,
// all subsequent drawing will be shadowed
CGContextSetShadowWithColor(ctx, offset, 2.0, color);
// Draw the string
[text drawInRect:textRect
withFont:font];
Build and run the application. You now have shadow.
UIImage has a similar useful method, drawInRect:, for drawing an image object to a context
Redrawing Views
When a UIView instance receives the message setNeedsDisplay, it will redraw its image. View subclasses send
themselves setNeedsDisplay when their drawable content changes. For example, a UILabel will mark itself for redisplay
if it is sent the message setText:. (It has to redraw its image if the text it displays changes.)
The redrawing of a view’s image is not immediate; instead, it is added to a list of views that need updating. To
understand when views actually get redrawn, we need to talk about the run loop – the infinite loop that comprises an
iOS application. The run loop’s job is to listen for input (a touch, Core Location updates, data coming in through a
network interface, etc.) and then find the appropriate handlers for that event (like an action or a delegate method for
an object). Those handler methods call other methods, which call more methods, and so on. Once all the methods
have completed, control returns to the run loop. At this point, the views are redrawn. Figure 6.10 shows where
redrawing views happens in the run loop.
When control returns to the run loop, it says, “Well, a bunch of code was just executed. I’m going to check if any
views need to be redrawn.” Then the run loop prepares the necessary drawing contexts and sends the message
drawRect: to all of the views that have been sent setNeedsDisplay in this iteration of the loop.
If one view redraws its image, the other views on the screen are not required to redraw theirs. Instead, their existing
images will just be composited on the screen again. This optimization is why drawing and animation on iOS feels so
responsive.
Motion Events
Instances of UIResponder can become the
first responder of the window and will receive events when the device is shaken or a key is pressed on the keyboard.
也即是成为First Responseder 时候会接收到事件
To give a UIResponder first responder status, you send it the message becomeFirstResponder. In
HypnosisterAppDelegate.m, tell the HypnosisView instance to become the first responder. The
becomeFirstResponder method returns a Boolean value indicating whether the receiving object successfully
became the first responder of the window.
[[self window] addSubview:view];
BOOL success = [view becomeFirstResponder];
if (success) {
NSLog(@"HypnosisView became the first responder");
} else {
NSLog(@"Could not become first responder");
}
self.window.backgroundColor = [UIColor whiteColor];
Build and run the application. Notice that the console tells you that the HypnosisView failed to become the first
responder. Most UIResponder objects return NO in becomeFirstResponder. This is because most views, by default,
only care about events connected with themselves, and they (almost) always get a chance to handle these events. For
example, a tapped UIButton gets sent a message regardless of who the first responder is. So, a responder object must
explicitly state that it is willing to become the first responder. In HypnosisView.m, override the UIResponder method
canBecomeFirstResponder to return YES.
- (BOOL)canBecomeFirstResponder
{
return YES;
} //光是发送becomeFirstResponder 还不够,因为默认情况下View只关心发生在自己的事件,而通常他们都是有机会去处理这些事件的,例如Target-action 机制.
所以要重写canBecomeFirstResponder
处理shakes 事件
For handling shakes, the methods you override in a UIResponder
subclass are known as the motion event methods. They are declared like so:
// Send to the first responder when the user starts shaking the device
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
// Sent to the first responder when the user stops shaking the device
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
// Sent to the first responder if the motion event is interrupted, like when
// a phone call or SMS message occurs while shaking
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
Using UIScrollView
Scroll views are typically used for views that are larger than the screen. A scroll view draws a rectangular portion of
its subview, and moving your finger, or panning, on the scroll view changes the position of that rectangle on the
subview. Thus, you can think of the scroll view as a viewing port that you move around a virtual world
(Figure 6.12). The size of the scroll view is the size of this viewing port. The size of the area it can view is the
UIScrollView’s contentSize, which is typically the size of the UIScrollView’s subview.
UIScrollView 其实是一个视窗,移动这个视窗就可以看到背后的整个Subview了,但每次看到的就是这个视窗个的大小.
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
CGRect screenRect = [[self window] bounds];
// Create the UIScrollView to have the size of the window, matching its size
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:screenRect];
[[self window] addSubview:scrollView];
HypnosisView *view =
[[HypnosisView alloc] initWithFrame:[[self window] bounds]];
// Create the HypnosisView with a frame that is twice the size of the screen
CGRect bigRect = screenRect;
bigRect.size.width *= 2.0;
bigRect.size.height *= 2.0;
HypnosisView *view = [[HypnosisView alloc] initWithFrame:bigRect];
[[self window] addSubview:view];
// Add the HypnosisView as a subview of the scrollView instead of the window
[scrollView addSubview:view];
// Tell the scrollView how big its virtual world is
[scrollView setContentSize:bigRect.size];
BOOL success = [view becomeFirstResponder];
Build and run your application. You can pan your view up and down, left and right.
HypnosisView 就是整个的大世界,而UIScrollView只是个窗口,用来观察这个大世界,但每次就看这个视窗的大小.
Panning and paging
In the last example, a scroll view was used to move around a much larger view. A scroll view can pan between a
number of different view instances. For example, if there are two screen-sized views, a user could pan between them.
In HypnosisterAppDelegate.m, shrink the HypnosisView back to the size of the screen and add another screensized
HypnosisView as a subview of the UIScrollView. Also, make the contentSize twice as wide as the screen,
but the same height.
CGRect bigRect = screenRect;
bigRect.size.width *= 2.0;
bigRect.size.height *= 2.0;
HypnosisView *view = [[HypnosisView alloc] initWithFrame:bigRect];
HypnosisView *view = [[HypnosisView alloc] initWithFrame:screenRect];
[scrollView addSubview:view];
// Move the rectangle for the other HypnosisView to the right, just off
// the screen
screenRect.origin.x = screenRect.size.width;
HypnosisView *anotherView = [[HypnosisView alloc] initWithFrame:screenRect];
[scrollView addSubview:anotherView];
// Tell the scrollView how big its virtual world is
[scrollView setContentSize:bigRect.size];
Build and run the application. Pan from left to right to see each instance of HypnosisView.
也就是这个scrollView 后面的世界可以不止一个,这个视窗可以这些不同View之间移动,还可以这个视窗跨在两个view之间, 如果不想的话,就要开启paging了,如下 :
Notice that you can stop in between the two HypnosisViews. Sometimes you want this, but other times, you do not.
To force the scroll view to snap its viewing port to one of the views, turn on paging for the scroll view in
HypnosisterAppDelegate.m.
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:screenRect];
[scrollView setPagingEnabled:YES];
[[self window] addSubview:scrollView];
Build and run the application. Pan to the middle of two HypnosisViews and see how it automatically scrolls to one of
the views. Paging works by taking the size of the scroll view’s bounds and dividing up the contentSize it displays
into sections of the same size. After the user pans, the view port will scroll to show only one of these sections.