The UIView class offers numerous
methods that help build and manage views. These methods let you add,
order, remove, and query the view hierarchy. Since this hierarchy
controls what you see onscreen, updating the way that views relate to
each other changes what you see on the iPhone. Here are some approaches
for typical view-management tasks.
Adding Subviews
Call [parentView addSubview:child]
to add new subviews to a parent. Newly added subviews are always placed
frontmost on your screen; the iPhone adds them on top of any existing
views. To insert a subview into the view hierarchy at a particular
location other than the front, the SDK offers a trio of utility methods:
These methods control where view insertion
happens. That insertion can remain relative to another view, or it can
move into a specific index of the subviews array. The above and below
methods add subviews in front of or behind a given child. Insertion
pushes other views forward and does not replace any views that are
already there.
Reordering and Removing Subviews
Applications often need to reorder and remove
views as users interact with the screen. The iPhone SDK offers several
easy ways to do this, allowing you to change the view order and
contents.
Use [parentView exchangeSubviewAtIndex:i withSubviewAtIndex:j] to exchange the positions of two views.
Move subviews to the front or back using bringSubviewToFront: and sendSubviewToBack.
To remove a subview from its parent, call [childView removeFromSuperview].
If the child view had been onscreen, it disappears. Removing a child
from the superview calls a release on the subview, allowing its memory
to be freed if its retain count has returned to zero.
When you reorder, add, or remove views, the screen automatically redraws to show the new view presentation.
View Callbacks
When the view hierarchy changes, callbacks can be
sent to the views in question. The iPhone SDK offers six callback
methods. These callbacks may help your application keep track of views
that are moving and changing parents.
didAddSubview: is sent to a view after a successful invocation of addSubview: lets subclasses of UIView perform additional actions when new views are added.
didMoveToSuperview:
informs views that they’ve been re-parented to a new superview. The
view may want to respond to that new parent in some way. When the view
was removed from its superview, the new parent is nil.
willMoveToSuperview: is sent before the move occurs.
didMoveToWindow:
provides the callback equivalent of didMoveToSuperview but when the
view moves to a new Window hierarchy instead of to just a new superview.
willMoveToWindow: is, again, sent before the move occurs.
willRemoveSubview: informs the parent view that the child view is about to be removed.
1. Tagging and Retrieving Views
The iPhone SDK offers a built-in search feature
that lets you recover views by tagging them. Tags are just numbers,
usually positive integers, that identify a view. Assign them using the
view’s tag property, for example, myView.tag = 101. In Interface Builder, you can set a view’s tag in the attributes inspector. As Figure 1 shows, you specify the tag in the View section.
Tags are completely arbitrary. The only “reserved”
tag is 0, which is the default property setting for all newly created
views. It’s up to you to decide how you want to tag your views and which
values to use. You can tag any instance that is a child of UIView,
including windows and controls. So if you have many onscreen buttons
and switches, adding tags helps tell them apart when users trigger them.
You can add a simple switch statement to your callback methods that
looks at the tag and determines how to react.
Apple rarely tags subviews. The only instance I
have ever found of their view tagging has been in UIAlertViews where the
buttons use tags of 1, 2, and so forth. (I’m half convinced they left
this tagging in there as a mistake.) If you worry about conflicting with
Apple tags, start your numbering at 10 or 100, or some other number
higher than any value Apple might use.
Using Tags to Find Views
Tags let you avoid passing user interface
elements around your program by making them directly accessible from any
parent view. The viewWithTag: method recovers a tagged view
from a child hierarchy. The search is recursive, so the tagged item need
not be an immediate child of the view in question. You can search from
the window with [window
viewWithTag:101] and find a view that is several branches down the hierarchy tree. When more than one view uses the same tag, viewWithTag: returns the first item it finds.
The problem with viewWithTag: is that it returns a UIView
object. This means you often have to cast it to the proper type before
you can use it. Say you want to retrieve a label and set its text.
UILabel *label = (UILabel *)[self.view.window viewWithTag:101];
label.text = @"Hello World";
It would be far easier to use a call that
returned an already typed object and then be able to use that object
right away, as these calls do:
- (IBAction)updateTime:(id)sender
{
// set the label to the current time
[self.view.window labelWithTag:LABEL_TAG].text =
[[NSDate date] description];
}
- (IBAction)updateSwitch:(id)sender
{
// toggle the switch from its current setting
UISwitch *s = [self.view.window switchWithTag:SWITCH_TAG];
[s setOn:!s.isOn];
}
Recipe 1 extends the behavior of UIView to introduce a new category, TagExtensions. This category adds just two typed tag methods, for UILabel and UISwitch. . The additional classes were omitted for space
considerations; they follow the same pattern of casting from viewWithTag:. Access the full collection by including the UIView-TagExtensions files in your projects.
Recipe 1. Recovering Tagged Views with Properly Cast Objects
@interface UIView (TagExtensions)
- (UILabel *) labelWithTag: (NSInteger) aTag;
- (UISwitch *) switchWithTag: (NSInteger) aTag;
@end
@implementation UIView (TagExtensions)
- (UILabel *) labelWithTag: (NSInteger) aTag
{
return (UILabel *)[self viewWithTag:aTag];
}
- (UISwitch *) switchWithTag: (NSInteger) aTag
{
return (UISwitch *)[self viewWithTag:aTag];
}
@end
2. Naming Views
Although tagging offers a thorough approach to
identifying views, some developers may prefer to work with names rather
than numbers. Using names adds an extra level of meaning to your view
identification schemes. Instead of referring to “the view with a tag of
101,” a switch named “Ignition Switch” describes its role and adds a
level of self-documentation missing from a plain number.
// Toggle switch
UISwitch *s = [self.view switchNamed:@"Ignition Switch"];
[s setOn:!s.isOn];
It’s relatively easy to design a class that
associates strings with view tags. This custom class needs to store a
dictionary that matches names with tags, allowing views to register and
unregister those names. Recipe 6-4 shows how to build that view name manager, which uses a singleton instance ([ViewIndexer sharedInstance]) to store its tag and name dictionary.
The class demands unique names. If a view name is
already registered, a new registration request will fail. If a view was
already registered under another name, a second registration request
will unregister the first name. There are ways to fool this of course.
If you change a view’s tag and then register it again, the indexer has
no way of knowing that the view had been previously registered. So if
you decide to use this approach, set your tags in Interface Builder or
let the registration process automatically tag the view but otherwise
leave the tags be.
If you build your views by hand, register them at
the same point you create them and add them into your overall view
hierarchy. When using an IB-defined view, register your names in viewDidLoad using the tag numbers you set in the attributes inspector.
- (void) viewDidLoad
{
[[self.view viewWithTag:LABEL_TAG] registerName:@"my label"];
[[self.view viewWithTag:SWITCH_TAG] registerName:@"my switch"];
}
Recipe 2 hides the view indexer class from public view. It wraps its calls inside a UIView category for name extensions. This allows you to register, retrieve, and unregister views without using ViewIndexer directly.
Recipe 2. Creating a View Name Manager
@interface ViewIndexer : NSObject {
NSMutableDictionary *tagdict;
NSInteger count;
}
@property (nonatomic, retain) NSMutableDictionary *tagdict;
@end
@implementation ViewIndexer
@synthesize tagdict;
static ViewIndexer *sharedInstance = nil;
+(ViewIndexer *) sharedInstance {
if(!sharedInstance) sharedInstance = [[self alloc] init];
return sharedInstance;
}
- (id) init
{
if (!(self = [super init])) return self;
self.tagdict = [NSMutableDictionary dictionary];
count = 10000;
return self;
}
- (void) dealloc
{
self.tagdict = nil;
[super dealloc];
}
// Pull a new number and increase the count
- (NSInteger) pullNumber
{
return count++;
}
// Check to see if name exists in dictionary
- (BOOL) nameExists: (NSString *) aName
{
return [self.tagdict objectForKey:aName] != nil;
}
// Pull out first matching name for tag
- (NSString *) nameForTag: (NSInteger) aTag
{
NSNumber *tag = [NSNumber numberWithInt:aTag];
NSArray *names = [self.tagdict allKeysForObject:tag];
if (!names) return nil;
if ([names count] == 0) return nil;
return [names objectAtIndex:0];
}
// Return the tag for a registered name. 0 if not found
- (NSInteger) tagForName: (NSString *)aName
{
NSNumber *tag = [self.tagdict objectForKey:aName];
if (!tag) return 0;
return [tag intValue];
}
// Unregistering reverts tag to 0
- (BOOL) unregisterName: (NSString *) aName forView: (UIView *) aView
{
NSNumber *tag = [self.tagdict objectForKey:aName];
// tag not found
if (!tag) return NO;
// tag does not match registered name
if (aView.tag != [tag intValue]) return NO;
aView.tag = 0;
[self.tagdict removeObjectForKey:aName];
return YES;
}
// Register a new name. Names will not re-register. (Unregister first,
// please). If a view is already registered, it is unregistered and
// re-registered
- (NSInteger) registerName:(NSString *)aName forView: (UIView *) aView
{
// You cannot re-register an existing name
if ([[ViewIndexer sharedInstance] nameExists:aName]) return 0;
// Check to see if the view is named already. If so, unregister.
NSString *currentName = [self nameForTag:aView.tag];
if (currentName) [self unregisterName:currentName forView:aView];
// Register the existing tag or pull a new tag if aView.tag is 0
if (!aView.tag) aView.tag = [[ViewIndexer sharedInstance]
pullNumber];
[self.tagdict setObject:[NSNumber numberWithInt:aView.tag]
forKey: aName];
return aView.tag;
}
@end
@implementation UIView (NameExtensions)
- (NSInteger) registerName: (NSString *) aName
{
return [[ViewIndexer sharedInstance] registerName: aName
forView: self];
}
- (BOOL) unregisterName: (NSString *) aName
{
return [[ViewIndexer sharedInstance] unregisterName: aName
forView:self];
}
- (UIView *) viewNamed: (NSString *) aName
{
NSInteger tag = [[ViewIndexer sharedInstance] tagForName: aName];
return [self viewWithTag: tag];
}
@end
|