This chapter discusses all UIView subclasses provided by UIKit that haven’t been discussed already (except for the two modal dialog classes, which are described in the next chapter). It’s remarkable how few of them there are; UIKit exhibits a noteworthy economy of means in this regard.
Additional UIView subclasses, as well as UIViewController subclasses that create interface, are provided by other frameworks. There will be lots of examples in Part III. For example, the Map Kit framework provides the MKMapView (Chapter 21); and the MessageUI framework provides MFMailComposeViewController, which supplies a user interface for composing and sending a mail message (Chapter 20).
An activity indicator (UIActivityIndicatorView) appears as the spokes of a small wheel. You set the spokes spinning with startAnimating
, giving the user a sense that some time-consuming process is taking place. You stop the spinning with stopAnimating
. If the activity indicator’s hidesWhenStopped
is YES (the default), it is visible only while spinning.
An activity indicator comes in a style, its activityIndicatorViewStyle
; if it is created in code, you’ll set its style with initWithActivityIndicatorStyle:
. Your choices are:
UIActivityIndicatorViewStyleWhiteLarge
UIActivityIndicatorViewStyleWhite
UIActivityIndicatorViewStyleGray
An activity indicator has a standard size, which depends on its style. Changing its size in code changes the size of the view, but not the size of the spokes. For bigger spokes, you can resort to a scale transform.
You can assign an activity indicator a color
; this overrides the color assigned through the style. An activity indicator is a UIView, so you can set its backgroundColor
; a nice effect is to give an activity indicator a contrasting background color and to round its corners by way of the view’s layer (Figure 12-1).
Here’s some code from a UITableViewCell subclass in one of my apps. In this app, it takes some time, after the user taps a cell to select it, for me to construct the next view and navigate to it; to cover the delay, I show a spinning activity indicator in the center of the cell while it’s selected:
- (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; if (selected) { UIActivityIndicatorView* v = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle: UIActivityIndicatorViewStyleWhiteLarge]; v.color = [UIColor yellowColor]; dispatch_async(dispatch_get_main_queue(), ^{ // cell tries to change background color match selection color v.backgroundColor = [UIColor colorWithWhite:0.2 alpha:0.6]; }); v.layer.cornerRadius = 10; CGRect f = v.frame; f = CGRectInset(f, -10, -10); v.frame = f; CGRect cf = self.frame; cf = [self.contentView convertRect:cf fromView:self]; v.center = CGPointMake(CGRectGetMidX(cf), CGRectGetMidY(cf)); v.tag = 1001; [self.contentView addSubview:v]; [v startAnimating]; } else { UIView* v = [self viewWithTag:1001]; if (v) { [v removeFromSuperview]; } } }
If activity involves the network, you might want to set UIApplication’s networkActivityIndicatorVisible
to YES. This displays a small spinning activity indicator in the status bar. The indicator is not reflecting actual network activity; if it’s visible, it’s spinning. Be sure to set it back to NO when the activity is over.
An activity indicator is simple and standard, but you can’t change the way it’s drawn. One obvious alternative would be a UIImageView with an animated image, as described in Chapter 4.
A progress view (UIProgressView) is a “thermometer,” graphically displaying a percentage. It is often used to represent a time-consuming process whose percentage of completion is known (if the percentage of completion is unknown, you’re more likely to use an activity indicator). But it’s good for static percentages too. In one of my apps, I use a progress view to show the current position within the song being played by the built-in music player; in another app, which is a card game, I use a progress view to show how many cards are left in the deck.
A progress view comes in a style, its progressViewStyle
; if the progress view is created in code, you’ll set its style with initWithProgressViewStyle:
. Your choices are:
UIProgressViewStyleDefault
UIProgressViewStyleBar
UIProgressViewStyleBar
is intended for use in a UIBarButtonItem, as the title view of a navigation item, and so on. In iOS 7, both styles by default draw the thermometer extremely thin — just 2 pixels and 3 pixels, respectively. (Figure 12-2 shows a UIProgressViewStyleDefault
progress view.) Changing a progress view’s frame height directly has no visible effect on how the thermometer is drawn. Under autolayout, to make a thicker thermometer, supply a height constraint with a larger value (thus overriding the intrinsic content height).
The fullness of the thermometer is the progress view’s progress
property. This is a value between 0 and 1, inclusive; you’ll usually need to do some elementary arithmetic in order to convert from the actual value you’re reflecting to a value within that range. For example, to reflect the number of cards remaining in a deck of 52 cards:
prog.progress = [[deck cards] count] / 52.0;
A change in progress
value can be animated by calling setProgress:animated:
.
In iOS 7, the default color of the filled portion of a progress view is the tintColor
(which may be inherited from higher up the view hierarchy). The default color for the unfilled portion is gray for a UIProgressViewStyleDefault
progress view and transparent for a UIProgressViewStyleBar
progress view. You can customize the colors; set the progress view’s progressTintColor
and trackTintColor
, respectively. This can also be done in the nib.
Alternatively, you can customize the image used to draw the filled portion of the progress view, the progress view’s progressImage
. If you do that, you can optionally customize also the image used to draw the unfilled portion, the trackImage
. This can also be done in the nib. Each image must be stretched to the length of the filled or unfilled area, so you’ll want to use a resizable image.
Here’s a simple example from one of my apps (Figure 12-3):
self.prog.backgroundColor = [UIColor blackColor]; self.prog.trackTintColor = [UIColor blackColor]; UIGraphicsBeginImageContextWithOptions(CGSizeMake(10,10), YES, 0); CGContextRef con = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(con, [UIColor yellowColor].CGColor); CGContextFillRect(con, CGRectMake(0, 0, 10, 10)); CGRect r = CGRectInset(CGContextGetClipBoundingBox(con),1,1); CGContextSetLineWidth(con, 2); CGContextSetStrokeColorWithColor(con, [UIColor blackColor].CGColor); CGContextStrokeRect(con, r); CGContextStrokeEllipseInRect(con, r); self.prog.progressImage = [UIGraphicsGetImageFromCurrentImageContext() resizableImageWithCapInsets:UIEdgeInsetsMake(0, 4, 0, 4)]; UIGraphicsEndImageContext();
For maximum flexibility, you can design your own UIView subclass that draws something similar to a thermometer. Figure 12-4 shows a simple custom thermometer view; it has a value
property, and you set this to something between 0 and 1 and call setNeedsDisplay
to make the view redraw itself. Here’s its drawRect:
code:
- (void)drawRect:(CGRect)rect { CGContextRef c = UIGraphicsGetCurrentContext(); [[UIColor whiteColor] set]; CGFloat ins = 2.0; CGRect r = CGRectInset(self.bounds, ins, ins); CGFloat radius = r.size.height / 2.0; CGMutablePathRef path = CGPathCreateMutable(); CGPathMoveToPoint(path, nil, CGRectGetMaxX(r)-radius, ins); CGPathAddArc(path, nil, radius+ins, radius+ins, radius, -M_PI/2.0, M_PI/2.0, true); CGPathAddArc(path, nil, CGRectGetMaxX(r)-radius, radius+ins, radius, M_PI/2.0, -M_PI/2.0, true); CGPathCloseSubpath(path); CGContextAddPath(c, path); CGContextSetLineWidth(c, 2); CGContextStrokePath(c); CGContextAddPath(c, path); CGContextClip(c); CGContextFillRect(c, CGRectMake( r.origin.x, r.origin.y, r.size.width * self.value, r.size.height)); }
A picker view (UIPickerView) displays selectable choices using a rotating drum metaphor. It has a standard legal range of possible heights, which is undocumented but seems to be between 162 and 180; its width is largely up to you. Each drum, or column, is called a component.
Your code configures the UIPickerView’s content through its data source (UIPickerViewDataSource) and delegate (UIPickerViewDelegate), which are usually the same object. Your data source and delegate must answer questions similar to those posed by a UITableView (Chapter 8):
numberOfComponentsInPickerView:
(data source)
pickerView:numberOfRowsInComponent:
(data source)
0
.
pickerView:titleForRow:forComponent:
pickerView:attributedTitleForRow:forComponent:
pickerView:viewForRow:forComponent:reusingView:
(delegate)
0
. You can supply a simple string, an attributed string (Chapter 10), or an entire view such as a UILabel; but you should supply every row of every component the same way. The reusingView
parameter, if not nil, is a view that you supplied for a row now no longer visible, giving you a chance to reuse it, much as cells are reused in a table view.
Here’s the code for a UIPickerView (Figure 12-5) that displays the names of the 50 U.S. states, stored in an array. We implement pickerView:viewForRow:forComponent:reusingView:
just because it’s the most interesting case; as our views, we supply UILabel instances. The state names appear centered because the labels are centered within the picker view:
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { return 1; } - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { return 50; } - (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view { UILabel* lab; if (view) lab = (UILabel*)view; // reuse it else lab = [UILabel new]; lab.text = self.states[row]; lab.backgroundColor = [UIColor clearColor]; [lab sizeToFit]; return lab; }
The delegate may further configure the UIPickerView’s physical appearance by means of these methods:
pickerView:rowHeightForComponent:
pickerView:widthForComponent:
The delegate may implement pickerView:didSelectRow:inComponent:
to be notified each time the user spins a drum to a new position. You can also query the picker view directly by sending it selectedRowInComponent:
.
You can set the value to which any drum is turned using selectRow:inComponent:animated:
. Other handy picker view methods allow you to request that the data be reloaded, and there are properties and methods to query the picker view’s contents (though of course they do not relieve you of responsibility for knowing the data model from which the picker view’s contents are supplied):
reloadComponent:
reloadAllComponents
numberOfComponents
numberOfRowsInComponent:
viewForRow:forComponent:
By implementing pickerView:didSelectRow:inComponent:
and using reloadComponent:
, you can make a picker view where the values displayed by one drum depend dynamically on what is selected in another. For example, one can imagine expanding our U.S. states example to include a second drum listing major cities in each state; when the user switches to a different state in the first drum, a different set of major cities appears in the second drum.
A search bar (UISearchBar) is essentially a wrapper for a text field; it has a text field as one of its subviews, though there is no official access to it. It is displayed by default as a rounded rectangle containing a magnifying glass icon, where the user can enter text (Figure 12-6). It does not, of itself, do any searching or display the results of a search; a common interface involves displaying the results of a search as a table, and the UISearchDisplayController class makes this easy to do (see Chapter 8).
A search bar’s current text is its text
property. It can have a placeholder
, which appears when there is no text. A prompt
can be displayed above the search bar to explain its purpose. Delegate methods (UISearchBarDelegate) notify you of editing events; for their use, compare the text field and text view delegate methods discussed in Chapter 10:
searchBarShouldBeginEditing:
searchBarTextDidBeginEditing:
searchBar:textDidChange:
searchBar:shouldChangeTextInRange:replacementText:
searchBarShouldEndEditing:
searchBarTextDidEndEditing:
A search bar has a barStyle
, for which your choices and their default appearances are:
UIBarStyleDefault
, a flat light gray background and a white search field
UIBarStyleBlack
, a black background and a black search field
In iOS 7, both styles are translucent.
In addition, iOS 7 introduces a searchBarStyle
property:
UISearchBarStyleDefault
, as already described
UISearchBarStyleProminent
, identical to UISearchBarStyleDefault
UISearchBarStyleMinimal
, transparent background and dark transparent search field
Alternatively, you can set a UISearchBarStyleDefault
search bar’s barTintColor
to change its background color; if the bar style is UIBarStyleBlack
, the barTintColor
will also tint the search field itself. An opaque barTintColor
is a way to make a search bar opaque. This property is new in iOS 7; the old tintColor
property, whose value may be inherited from higher up the view hierarchy, now governs the color of search bar components such as the Cancel button title and the flashing insertion cursor.
A search bar can also have a custom backgroundImage
; this will be treated as a resizable image. The full setter method in iOS 7 is setBackgroundImage:forBarPosition:barMetrics:
; I’ll talk later about what bar position and bar metrics are. The backgroundImage
overrides all other ways of determining the background, and the search bar’s backgroundColor
, if any, appears behind it — though under some circumstances, if the search bar’s translucent
is NO, the barTintColor
may appear behind it instead.
The search field area where the user enters text can be offset with respect to its background, using the searchFieldBackgroundPositionAdjustment
property; you might do this, for example, if you had enlarged the search bar’s height and wanted to position the search field within that height. The text can be offset within the search field with the searchTextPositionAdjustment
property.
You can also replace the image of the search field itself; this is the image that is normally a rounded rectangle. To do so, call setSearchFieldBackgroundImage:forState:
. According to the documentation, the possible state:
values are UIControlStateNormal
and UIControlStateDisabled
; but the API provides no way to disable a search field, so what does Apple have in mind here? The only way I’ve found is to cycle through the search bar’s subviews, find the search field, and disable it:
for (UIView* v in [self.searchbar.subviews[0] subviews]) { if ([v isKindOfClass: [UITextField class]]) { UITextField* tf = (UITextField*)v; tf.enabled = NO; break; } }
The search field image will be drawn in front of the background and behind the contents of the search field (such as the text); its width will be adjusted for you, but its height will not be — instead, the image is placed vertically centered where the search field needs to go. It’s up to you choose an appropriate height, and to ensure an appropriate color in the middle so the user can read the text.
A search bar displays an internal cancel button automatically (normally an X in a circle) if there is text in the search field. Internally, at its right end, a search bar may display a search results button (showsSearchResultsButton
), which may be selected or not (searchResultsButtonSelected
), or a bookmark button (showsBookmarkButton
); if you ask to display both, you’ll get the search results button. These buttons vanish if text is entered in the search bar so that the cancel button can be displayed. There is also an option to display a Cancel button externally (showsCancelButton
, or call setShowsCancelButton:animated:
). The internal cancel button works automatically to remove whatever text is in the field; the other buttons do nothing, but delegate methods notify you when they are tapped:
searchBarResultsListButtonClicked:
searchBarBookmarkButtonClicked:
searchBarCancelButtonClicked:
You can customize the images used for the search icon (a magnifying glass, by default) and any of the internal right icons (the internal cancel button, the search results button, and the bookmark button) with setImage:forSearchBarIcon:state:
. The images will be resized for you, except for the internal cancel button, for which about 20×20 seems to be a good size. The icons are specified with constants:
UISearchBarIconSearch
UISearchBarIconClear
(the internal cancel button)
UISearchBarIconBookmark
UISearchBarIconResultsList
The documentation says that the possible state:
values are UIControlStateNormal
and UIControlStateDisabled
, but this is wrong; the choices are UIControlStateNormal
and UIControlStateHighlighted
. The highlighted image appears while the user taps on the icon (except for the search icon, which isn’t a button). If you don’t supply a normal image, the default image is used; if you supply a normal image but no highlighted image, the normal image is used for both. Setting searchResultsButtonSelected
to YES reverses this button’s behavior: it displays the highlighted image, but when the user taps it, it displays the normal image.
The position of an icon can be adjusted with setPositionAdjustment:forSearchBarIcon:
.
A search bar may also display scope buttons (see the example in Chapter 8). These are intended to let the user alter the meaning of the search; precisely how you use them is up to you. To make the scope buttons appear, use the showsScopeBar
property; the button titles are the scopeButtonTitles
property, and the currently selected scope button is the selectedScopeButtonIndex
property. The delegate is notified when the user taps a different scope button:
searchBar:selectedScopeButtonIndexDidChange:
A background image applied with setBackgroundImage:forBarPosition:barMetrics:
using UIBarMetricsDefault
will not appear when the scope bar is showing. In iOS 7, when the scope bar is showing, the bar metrics value becomes UIBarMetricsDefaultPrompt
(or UIBarMetricsLandscapePhonePrompt
).
The overall look of the scope bar can be heavily customized. Its background is the scopeBarBackgroundImage
, which will be stretched or tiled as needed. To set the background of the smaller area constituting the actual buttons, call setScopeBarButtonBackgroundImage:forState:
; the states are UIControlStateNormal
and UIControlStateSelected
. If you don’t supply a separate selected image, a darkened version of the normal image is used. If you don’t supply a resizable image, the image will be made resizable for you; the runtime decides what region of the image will be stretched behind each button.
The dividers between the buttons are normally vertical lines, but you can customize them as well: call setScopeBarButtonDividerImage:forLeftSegmentState:rightSegmentState:
. A full complement of dividers consists of three images, one when the buttons on both sides of the divider are normal (unselected) and one each when a button on one side or the other is selected; if you supply an image for just one state combination, it is used for the other two state combinations. The height of the divider image is adjusted for you, but the width is not; you’ll normally use an image just a few pixels wide.
The font attributes of the titles of the scope buttons can customized by calling setScopeBarButtonTitleTextAttributes:forState:
. In iOS 7, the attributes:
argument is an NSAttributedString attributes dictionary; in earlier systems, you could set only the font, text color, and text shadow, but in iOS 7 it is legal to provide other attributes, such as an underline.
It may appear that there is no way to customize the external Cancel button, but in fact, although you’ve no official direct access to it through the search bar, the Cancel button is a UIBarButtonItem and you can customize it using the UIBarButtonItem appearance proxy, discussed later in this chapter.
By combining the various customization possibilities, a completely unrecognizable search bar of inconceivable ugliness can easily be achieved (Figure 12-7). Let’s be careful out there.
The problem of allowing the keyboard to appear without hiding the search bar is exactly as for a text field (Chapter 10). Text input properties of the search bar configure its keyboard and typing behavior like a text field as well:
keyboardType
autocapitalizationType
autocorrectionType
spellCheckingType
inputAccessoryView
When the user taps the Search key in the keyboard, the delegate is notified, and it is then up to you to dismiss the keyboard (resignFirstResponder
) and perform the search:
searchBarSearchButtonClicked:
A common interface is a search bar at the top of the screen. On the iPad, a search bar can be embedded as a bar button item’s view in a toolbar at the top of the screen. On the iPhone, a search bar can be a navigation item’s titleView
. In Chapter 8, I discussed UISearchDisplayController’s displaysSearchBarInNavigationBar
property. A search bar used in this way, however, has some limitations: for example, there may be no room for a prompt, scope buttons, or an external Cancel button, and you might not be able to assign it a background image or change its barTintColor
.
On the other hand, in iOS 7, a UISearchBar can itself function as a top bar, like a navigation bar without being in a navigation bar. If you use a search bar in this way, you’ll want its height to be extended automatically under the status bar; I’ll explain later in this chapter how to arrange that.
UIControl is a subclass of UIView whose chief purpose is to be the superclass of several further built-in classes and to endow them with common behavior. These are classes representing views with which the user can interact (controls).
The most important thing that controls have in common is that they automatically track and analyze touch events (Chapter 5) and report them to your code as significant control events by way of action messages. Each control implements some subset of the possible control events. The full set of control events is listed under UIControlEvents in the Constants section of the UIControl class documentation:
UIControlEventTouchDown
UIControlEventTouchDownRepeat
UIControlEventTouchDragInside
UIControlEventTouchDragOutside
UIControlEventTouchDragEnter
UIControlEventTouchDragExit
UIControlEventTouchUpInside
UIControlEventTouchUpOutside
UIControlEventTouchCancel
UIControlEventValueChanged
UIControlEventEditingDidBegin
UIControlEventEditingChanged
UIControlEventEditingDidEnd
UIControlEventEditingDidEndOnExit
UIControlEventAllTouchEvents
UIControlEventAllEditingEvents
UIControlEventAllEvents
The control events also have informal names that are visible in the Connections inspector when you’re editing a nib. I’ll mostly use the informal names in the next couple of paragraphs.
Control events fall roughly into three groups: the user has touched the screen (Touch Down, Touch Drag Inside, Touch Up Inside, etc.), edited text (Editing Did Begin, Editing Changed, etc.), or changed the control’s value (Value Changed).
Apple’s documentation is rather coy about which controls normally emit actions for which control events, so here’s a list obtained through experimentation (but keep in mind that Apple’s silence on this matter may mean that the details are subject to change):
For each control event that you want to hear about automatically, you attach to the control one or more target–action pairs. You can do this in the nib or in code.
For any given control, each control event and its target–action pairs form a dispatch table. The following methods permit you to manipulate and query the dispatch table:
addTarget:action:forControlEvents:
removeTarget:action:forControlEvents:
actionsForTarget:forControlEvent:
allTargets
allControlEvents
(a bitmask of control events to which a target–action pair is attached)
An action method (the method that will be called on the target when the control event occurs) may adopt any of three signatures, whose parameters are:
The second signature is by far the most common. It’s unlikely that you’d want to dispense altogether with the parameter telling you which control sent the control event. On the other hand, it’s equally unlikely that you’d want to examine the original UIEvent that triggered this control event, since control events deliberately shield you from dealing with the nitty-gritty of touches — though you might have some reason to examine the UIEvent’s timestamp
.
When a control event occurs, the control consults its dispatch table, finds all the target–action pairs associated with that control event, and reports the control event by sending each action message to the corresponding target.
The action messaging mechanism is actually more complex than I’ve just stated. The UIControl does not really send the action message directly; rather, it tells the shared application to send it. When a control wants to send an action message reporting a control event, it calls its own sendAction:to:forEvent:
method. This in turn calls the shared application instance’s sendAction:to:from:forEvent:
, which actually sends the specified action message to the specified target. In theory, you could call or override either of these methods to customize this aspect of the message-sending architecture, but it is extremely unlikely that you would do so.
To make a control emit its action message(s) corresponding to a particular control event right now, in code, call its sendActionsForControlEvents:
method (which is never called automatically by the framework). For example, suppose you tell a UISwitch programmatically to change its setting from Off to On. This doesn’t cause the switch to report a control event, as it would if the user had slid the switch from Off to On; if you wanted it to do so, you could use sendActionsForControlEvents:
, like this:
[self.sw setOn: YES animated: YES]; [self.sw sendActionsForControlEvents:UIControlEventValueChanged];
You might also use sendActionsForControlEvents:
in a subclass to customize the circumstances under which a control reports control events.
A control has enabled
, selected
, and highlighted
properties; any of these can be YES or NO independently of the others. Together, they correspond to the control’s state
, which is reported as a bitmask of three possible values:
UIControlStateHighlighted
UIControlStateDisabled
UIControlStateSelected
A fourth state, UIControlStateNormal
, corresponds to a zero state
bitmask, and means that enabled
, selected
, and highlighted
are all NO.
A control that is not enabled does not respond to user interaction. Whether the control also portrays itself differently, to cue the user to this fact, depends upon the control. For example, a disabled UISwitch is faded; but a rounded rect text field gives the user no cue that it is disabled. The visual nature of control selection and highlighting, too, depends on the control. Neither highlighting nor selection make any difference to the appearance of a UISwitch, but a highlighted UIButton usually looks quite different from a nonhighlighted UIButton.
A control has contentHorizontalAlignment
and contentVerticalAlignment
properties. These matter only if the control has content that can be aligned. You are most likely to use them in connection with a UIButton to position its title and internal image.
A text field (UITextField) is a control; see Chapter 10. A refresh control (UIRefreshControl) is a control; see Chapter 8. The remaining controls are covered here, and then I’ll give a simple example of writing your own custom control.
A switch (UISwitch, Figure 12-8) portrays a BOOL value: it looks like a sliding switch, and its on
property is either YES or NO. The user can slide or tap to toggle the switch’s position. When the user changes the switch’s position, the switch reports a Value Changed control event. To change the on
property’s value with accompanying animation, call setOn:animated:
.
A switch has only one size (radically changed in iOS 7 to 51×31); any attempt to set its size will be ignored.
You can customize a switch’s appearance by setting these properties:
onTintColor
thumbTintColor
tintColor
In iOS 7, a switch’s track when the switch is in the Off position is transparent, and can’t be customized. I regard this as a bug. (Changing the switch’s backgroundColor
is not a successful workaround, because the background color shows outside the switch’s outline.)
The UISwitch properties onImage
and offImage
, added in iOS 6 after much clamoring (and hacking) by developers, are withdrawn in iOS 7.
Don’t name a UISwitch instance variable or property switch
, as this is a reserved word in C.
A stepper (UIStepper, Figure 12-9) lets the user increase or decrease a numeric value: it looks like two buttons side by side, one labeled (by default) with a minus sign, the other with a plus sign. The user can tap or hold a button, and can slide a finger from one button to the other as part of the same interaction with the stepper. It has only one size (apparently 94×29). It maintains a numeric value, which is its value
. Each time the user increments or decrements the value, it changes by the stepper’s stepValue
. If the minimumValue
or maximumValue
is reached, the user can go no further in that direction, and to show this, the corresponding button is disabled — unless the stepper’s wraps
property is YES, in which case the value goes beyond the maximum by starting again at the minimum, and vice versa.
As the user changes the stepper’s value
, a Value Changed control event is reported. Portraying the numeric value itself is up to you; you might, for example, use a label or (as here) a progress view:
- (IBAction)doStep:(UIStepper*)step { self.prog.progress = step.value / (step.maximumValue - step.minimumValue); }
If a stepper’s continuous
is YES (the default), a long touch on one of the buttons will update the value repeatedly; the updates start slowly and get faster. If the stepper’s autorepeat
is NO, the updated value is not reported as a Value Changed control event until the entire interaction with the stepper ends; the default is YES.
The appearance of a stepper can be customized. In iOS 7, the color of the outline and the button captions is the stepper’s tintColor
, which may be inherited from further up the view hierarchy. You can also dictate the images that constitute the stepper’s structure with these methods:
setDecrementImageForState:
setIncrementImageForState:
setDividerImage:forLeftSegmentState:rightSegmentState:
setBackgroundImage:forState:
The images work similarly to a search bar’s scope bar (described earlier in this chapter). The background images should probably be resizable. They are stretched behind both buttons, half the image being seen as the background of each button. If the button is disabled (because we’ve reached the value’s limit in that direction), it displays the UIControlStateDisabled
background image; otherwise, it displays the UIControlStateNormal
background image, except that it displays the UIControlStateHighlighted
background image while the user is tapping it. You’ll probably want to provide all three background images if you’re going to provide any; the default is used if a state’s background image is nil. You’ll probably want to provide three divider images as well, to cover the three combinations normal left and normal right, highlighted left and normal right, and normal left and highlighted right. The increment and decrement images are composited on top of the background image; in iOS 7, they are treated as template images, colored by the tintColor
, unless you explicitly provide a UIImageRenderingModeAlwaysOriginal
image. At a minimum, you’ll provide a UIControlStateNormal
image, which will be adjusted automatically for the other two states, though of course you can provide all three images for each button. Figure 12-9 shows a customized stepper.
A page control (UIPageControl) is a row of dots; each dot is called a page, because it is intended to be used in conjunction with some other interface that portrays something analogous to pages, such as a UIScrollView with its pagingEnabled
set to YES. Coordinating the page control with this other interface is usually up to you; see Chapter 7 for an example. A UIPageViewController in scroll style can optionally display a page control that’s automatically coordinated with its content (Chapter 6).
The number of dots is the page control’s numberOfPages
. To learn the minimum size required for a given number of pages, call sizeForNumberOfPages:
. You can make the page control wider than the dots to increase the target region on which the user can tap. The user can tap to one side or the other of the current page’s dot to increment or decrement the current page; the page control then reports a Value Changed control event. It is possible to set a page control’s backgroundColor
to show the user the tappable area, but that isn’t commonly done: the background is usually transparent.
The dot colors differentiate the current page, the page control’s currentPage
, from the others; by default, the current page is portrayed as a solid dot, while the others are slightly transparent. You can customize a page control’s pageIndicatorTintColor
(the color of the dots in general) and currentPageIndicatorTintColor
(the color of the current page’s dot).
If a page control’s hidesForSinglePage
is YES, the page control becomes invisible when its numberOfPages
changes to 1.
If a page control’s defersCurrentPageDisplay
is YES, then when the user taps to increment or decrement the page control’s value, the display of the current page is not changed. A Value Changed control event is reported, but it is up to your code to handle this action and call updateCurrentPageDisplay
. A case in point might be if the user’s changing the current page triggers an animation, but you don’t want the current page dot to change until the animation ends.
A date picker (UIDatePicker) looks like a UIPickerView (discussed earlier in this chapter), but it is not a UIPickerView subclass; it uses a UIPickerView to draw itself, but it provides no official access to that picker view. Its purpose is to express the notion of a date and time, taking care of the calendrical and numerical complexities so that you don’t have to. When the user changes its setting, the date picker reports a Value Changed control event.
A UIDatePicker has one of four modes (datePickerMode
), determining how it is drawn:
UIDatePickerModeTime
UIDatePickerModeDate
UIDatePickerModeDateAndTime
UIDatePickerModeCountDownTimer
Exactly what components a date picker displays, and what values they contain, depends by default upon the user’s preferences in the Settings app (General → International → Region Format). For example, a U.S. time displays an hour numbered 1 through 12 plus minutes and AM or PM, but a British time displays an hour numbered 1 through 24 plus minutes. If the user changes the region format in the Settings app, the date picker’s display will change immediately.
A date picker has calendar
and timeZone
properties, respectively an NSCalendar and an NSTimeZone; these are nil by default, meaning that the date picker responds to the user’s system-level settings. You can also change these values manually; for example, if you live in California and you set a date picker’s timeZone
to GMT, the displayed time is shifted forward by 8 hours, so that 11 AM is displayed as 7 PM (if it is winter).
Don’t change the timeZone
of a UIDatePickerModeCountDownTimer
date picker; if you do, the displayed value will be shifted, and you will confuse the heck out of yourself and your users.
The minutes component, if there is one, defaults to showing every minute, but you can change this with the minuteInterval
property. The maximum value is 30
, in which case the minutes component values are 0 and 30. An attempt to set a value that doesn’t divide evenly into 60 will be silently ignored.
The date represented by a date picker (unless its mode is UIDatePickerModeCountDownTimer
) is its date
property, an NSDate. The default date is now, at the time the date picker is instantiated. For a UIDatePickerModeDate
date picker, the time by default is 12 AM (midnight), local time; for a UIDatePickerModeTime
date picker, the date by default is today. The internal value is reckoned in the local time zone, so it may be different from the displayed value, if you have changed the date picker’s timeZone
.
The maximum and minimum values enabled in the date picker are determined by its maximumDate
and minimumDate
properties. Values outside this range may appear disabled. There isn’t really any practical limit on the range that a date picker can display, because the “drums” representing its components are not physical, and values are added dynamically as the user spins them. In this example, we set the initial minimum and maximum dates of a date picker (self.picker
) to the beginning and end of 1954. We also set the actual date
, so that the date picker will be set initially to a value within the minimum–maximum range:
self.picker.datePickerMode = UIDatePickerModeDate; NSDateComponents* dc = [NSDateComponents new]; [dc setYear:1954]; [dc setMonth:1]; [dc setDay:1]; NSCalendar* c = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; NSDate* d = [c dateFromComponents:dc]; self.picker.minimumDate = d; [dc setYear:1955]; d = [c dateFromComponents:dc]; self.picker.maximumDate = d; self.picker.date = self.picker.minimumDate;
Don’t set the maximumDate
and minimumDate
properties values for a UIDatePickerModeCountDownTimer
date picker; if you do, you might cause a crash with an out-of-range exception.
To convert between an NSDate and a string, you’ll need an NSDateFormatter (see Apple’s Date and Time Programming Guide):
NSDate* d = self.picker.date; NSDateFormatter* df = [NSDateFormatter new]; [df setTimeStyle:kCFDateFormatterFullStyle]; [df setDateStyle:kCFDateFormatterFullStyle]; NSLog(@"%@", [df stringFromDate:d]); // "Tuesday, August 10, 1954, 3:16:25 AM GMT-07:00"
The value displayed in a UIDatePickerModeCountDownTimer
date picker is its countDownDuration
; this is an NSTimeInterval, which is a double representing a number of seconds, even though the minimum interval displayed is a minute. A UIDatePickerModeCountDownTimer
date picker does not actually do any counting down! You are expected to use some other interface to display the countdown. The Timer tab of Apple’s Clock app shows a typical interface; the user configures the date picker to set the countDownDuration
initially, but once the counting starts, the date picker is hidden and a label displays the remaining time.
Converting the countDownDuration
from an NSTimeInterval to hours and minutes is up to you; you could use the built-in calendrical classes:
NSTimeInterval t = self.picker.countDownDuration; NSDate* d = [NSDate dateWithTimeIntervalSinceReferenceDate:t]; NSCalendar* c = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; [c setTimeZone: [NSTimeZone timeZoneForSecondsFromGMT:0]]; // normalize NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit; NSDateComponents* dc = [c components:units fromDate:d]; NSLog(@"%d hr, %d min", [dc hour], [dc minute]);
A nasty iOS 7 bug makes the Value Changed event from a UIDatePickerModeCountDownTimer
date picker unreliable (especially just after the app launches, and whenever the user has tried to set the timer to zero). The workaround is not to rely on the Value Changed event; for example, provide a button in the interface that the user can tap to make your code read the date picker’s countDownDuration
.
A slider (UISlider) is an expression of a continuously settable value (its value
) between some minimum and maximum (its minimumValue
and maximumValue
; they are 0 and 1 by default). It is portrayed as an object, the thumb, positioned along a track. As the user changes the thumb’s position, the slider reports a Value Changed control event; it may do this continuously as the user presses and drags the thumb (if the slider’s continuous
is YES, the default) or only when the user releases the thumb (if its continuous
is NO). While the user is pressing on the thumb, the slider is in the highlighted
state. To change the slider’s value with animation, call setValue:animated:
. (But in iOS 7, there’s no animation; that’s presumably a bug.)
A commonly expressed desire is to modify a slider’s behavior so that if the user taps on its track, the slider moves to the spot where the user tapped. Unfortunately, a slider does not, of itself, respond to taps on its track; such a tap doesn’t even cause it to report a Touch Up Inside control event. However, with a gesture recognizer, most things are possible; here’s the action handler for a UITapGestureRecognizer attached to a UISlider:
- (void) tapped: (UIGestureRecognizer*) g { UISlider* s = (UISlider*)g.view; if (s.highlighted) return; // tap on thumb, let slider deal with it CGPoint pt = [g locationInView: s]; CGRect track = [s trackRectForBounds:s.bounds]; if (!CGRectContainsPoint(CGRectInset(track, 0, -10), pt)) return; // not on track, forget it CGFloat percentage = pt.x / s.bounds.size.width; CGFloat delta = percentage * (s.maximumValue - s.minimumValue); CGFloat value = s.minimumValue + delta; [s setValue:value animated:YES]; }
In iOS 7, a slider’s tintColor
(which may be inherited from further up the view hierarchy) determines the color of the track to the left of the thumb. You can change the color of the two parts of the track with the minimumTrackTintColor
and maximumTrackTintColor
properties. (The thumbTintColor
property does not affect the thumb color in iOS 7.)
To go further, you can provide your own thumb image and your own track image, along with images to appear at each end of the track, and you can override in a subclass the methods that position these.
The images at the ends of the track are the slider’s minimumValueImage
and maximumValueImage
, and they are nil by default. If you set them to actual images (which can also be done in the nib), the slider will attempt to position them within its own bounds, shrinking the drawing of the track to compensate.
You can change that behavior by overriding minimumValueImageRectForBounds:
, maximumValueImageRectForBounds:
, and trackRectForBounds:
in a subclass. The bounds passed in are the slider’s bounds. In this example (Figure 12-11), we expand the track width to the full width of the slider, and draw the images outside the slider’s bounds. The images are still visible, because the slider does not clip its subviews to its bounds. In the figure, I’ve given the slider a green background color so you can see how the track and images are related to its bounds:
- (CGRect)maximumValueImageRectForBounds:(CGRect)bounds { CGRect result = [super maximumValueImageRectForBounds:bounds]; result = CGRectOffset(result, 31, 0); return result; } - (CGRect)minimumValueImageRectForBounds:(CGRect)bounds { CGRect result = [super minimumValueImageRectForBounds:bounds]; result = CGRectOffset(result, -31, 0); return result; } - (CGRect)trackRectForBounds:(CGRect)bounds { CGRect result = [super trackRectForBounds:bounds]; result.origin.x = 0; result.size.width = bounds.size.width; return result; }
The thumb is also an image, and you set it with setThumbImage:forState:
. There are two chiefly relevant states, UIControlStateNormal
and UIControlStateHighlighted
. If you supply images for both, the thumb will change automatically while the user is dragging it. By default, the image will be centered in the track at the point represented by the slider’s current value; you can shift this position by overriding thumbRectForBounds:trackRect:value:
in a subclass. In this example, the image is repositioned upward slightly (Figure 12-12):
- (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value { CGRect result = [super thumbRectForBounds:bounds trackRect:rect value:value]; result = CGRectOffset(result, 0, -7); return result; }
Enlarging or offsetting a slider’s thumb can mislead the user as to the area on which it can be touched to drag it. The slider, not the thumb, is the touchable UIControl; only the part of the thumb that intersects the slider’s bounds will be draggable. The user may try to drag the part of the thumb that is drawn outside the slider’s bounds, and will fail (and be confused). A solution is to increase the slider’s height; if you’re using autolayout, you can add an explicit height constraint in the nib, or override intrinsicContentSize
in code (Chapter 1).
The track is two images, one appearing to the left of the thumb, the other to its right. They are set with setMinimumTrackImage:forState:
and setMaximumTrackImage:forState:
. If you supply images both for normal state and for highlighted state, the images will change while the user is dragging the thumb.
The images should be resizable, because that’s how the slider cleverly makes it look like the user is dragging the thumb along a single static track. In reality, there are two images; as the user drags the thumb, one image grows horizontally and the other shrinks horizontally. For the left track image, the right end cap inset will be partially or entirely hidden under the thumb; for the right track image, the left end cap inset will be partially or entirely hidden under the thumb. Figure 12-13 shows a track derived from a single 15×15 image of a circular object (a coin):
UIImage* coin = [UIImage imageNamed: @"coin.png"]; UIImage* coinEnd = [coin resizableImageWithCapInsets:UIEdgeInsetsMake(0,7,0,7) resizingMode:UIImageResizingModeStretch]; [slider setMinimumTrackImage:coinEnd forState:UIControlStateNormal]; [slider setMaximumTrackImage:coinEnd forState:UIControlStateNormal];
A segmented control (UISegmentedControl, Figure 12-14) is a row of tappable segments; a segment is rather like a button. The user is thus choosing among options. By default (momentary
is NO), the most recently tapped segment remains selected. Alternatively (momentary
is YES), the tapped segment is shown as highlighted momentarily (by default, highlighted is indistinguishable from selected, but you can change that); afterward, no segment selection is displayed, though internally the tapped segment remains the selected segment.
The selected segment can be set and retrieved with the selectedSegmentIndex
property; when you set it in code, the selected segment remains visibly selected, even for a momentary
segmented control. A selectedSegmentIndex
value of UISegmentedControlNoSegment
means no segment is selected. When the user taps a segment that isn’t already visibly selected, the segmented control reports a Value Changed event.
A segment can be separately enabled or disabled with setEnabled:forSegmentAtIndex:
, and its enabled state can be retrieved with isEnabledForSegmentAtIndex:
. A disabled segment, by default, is drawn faded; the user can’t tap it, but it can still be selected in code.
A segment has either a title or an image; when one is set, the other becomes nil. In iOS 7, an image is treated as a template image, colored by the tintColor
, unless you explicitly provide a UIImageRenderingModeAlwaysOriginal
image. The methods for setting and fetching the title and image for existing segments are:
setTitle:forSegmentAtIndex:
, titleForSegmentAtIndex:
setImage:forSegmentAtIndex:
, imageForSegmentAtIndex:
You will also want to set the title or image when creating the segment. You can do this in code if you’re creating the segmented control from scratch, with initWithItems:
, which takes an array each item of which is either a string or an image.
Methods for managing segments dynamically are:
insertSegmentWithTitle:atIndex:animated:
insertSegmentWithImage:atIndex:animated:
removeSegmentAtIndex:animated:
removeAllSegments
The number of segments can be retrieved with the read-only numberOfSegments
property.
A segmented control has a standard height; if you’re using autolayout, you can change the height through constraints or by overriding intrinsicContentSize
— or by setting its background image, as I’ll describe in a moment.
If you’re using autolayout, the width of all segments and the intrinsicContentSize
width of the entire segmented control are adjusted automatically whenever you set a segment’s title or image. If the segmented control’s apportionsSegmentWidthsByContent
property is NO, segment sizes will be made equal to one another; if it is YES, each segment will be sized individually to fit its content. Alternatively, you can set a segment’s width explicitly with setWidth:forSegmentAtIndex:
(and retrieve it with widthForSegmentAtIndex:
); setting a width of 0
means that this segment is to be sized automatically.
To change the position of the content (title or image) within a segment, call setContentOffset:forSegmentAtIndex:
(and retrieve it with contentOffsetForSegmentAtIndex:
).
In iOS 7, the color of a segmented control’s outline, title text, and selection are dictated by its tintColor
, which may be inherited from further up the view hierarchy. (The segmentedControlStyle
property is deprecated in iOS 7.)
Further methods for customizing a segmented control’s appearance are parallel to those for setting the look of a stepper or the scope bar portion of a search bar, both described earlier in this chapter. You can set the overall background, the divider image, the text attributes for the segment titles, and the position of segment contents:
setBackgroundImage:forState:barMetrics:
setDividerImage:forLeftSegmentState:rightSegmentState:barMetrics:
setTitleTextAttributes:forState:
setContentPositionAdjustment:forSegmentType:barMetrics:
You don’t have to customize for every state, as the segmented control will use the normal state setting for the states you don’t specify. As I mentioned a moment ago, setting a background image changes the segmented control’s height.
Here’s the code that achieved Figure 12-15; selecting a segment automatically darkens the background image for us (similar to a button’s adjustsImageWhenHighlighted
, described in the next section), so there’s no need to specify a separate selected image:
// background, set desired height but make width resizable // sufficient to set for Normal only UIImage* image = [UIImage imageNamed: @"linen.png"]; CGFloat w = 100; CGFloat h = 60; UIGraphicsBeginImageContextWithOptions(CGSizeMake(w,h), NO, 0); [image drawInRect:CGRectMake(0,0,w,h)]; UIImage* image2 = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); UIImage* image3 = [image2 resizableImageWithCapInsets:UIEdgeInsetsMake(0,10,0,10) resizingMode:UIImageResizingModeStretch]; [self.seg setBackgroundImage:image3 forState:UIControlStateNormal barMetrics:UIBarMetricsDefault]; // segment images, redraw at final size NSArray* pep = @[@"manny.jpg", @"moe.jpg", @"jack.jpg"]; for (int i = 0; i < 3; i++) { UIImage* image = [UIImage imageNamed: pep[i]]; UIGraphicsBeginImageContextWithOptions(CGSizeMake(30,30), NO, 0); [image drawInRect:CGRectMake(0,0,30,30)]; UIImage* image2 = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); image2 = [image2 imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; [self.seg setImage:image2 forSegmentAtIndex:i]; [self.seg setWidth:80 forSegmentAtIndex:i]; } // divider, set at desired width, sufficient to set for Normal only UIGraphicsBeginImageContextWithOptions(CGSizeMake(1,10), NO, 0); [[UIColor whiteColor] set]; CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0,0,1,10)); UIImage* div = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); [self.seg setDividerImage:div forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
The segmentType:
parameter in setContentPositionAdjustment:forSegmentType:barMetrics:
is needed because, by default, the segments at the two extremes have rounded ends (and, if a segment is the lone segment, both its ends are rounded). The argument allows you distinguish between the various possibilities:
UISegmentedControlSegmentAny
UISegmentedControlSegmentLeft
UISegmentedControlSegmentCenter
UISegmentedControlSegmentRight
UISegmentedControlSegmentAlone
The barMetrics:
parameter will be ignored in iOS 7 unless its value is UIBarMetricsDefault
.
A button (UIButton) is a fundamental tappable control, which may contain a title, an image, and a background image (and may have a backgroundColor
). A button has a type, and the code creation method is a class method, buttonWithType:
. The types are:
UIButtonTypeSystem
tintColor
, which may be inherited from further up the view hierarchy; when the button is tapped, the title text color momentarily changes to a color derived from what’s behind it (which might be the button’s backgroundColor
). The image is treated as a template image, colored by the tintColor
, unless you explicitly provide a UIImageRenderingModeAlwaysOriginal
image; when the button is tapped, the image (even if it isn’t a template image) is momentarily tinted to a color derived from what’s behind it.
UIButtonTypeDetailDisclosure
UIButtonTypeInfoLight
UIButtonTypeInfoDark
UIButtonTypeContactAdd
UIButtonTypeSystem
buttons whose image is set automatically to standard button images. In iOS 7, the first three are an “i” in a circle, and the last is a Plus in a circle; the two Info
types are identical, and they differ from the DetailDisclosure
type only in that their showsTouchWhenHighlighted
is YES by default.
UIButtonTypeCustom
UIButtonTypeSystem
, except that there’s no automatic coloring of the title or image by the tintColor
or the color of what’s behind the button, and the image is a normal image by default.
UIButtonTypeSystem
replaces the old UIButtonTypeRoundedRect
, which equates to it numerically. There is no built-in iOS 7 button type with an outline (border). You can add an outline — by adding a background image, for example, or by manipulating the button’s layer — but the default look of an iOS 7 button is the text or image alone. In one of my apps, I make a button stand out a bit more, like a pre-iOS 7 button, entirely through settings made in the nib:
layer.borderWidth
to 2 and its layer.cornerRadius
to 5.
A button has a title, a title color, and a title shadow color — or you can supply an attributed title, thus dictating these features and more in a single value through an NSAttributedString (Chapter 10).
Distinguish a button’s internal image from its background image. The background image, if any, is stretched, if necessary, to fill the button’s bounds (technically, its backgroundRectForBounds:
). The image, on the other hand, if smaller than the button, is not resized, and is thus shown internally within the button. The button can have both a title and an image, if the image is small enough; in that case, the image is shown to the left of the title by default.
These six features (title, title color, title shadow color, attributed title, image, and background image) can all be made to vary depending on the button’s current state: UIControlStateHighlighted
, UIControlStateSelected
, UIControlStateDisabled
, and UIControlStateNormal
. The button can be in more than one state at once, except for UIControlStateNormal
which means “none of the other states”. A state change, whether automatic (the button is highlighted while the user is tapping it) or programmatically imposed, will thus in and of itself alter a button’s appearance. The methods for setting these button features, therefore, all involve specifying a corresponding state — or multiple states, using a bitmask:
setTitle:forState:
setTitleColor:forState:
setTitleShadowColor:forState:
setAttributedTitle:forState:
setImage:forState:
setBackgroundImage:forState:
Similarly, when getting these button features, you must either specify a single state you’re interested in or ask about the feature as currently displayed:
titleForState:
, currentTitle
titleColorForState:
, currentTitleColor
titleShadowColorForState:
, currentTitleShadowColor
attributedTitleForState:
, currentAttributedTitle
imageForState:
, currentImage
backgroundImageForState:
, currentBackgroundImage
If you don’t specify a feature for a particular state, or if the button adopts more than one state at once, an internal heuristic is used to determine what to display. I can’t describe all possible combinations, but here are some general observations:
UIControlStateNormal
only; this is sufficient to give the button a title in every state.)
A UIButtonTypeSystem
button with an attributed normal title will tint the title to the tintColor
if you don’t give the attributed string a color, and will tint the title while highlighted to the color derived from what’s behind the button if you haven’t supplied a highlighted title with its own color. But a UIButtonTypeCustom
button will not do any of that; it leaves control of the title color for each state completely up to you.
In addition, a UIButton has some properties determining how it draws itself in various states, which can save you the trouble of specifying different images for different states:
showsTouchWhenHighlighted
UIButtonTypeInfoLight
or UIButtonTypeInfoDark
button. If the button has no internal image, the glow is centered at the button’s center. The glow is drawn on top of the background image or color, if any.
adjustsImageWhenHighlighted
UIButtonTypeCustom
button, if this property is YES (the default), then if there is no separate highlighted image (and if showsTouchWhenHighlighted
is NO), the normal image is darkened when the button is highlighted. This applies equally to the internal image and the background image. (A UIButtonTypeSystem
button is already tinting its highlighted image, so this property doesn’t apply.)
adjustsImageWhenDisabled
UIButtonTypeCustom
button and NO for a UIButtonTypeSystem
button.
A button has a natural size in relation to its contents. If you’re using autolayout, the button can adopt that size automatically as its intrinsicContentSize
, and you can modify the way it does this by overriding intrinsicContentSize
in a subclass or by applying explicit constraints. If you’re not using autolayout and you create a button in code, send it sizeToFit
or give it an explicit size — otherwise, the button will have a zero size and you’ll be left wondering why your button hasn’t appeared in the interface.
The title is a UILabel (Chapter 10), and the label features of the title can be accessed through the button’s titleLabel
. Its properties may be set, provided they do not conflict with existing UIButton features. For example, you can set the title’s font
and shadowOffset
by way of the label, but the title’s text, color, and shadow color should be set using the appropriate button methods. If the title is given a shadow in this way, then the button’s reversesTitleShadowWhenHighlighted
property also applies: if YES, the shadowOffset
values are replaced with their additive inverses when the button is highlighted. Similarly, you can manipulate the label’s wrapping behavior to make the button’s title consist of multiple lines. The modern way, however, is to manipulate all these features using attributed strings.
The internal image is drawn by a UIImageView (Chapter 2), whose features can be accessed through the button’s imageView
. Thus, for example, you can change the internal image view’s alpha
to make the image more transparent.
The internal position of the image and title as a whole are governed by the button’s contentVerticalAlignment
and contentHorizontalAlignment
(inherited from UIControl). You can also tweak the position of the image and title, together or separately, by setting the button’s contentEdgeInsets
, titleEdgeInsets
, or imageEdgeInsets
. Increasing an inset component increases that margin; thus, for example, a positive top
component makes the distance between that object and the top of the button larger than normal (where “normal” is where the object would be according to the alignment settings). The titleEdgeInsets
or imageEdgeInsets
values are added to the overall contentEdgeInsets
values. So, for example, if you really wanted to, you could make the internal image appear to the right of the title by decreasing the left titleEdgeInsets
and increasing the left imageEdgeInsets
.
Four methods also provide access to the button’s positioning of its elements:
titleRectForContentRect:
imageRectForContentRect:
contentRectForBounds:
backgroundRectForBounds:
These methods are called whenever the button is redrawn, including every time it changes state. The content rect is the area in which the title and image are placed. By default, contentRectForBounds:
and backgroundRectForBounds:
yield the same result. You can override these methods in a subclass to change the way the button’s elements are positioned.
Here’s an example of a customized button. In a UIButton subclass, we increase the button’s intrinsicContentSize
to give it larger margins around its content, and we override backgroundRectForBounds
to shrink the button slightly when highlighted as a way of providing feedback:
- (CGRect)backgroundRectForBounds:(CGRect)bounds { CGRect result = [super backgroundRectForBounds:bounds]; if (self.highlighted) result = CGRectInset(result, 3, 3); return result; } -(CGSize)intrinsicContentSize { CGSize sz = [super intrinsicContentSize]; sz.height += 16; sz.width += 20; return sz; }
The button, which is a UIButtonTypeCustom
button, is assigned an internal image and a background image from the same image, along with an attributed title. When highlighted, its title color changes, thanks to a second attributed string, and its image glows, thanks to adjustsImageWhenHighlighted
(Figure 12-16):
UIImage* im = [UIImage imageNamed: @"coin2.png"]; CGSize sz = [im size]; UIImage* im2 = [im resizableImageWithCapInsets: UIEdgeInsetsMake(sz.height/2.0, sz.width/2.0, sz.height/2.0, sz.width/2.0) resizingMode: UIImageResizingModeStretch]; [self.button setBackgroundImage: im2 forState: UIControlStateNormal]; self.button.backgroundColor = [UIColor clearColor]; [self.button setImage: im2 forState: UIControlStateNormal]; NSMutableAttributedString* mas = [[NSMutableAttributedString alloc] initWithString: @"Pay Tribute" attributes: @{ NSFontAttributeName: [UIFont fontWithName:@"GillSans-Bold" size:16], NSForegroundColorAttributeName: [UIColor purpleColor] }]; [mas addAttributes: @{ NSStrokeColorAttributeName:[UIColor redColor], NSStrokeWidthAttributeName:@(-2.0), NSUnderlineStyleAttributeName:@1} range: NSMakeRange(4,mas.length-4)]; [self.button setAttributedTitle: mas forState: UIControlStateNormal]; mas = [mas mutableCopy]; [mas addAttributes: @{ NSForegroundColorAttributeName:[UIColor whiteColor]} range: NSMakeRange(0,mas.length)]; [self.button setAttributedTitle: mas forState: UIControlStateHighlighted]; self.button.adjustsImageWhenHighlighted = YES;
The UIControl class implements several touch-tracking methods that you might override in order to customize a built-in UIControl type or to create your own UIControl subclass, along with properties that tell you whether touch tracking is going on:
beginTrackingWithTouch:withEvent:
continueTrackingWithTouch:withEvent:
endTrackingWithTouch:withEvent:
cancelTrackingWithEvent:
tracking
(property)
touchInside
(property)
With the advent of gesture recognizers (Chapter 5), such direct involvement with touch tracking is probably less needed than it used to be, especially if your purpose is to modify the behavior of a built-in UIControl subclass. So, to illustrate their use, I’ll give a simple example of creating a custom control. The main reason for doing this (rather than using, say, a UIView and gesture recognizers) would probably be to obtain the convenience of control events. Also, the touch-tracking methods, though not as high-level as gesture recognizers, are at least a level up from the UIResponder touches...
methods (Chapter 5): they track a single touch, and both beginTracking...
and continueTracking...
return a BOOL, giving you a chance to stop tracking the current touch.
We’ll build a simplified knob control (Figure 12-17). The control starts life at its minimum position, with an internal angle value of 0
; it can be rotated clockwise with a single finger as far as its maximum position, with an internal angle value of 5
(radians). To keep things simple, the words “Min” and “Max” appearing in the interface are actually labels; the control just draws the knob, and to rotate it we’ll apply a rotation transform.
Our control is a UIControl subclass, MyKnob. It has a public CGFloat angle
property, and a CGFloat instance variable _initialAngle
that we’ll use internally during rotation. Because a UIControl is a UIView, it can draw itself, which it does with an image file included in our app bundle:
- (void) drawRect:(CGRect)rect { UIImage* knob = [UIImage imageNamed:@"knob.png"]; [knob drawInRect:rect]; }
We’ll need a utility function for transforming a touch’s Cartesian coordinates into polar coordinates, giving us the angle to be applied as a rotation to the view:
static inline CGFloat pToA (UITouch* touch, UIView* self) { CGPoint loc = [touch locationInView: self]; CGPoint c = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); return atan2(loc.y - c.y, loc.x - c.x); }
Now we’re ready to override the tracking methods. beginTrackingWithTouch:withEvent:
simply notes down the angle of the initial touch location. continueTrackingWithTouch:withEvent:
uses the difference between the current touch location’s angle and the initial touch location’s angle to apply a transform to the view, and updates the angle
property. endTrackingWithTouch:withEvent:
triggers the Value Changed control event. So our first draft looks like this:
- (BOOL) beginTrackingWithTouch:(UITouch*)touch withEvent:(UIEvent*)event { self->_initialAngle = pToA(touch, self); return YES; } - (BOOL) continueTrackingWithTouch:(UITouch*)touch withEvent:(UIEvent*)event { CGFloat ang = pToA(touch, self); ang -= self->_initialAngle; CGFloat absoluteAngle = self->_angle + ang; self.transform = CGAffineTransformRotate(self.transform, ang); self->_angle = absoluteAngle; return YES; } - (void) endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event { [self sendActionsForControlEvents:UIControlEventValueChanged]; }
This works: we can put a MyKnob into the interface and hook up its Value Changed control event (this can be done in the nib editor), and sure enough, when we run the app, we can rotate the knob and, when our finger lifts from the knob, the Value Changed action handler is called. However, continueTrackingWithTouch:withEvent:
needs modification.
First, we need to peg the minimum and maximum rotation at 0
and 5
, respectively. For simplicity, we’ll just stop tracking, by returning NO, if the rotation goes below 0
or above 5
, fixing the angle at the exceeded limit. However, because we’re no longer tracking, endTracking...
will never be called, so we also need to trigger the Value Changed control event. (Doubtless you can come up with a more sophisticated way of pegging the knob at its minimum and maximum, but remember, this is only a simple example.) Second, it might be nice to give the programmer the option to have the Value Changed control event reported continuously as continueTracking...
is called repeatedly. So we’ll add a public continuous
BOOL property and obey it.
Here, then, is our revised continueTracking...
implementation:
- (BOOL) continueTrackingWithTouch:(UITouch*)touch withEvent:(UIEvent*)event { CGFloat ang = pToA(touch, self); ang -= self->_initialAngle; CGFloat absoluteAngle = self->_angle + ang; if (absoluteAngle < 0) { self.transform = CGAffineTransformIdentity; self->_angle = 0; [self sendActionsForControlEvents:UIControlEventValueChanged]; return NO; } if (absoluteAngle > 5) { self.transform = CGAffineTransformMakeRotation(5); self->_angle = 5; [self sendActionsForControlEvents:UIControlEventValueChanged]; return NO; } self.transform = CGAffineTransformRotate(self.transform, ang); self->_angle = absoluteAngle; if (self->continuous) [self sendActionsForControlEvents:UIControlEventValueChanged]; return YES; }
Finally, we’ll probably want a method that sets the angle programmatically as a way of rotating the knob:
- (void) setAngle: (CGFloat) ang { if (ang < 0) ang = 0; if (ang > 5) ang = 5; self.transform = CGAffineTransformMakeRotation(ang); self->_angle = ang; }
There are three bar types: navigation bar (UINavigationBar), toolbar (UIToolbar), and tab bar (UITabBar). They are often used in conjunction with a built-in view controller (Chapter 6):
You can also use these bar types independently. You are most likely to do that with a UIToolbar, which is often used as an independent bottom bar. On the iPad, it can also be used as a top bar, adopting a role analogous to a menu bar on the desktop. That’s such a common interface, in fact, that certain special automatic behaviors are associated with it; for example, a UISearchBar in a UIToolbar and managed by a UISearchDisplayController will automatically display its search results table in a popover (Chapter 9).
This section summarizes the facts about the three bar types (along with UISearchBar, which can act as a top bar), and about the items that populate them.
If a navigation bar or toolbar — or a search bar (discussed earlier in this chapter) — is to occupy the top of the screen, the iOS 7 convention is that its height should be increased to underlap the transparent status bar. To make this possible, iOS 7 introduces the notion of a bar position. The UIBarPositioning protocol, adopted by UINavigationBar, UIToolbar, and UISearchbar, defines one property, barPosition
, whose possible values are:
UIBarPositionAny
UIBarPositionBottom
UIBarPositionTop
UIBarPositionTopAttached
But barPosition
is read-only, so how are you supposed to set it? The UIBarPositionDelegate protocol defines one method, positionForBar:
. This provides a way for a bar’s delegate to dictate the bar’s barPosition
. The delegate protocols UINavigationBarDelegate, UIToolbarDelegate, and UISearchBarDelegate all conform to UIBarPositionDelegate.
Thus, if you use one of these bar types alone (not in conjunction with a view controller), you can give it a delegate and, in that delegate, implement positionForBar:
to return the bar’s desired barPosition
value.
The rule is then that the bar’s height will be extended upward, so that its top can go behind the status bar, if the bar’s delegate returns UIBarPositionTopAttached
from its implementation of positionForBar:
. To get the final position right, the bar’s top should also have a zero-length constraint to the view controller’s top layout guide — or, if you’re not using autolayout, then the bar’s top should have a y
value of 20
. Unfortunately, a toolbar in the nib editor has no delegate
outlet — I regard that as a bug — so you’ll have to form an outlet to the toolbar and assign the toolbar a delegate in code. Here’s an example for a toolbar:
- (UIBarPosition)positionForBar: (id<UIBarPositioning>) bar { return UIBarPositionTopAttached; } - (void)viewDidLoad { [super viewDidLoad]; self.toolbar.delegate = self; }
A bar’s height is reflected also by its bar metrics. If a navigation bar or toolbar belongs to a UINavigationController on the iPhone (not on the iPad), then if the interface rotates, the UINavigationController changes the height of the bar. The standard heights are 44
(portrait) and 32
(landscape) — plus 20
if the bar also underlaps the status bar. Possible bar metrics values are:
UIBarMetricsDefault
UIBarMetricsLandscapePhone
UIBarMetricsDefaultPrompt
UIBarMetricsLandscapePhonePrompt
(The two Prompt
values, new in iOS 7, apply to a bar whose height is extended downward to accommodate prompt text.)
When you’re customizing a feature of a bar, you may find yourself calling a method that takes a bar metrics parameter, and possibly a bar position parameter as well. The idea is that you can customize that feature differently depending on the bar metrics and the bar position. You don’t have to set that value for every possible combination of bar position and bar metrics! In general (though, unfortunately, the details are a little inconsistent from class to class), UIBarPositionAny
and UIBarMetricsDefault
are treated as defaults that encompass any positions and metrics you don’t specify.
The interface object classes and their features that participate in this system are:
In iOS 7, the overall look of the three bar types is identical by default, and the ways of customizing that overall look are unified. A bar can be styled at three levels:
barStyle
, translucent
The barStyle
options are:
UIBarStyleDefault
UIBarStyleBlack
In iOS 7 the bar styles are flat white and flat black respectively. The translucent
property turns on or off the blurry translucency that is so characteristic of iOS 7.
barTintColor
tintColor
property which is used for something else in iOS 7 (namely, it is inherited by bar button items to be the default color of their titles and template images). If you set the barTintColor
and you want translucency, then supplying a color with a low alpha
component is up to you. However, if you set the bar’s translucent
to NO, then the barTintColor
is treated as opaque.
backgroundImage
setBackgroundImage:forBarPosition:barMetrics:
). The transparency of the image is obeyed, but if you set the bar’s translucent
to NO, then the barTintColor
will appear opaque behind the image. If the image is too large, it is sized down to fit in iOS 7; if it is too small, it is tiled by default, but you can change that by supplying a resizable image.
If you assign a bar a background image, you can also customize its shadow, which is cast from the bottom of the bar (if the bar is at the top) or the top of the bar (if the bar is at the bottom) on whatever is behind it. The setter is usually the shadowImage
property, but a toolbar can be either at the top or the bottom, so its setter is setShadowImage:forToolbarPosition:
, and the barPosition
is used to decide whether the shadow should appear at the top or the bottom of the toolbar.
You’ll want a shadow image to be very small and very transparent; the image will be tiled horizontally. Here’s an example for a navigation bar:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(4,4), NO, 0); [[[UIColor grayColor] colorWithAlphaComponent:0.3] setFill]; CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0,0,4,2)); [[[UIColor grayColor] colorWithAlphaComponent:0.15] setFill]; CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0,2,4,2)); im = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); self.navbar.shadowImage = im;
The only things that can appear inside a navigation bar or a toolbar — aside from a navigation bar’s title and prompt — are bar button items (UIBarButtonItem, a subclass of UIBarItem). This is not much of a limitation, however, because a bar button item can contain a custom view, which can be any type of UIView at all. A bar button item itself, however, is not a UIView subclass.
A bar button item may be instantiated with any of five methods:
initWithBarButtonSystemItem:target:action:
initWithTitle:style:target:action:
initWithImage:style:target:action:
initWithImage:landscapeImagePhone:style:target:action:
initWithCustomView:
In iOS 7, a bar button item’s image is treated by default as a template image, unless you explicitly provide a UIImageRenderingModeAlwaysOriginal
image.
The style:
options are:
UIBarButtonItemStylePlain
UIBarButtonItemStyleDone
(in iOS 7, the title text is bold)
As I mentioned a moment ago, many aspects of a bar button item can be made dependent upon the bar metrics of the containing bar. Thus, you can initialize a bar button item with both an image
and a landscapeImagePhone
, the latter to be used when the bar metrics has landscape
in its name. A bar button item inherits from UIBarItem the ability to adjust the image position with imageInsets
(and landscapeImagePhoneInsets
), plus the enabled
and tag
properties. Recall from Chapter 6 that you can also set a bar button item’s possibleTitles
and width
properties, to determine its width.
A bar button item’s tintColor
property, in iOS 7, tints the title text or template image of the button; it is inherited from the tintColor
of the bar, or you can override it for an individual bar button item.
You can apply a text attributes dictionary to a bar button item’s title, and you can give it a background image:
setTitleTextAttributes:forState:
(inherited from UIBarItem)
setTitlePositionAdjustment:forBarMetrics:
setBackgroundImage:forState:barMetrics:
setBackgroundImage:forState:style:barMetrics:
setBackgroundVerticalPositionAdjustment:forBarMetrics:
In addition, these methods apply only if the bar button item is being used as a back button item in a navigation bar (as I’ll describe in the next section):
setBackButtonTitlePositionAdjustment:forBarMetrics:
setBackButtonBackgroundImage:forState:barMetrics:
setBackButtonBackgroundVerticalPositionAdjustment:forBarMetrics:
In a bar button item with a custom view, the background vertical position adjustment doesn’t apply (because the custom view is the button’s content, not its background). To shift the apparent position of a custom view, construct the custom view as a subview within a superview, and shift the position of the subview. This technique can cause the subview to appear outside the containing bar, so be careful.
In iOS 7, no bar button item style supplies an outline (border); the default look of a button is just the text or image. (The old bar button item style UIBarButtonItemStyleBordered
, in iOS 7, is identical to UIBarButtonItemStylePlain
.) If you want an outline, you have to supply it yourself by way of the background image. Here’s how I create the outlined left bar button item in the settings view of my Zotz! app (Figure 12-18):
UIBarButtonItem* bb = [[UIBarButtonItem alloc] initWithTitle:@"New Game" style:UIBarButtonItemStylePlain target:self action:@selector(doNewGame:)]; [bb setTitleTextAttributes: @{NSFontAttributeName:[UIFont fontWithName:@"Avenir-Medium" size:11]} forState:UIControlStateNormal]; CAGradientLayer* grad = [CAGradientLayer new]; grad.frame = CGRectMake(0,0,15,15); grad.colors = @[(id)[UIColor colorWithRed:1 green:1 blue:0 alpha:0.8].CGColor, (id)[UIColor colorWithRed:.7 green:.7 blue:.3 alpha:0.8].CGColor]; UIGraphicsBeginImageContextWithOptions(CGSizeMake(15,15), NO, 0); UIBezierPath* p = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0,0,15,15) cornerRadius:8]; [p addClip]; [grad renderInContext:UIGraphicsGetCurrentContext()]; [[UIColor blackColor] setStroke]; p.lineWidth = 2; [p stroke]; UIImage* im = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); im = [im resizableImageWithCapInsets:UIEdgeInsetsMake(7,7,7,7) resizingMode:UIImageResizingModeStretch]; [bb setBackgroundImage:im forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
A navigation bar (UINavigationBar) is populated by navigation items (UINavigationItem). The UINavigationBar maintains a stack; UINavigationItems are pushed onto and popped off of this stack. Whatever UINavigationItem is currently topmost in the stack (the UINavigationBar’s topItem
), in combination with the UINavigationItem just beneath it in the stack (the UINavigationBar’s backItem
), determines what appears in the navigation bar:
title
, titleView
title
(string) or titleView
(UIView) of the topItem
appears in the center of the navigation bar.
prompt
prompt
(string) of the topItem
appears at the top of the navigation bar, whose height increases to accommodate it.
rightBarButtonItem
, leftBarButtonItem
rightBarButtonItem
and leftBarButtonItem
appear at the right and left ends of the navigation bar. A UINavigationItem can have multiple right bar button items and multiple left bar button items; its rightBarButtonItems
and leftBarButtonItems
properties are arrays (of bar button items). The bar button items are displayed from the outside in: that is, the first item in the leftBarButtonItems
is leftmost, while the first item in the rightBarButtonItems
is rightmost. If there are multiple buttons on a side, the rightBarButtonItem
is the first item of the rightBarButtonItems
array, and the leftBarButtonItem
is the first item of the leftBarButtonItems
array.
backBarButtonItem
backBarButtonItem
of the backItem
appears at the left end of the navigation bar. It is automatically configured so that, when tapped, the topItem
is popped off the stack. If the backItem
has no backBarButtonItem
, then there is still a back button at the left end of the navigation bar, taking its title from the title
of the backItem
. However, if the topItem
has its hidesBackButton
set to YES, the back button is suppressed. Also, unless the topItem
has its leftItemsSupplementBackButton
set to YES, the back button is suppressed if the topItem
has a leftBarButtonItem
.
In iOS 7, the indication that the back button is a back button is supplied by the navigation bar’s backIndicatorImage
, which by default is a left-pointing chevron appearing to the left of the back button. You can customize this image; the image that you supply is treated as a template image by default. If you set the backIndicatorImage
, you must also supply a backIndicatorTransitionMaskImage
. The purpose of the mask image is to indicate the region where the back button should disappear as it slides out to the left when a new navigation item is pushed onto the stack. For example, in Figure 12-19, the back button title is visible to the right of the chevron but not to the left of the chevron; that’s because on the left side of the chevron it is masked out (the mask is transparent).
In this example, I replace the chevron with a vertical bar. The vertical bar is not the entire image; the image is actually a wider rectangle, with the vertical bar at its right side. The mask is the entire wider rectangle, and is completely transparent; thus, the back button disappears as it passes behind the bar and stays invisible as it continues on to the left:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(10,20), NO, 0); CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(6,0,4,20)); UIImage* im = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); self.navbar.backIndicatorImage = im; UIGraphicsBeginImageContextWithOptions(CGSizeMake(10,20), NO, 0); UIImage* im2 = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); self.navbar.backIndicatorTransitionMaskImage = im2;
Changes to the navigation bar’s buttons can be animated by sending its topItem
any of these messages:
setRightBarButtonItem:animated:
setLeftBarButtonItem:animated:
setRightBarButtonItems:animated:
setLeftBarButtonItems:animated:
setHidesBackButton:animated:
UINavigationItems are pushed and popped with pushNavigationItem:animated:
and popNavigationItemAnimated:
, or you can set all items on the stack at once with setItems:animated:
.
You can set the title’s attributes dictionary (titleTextAttributes
), and you can shift the title’s vertical position by calling setTitleVerticalPositionAdjustment:forBarMetrics:
.
When you use a UINavigationBar implicitly as part of a UINavigationController interface, the navigation controller is the navigation bar’s delegate. If you were to use a UINavigationBar on its own, you might want to supply your own delegate. The delegate methods are:
navigationBar:shouldPushItem:
navigationBar:didPushItem:
navigationBar:shouldPopItem:
navigationBar:didPopItem:
This simple (and silly) example of a standalone UINavigationBar implements the legendary baseball combination trio of Tinker to Evers to Chance; see the relevant Wikipedia article if you don’t know about them (Figure 12-20, which also shows the custom back indicator and shadow I described earlier):
- (void)viewDidLoad { [super viewDidLoad]; UINavigationItem* ni = [[UINavigationItem alloc] initWithTitle:@"Tinker"]; UIBarButtonItem* b = [[UIBarButtonItem alloc] initWithTitle:@"Evers" style:UIBarButtonItemStylePlain target:self action:@selector(pushNext:)]; ni.rightBarButtonItem = b; self.navbar.items = @[ni]; } - (void) pushNext: (id) sender { UIBarButtonItem* oldb = sender; NSString* s = oldb.title; UINavigationItem* ni = [[UINavigationItem alloc] initWithTitle:s]; if ([s isEqualToString: @"Evers"]) { UIBarButtonItem* b = [[UIBarButtonItem alloc] initWithTitle:@"Chance" style:UIBarButtonItemStylePlain target:self action:@selector(pushNext:)]; ni.rightBarButtonItem = b; } [self.navbar pushNavigationItem:ni animated:YES]; }
A toolbar (UIToolbar, Figure 12-21) is intended to appear at the bottom of the screen; on the iPad, it may appear at the top of the screen. It displays a row of UIBarButtonItems, which are its items
. The items are displayed from left to right in the order in which they appear in the items
array. You can set the items with animation by calling setItems:animated:
. The items within the toolbar are positioned automatically; you can intervene in this positioning by using the system bar button items UIBarButtonSystemItemFlexibleSpace
and UIBarButtonSystemItemFixedSpace
, along with the UIBarButtonItem width
property.
A tab bar (UITabBar) displays tab bar items (UITabBarItem), its items
, each consisting of an image and a name. To change the items in an animated fashion, call setItems:animated:
. The tab bar maintains a current selection among its items, its selectedItem
, which is a UITabBarItem, not an index number. If no item is selected initially, you can set the initial selection in code:
self.tabbar.selectedItem = self.tabbar.items[0];
To hear about the user changing the selection, implement tabBar:didSelectItem:
in the delegate (UITabBarDelegate).
New in iOS 7, you get some control over how the tab bar items are laid out:
itemPositioning
There are three possible values:
UITabBarItemPositioningCentered
UITabBarItemPositioningFill
UITabBarItemPositioningAutomatic
Centered
; on the iPhone, the same as Fill
.
itemSpacing
Centered
. For the default space, specify 0
.
itemWidth
Centered
. For the default width, specify 0
.
You can set the image drawn behind the selected tab bar item to indicate that it’s selected, the selectionIndicatorImage
.
A UITabBarItem is created with one of these two methods:
initWithTabBarSystemItem:tag:
initWithTitle:image:tag:
UITabBarItem is a subclass of UIBarItem, so in addition to its title
and image
it inherits the ability to adjust the image position with imageInsets
, plus the enabled
and tag
properties.
In iOS 7, a bar item title text and template image are tinted, by default, with the tab bar’s tintColor
when selected (this is a change from iOS 6 behavior); the tab bar’s selectedImageTintColor
is ignored, and there’s no way to set the deselected tint color (I regard this as a bug).
In iOS 7, a tab bar item’s image is treated as a template image, but you can override that by supplying a UIImageRenderingModeAlwaysOriginal
image. For this reason, the method introduced in iOS 6 for specifying a nontemplate image (setFinishedSelectedImage:withFinishedUnselectedImage:
) is unnecessary and has been deprecated. Similarly, you can customize a tab bar item’s title (including its color) with an attributes dictionary (setTitleTextAttributes:forState:
, inherited from UIBarItem), and you can adjust the title’s position with the titlePositionAdjustment
property.
Figure 12-22 (from the first screen shown in Figure 6-9) is an example of a customized tab bar; I’ve set the selection indicator image (the checkmark), the tint color, and the text attributes (including the color, when selected) of the tab bar items.
The user can be permitted to alter the contents of the tab bar, setting its tab bar items from among a larger repertory of tab bar items. To summon the interface that lets the user do this, call beginCustomizingItems:
, passing an array of UITabBarItems that may or may not appear in the tab bar. (To prevent the user from removing an item from the tab bar, include it in the tab bar’s items
and don’t include it in the argument passed to beginCustomizingItems:
.) A presented view with a Done button appears, behind the tab bar but in front of everything else, displaying the customizable items. The user can then drag an item into the tab bar, replacing an item that’s already there. To hear about the customizing view appearing and disappearing, implement delegate methods:
tabBar:willBeginCustomizingItems:
tabBar:didBeginCustomizingItems:
tabBar:willEndCustomizingItems:changed:
tabBar:didEndCustomizingItems:changed:
A UITabBar on its own (outside a UITabBarController) does not provide any automatic access to the user customization interface; it’s up to you. In this (silly) example, we populate a UITabBar with four system tab bar items and a More item; we also populate an instance variable array with those same four system tab bar items, plus four more. When the user taps the More item, we show the user customization interface with all eight tab bar items:
- (void)viewDidLoad { [super viewDidLoad]; NSMutableArray* arr = [NSMutableArray new]; for (int ix = 1; ix < 8; ix++) { UITabBarItem* tbi = [[UITabBarItem alloc] initWithTabBarSystemItem:ix tag:ix]; [arr addObject: tbi]; } self.items = arr; // copy policy [arr removeAllObjects]; [arr addObjectsFromArray: [self.items subarrayWithRange:NSMakeRange(0,4)]]; UITabBarItem* tbi = [[UITabBarItem alloc] initWithTabBarSystemItem:0 tag:0]; [arr addObject: tbi]; // More button self.tabbar.items = arr; self.tabbar.selectedItem = self.tabbar.items[0]; } - (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item { NSLog(@"did select item with tag %d", item.tag); if (item.tag == 0) { // More button tabBar.selectedItem = nil; [tabBar beginCustomizingItems:self.items]; } } -(void)tabBar:(UITabBar *)tabBar didEndCustomizingItems:(NSArray *)items changed:(BOOL)changed { self.tabbar.selectedItem = self.tabbar.items[0]; }
When used in conjunction with a UITabBarController, the customization interface is provided automatically, in an elaborate way. If there are a lot of items, a More item is automatically present, and can be used to access the remaining items in a table view. Here, the user can select any of the excess items, navigating to the corresponding view. Or, the user can switch to the customization interface by tapping the Edit button. (See the iPhone Music app for a familiar example.) Figure 12-23 shows how a More list looks by default.
The way this works is that the automatically provided More item corresponds to a UINavigationController with a root view controller (UIViewController) whose view
is a UITableView. Thus, a navigation interface containing this UITableView appears through the tabbed interface when the user taps the More button. When the user selects an item in the table, the corresponding UIViewController is pushed onto the UINavigationController’s stack.
You can access this UINavigationController: it is the UITabBarController’s moreNavigationController
. Through it, you can access the root view controller: it is the first item in the UINavigationController’s viewControllers
array. And through that, you can access the table view: it is the root view controller’s view
. This means you can customize what appears when the user taps the More button! For example, let’s make the navigation bar black with white button titles, and let’s remove the word More from its title:
UINavigationController* more = self.tabBarController.moreNavigationController; UIViewController* list = more.viewControllers[0]; list.title = @""; UIBarButtonItem* b = [UIBarButtonItem new]; b.title = @"Back"; list.navigationItem.backBarButtonItem = b; // so user can navigate back more.navigationBar.barStyle = UIBarStyleBlack; more.navigationBar.tintColor = [UIColor whiteColor];
We can go even further by supplementing the table view’s data source with a data source of our own, thus proceeding to customize the table itself. This is tricky because we have no internal access to the actual data source, and we mustn’t accidentally disable it from populating the table. Still, it can be done. I’ll start by replacing the table view’s data source with an instance of my own MyDataSource. It has a public instance variable called originalDataSource
; in it, I’ll store a reference to the original data source object:
UITableView* tv = (UITableView*)list.view; MyDataSource* mds = [MyDataSource new]; self.myDataSource = mds; // retain policy self.myDataSource.originalDataSource = tv.dataSource; tv.dataSource = self.myDataSource;
In MyDataSource, I’ll use Objective-C’s automatic message forwarding mechanism (see Apple’s Objective-C Runtime Programming Guide) so that MyDataSource acts as a front end for originalDataSource
. MyDataSource will magically appear to respond to any message that originalDataSource
responds to, and any message that arrives that MyDataSource can’t handle will be magically forwarded to originalDataSource
. This way, the insertion of the MyDataSource instance as data source doesn’t break whatever the original data source does:
- (id)forwardingTargetForSelector:(SEL)aSelector { if ([self.originalDataSource respondsToSelector: aSelector]) return self.originalDataSource; return [super forwardingTargetForSelector:aSelector]; }
Finally, we’ll implement the two Big Questions required by the UITableViewDataSource protocol, to quiet the compiler. In both cases, we first pass the message along to originalDataSource
(somewhat analogous to calling super
); then we add our own customizations as desired. Here, just as a proof of concept, I’ll change each cell’s text font (Figure 12-24):
- (NSInteger)tableView:(UITableView *)tv numberOfRowsInSection:(NSInteger)sec { // this is just to quiet the compiler return [self.originalDataSource tableView:tv numberOfRowsInSection:sec]; } - (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)ip { UITableViewCell* cell = [self.originalDataSource tableView:tv cellForRowAtIndexPath:ip]; cell.textLabel.font = [UIFont fontWithName:@"GillSans-Bold" size:14]; return cell; }
New in iOS 7, tintColor
is a UIView property, and it has a remarkable built-in feature: its value, if not set explicitly (or if set to nil), is inherited from its superview. The idea is to simplify the task of giving your app a consistent overall appearance.
This works exactly the way you would expect. You can set the tintColor
of your UIWindow instance, and its value will be inherited by every view that ever appears. Any built-in view (or view-like interface object, such as a UIBarItem) whose details are colored by the tintColor
will display this same color. For example, if you set your window’s tintColor
to red, then every UIButtonTypeSystem
button that appears anywhere in your interface will have red title text by default.
Moreover, the inherited tintColor
can be overridden by setting a view’s tintColor
explicitly. In other words, you can set the tintColor
of a view partway down the view hierarchy so that it and all its subviews have a different tintColor
from the rest of the interface. In this way, you might subtly suggest that the user has entered a different world.
If you change the tintColor
of a view, the change immediately propagates down the hierarchy of its subviews — except, of course, that a view whose tintColor
has been explicitly set to a color of its own is unaffected, along with its subviews.
When you ask a view for its tintColor
, what you get is the tintColor
of the view itself, if its own tintColor
has been explicitly set to a color, or else the tintColor
inherited from up the view hierarchy. In this way, you can always learn what the effective tint color of a view is.
Whenever a view’s tintColor
changes, including when its tintColor
is initially set at launch time, it and all its affected subviews are sent the tintColorDidChange
message. A subview whose tintColor
has previously been explicitly set to a color of its own isn’t affected, so it is not sent the tintColorDidChange
message merely because its superview’s tintColor
changes — the subview’s own tintColor
didn’t change.
A UIView also has a tintAdjustmentMode
. Under certain circumstances, such as the summoning of a UIAlertView (Chapter 13) or a popover (Chapter 9), the system will set the tintAdjustmentMode
of the view at the top of the view hierarchy to UIViewTintAdjustmentModeDimmed
. This causes the tintColor
to change to a variety of gray. The idea is that the tinting of the background should become monochrome, thus emphasizing the primacy of the view that occupies the foreground (the alert view or popover). This change in the tintAdjustmentMode
propagates all the way down the view hierarchy, changing all tintAdjustmentMode
values and all tintColor
values — and sending all subviews the tintColorDidChange
message. When the foreground view goes away, the system will set the topmost view’s tintAdjustmentMode
to UIViewTintAdjustmentModeNormal
, and that change will propagate down the hierarchy.
The default tintAdjustmentMode
value is UIViewTintAdjustmentModeAutomatic
, meaning that you want this view’s tintAdjustmentMode
to adopt its superview’s tintAdjustmentMode
automatically. When you ask for such a view’s tintAdjustmentMode
, what you get is just like what you get for tintColor
— you’re told the effective tint adjustment mode (UIViewTintAdjustmentModeNormal
or UIViewTintAdjustmentModeDimmed
) inherited from up the view hierarchy.
If, on the other hand, you set a view’s tintAdjustmentMode
explicitly to UIViewTintAdjustmentModeNormal
or UIViewTintAdjustmentModeDimmed
, this tells the system that you want to be left in charge of the tintAdjustmentMode
for this part of the hierarchy; the automatic propagation of the tintAdjustmentMode
down the view hierarchy is prevented. To turn automatic propagation back on, set the tintAdjustmentMode
back to UIViewTintAdjustmentModeAutomatic
. (See Custom Presented View Controller Transition for an example of setting the interface’s tintAdjustmentMode
to UIViewTintAdjustmentModeDimmed
in imitation of an alert view.)
You can take advantage of tintColorDidChange
to make your custom UIView subclass behave like a built-in UIView subclass. For example, if you subclass UIButton, your subclass might not automatically dim the title text color; your button’s tintColor
is being dimmed, but that color isn’t being applied to the visible interface. You might be able to correct that problem just by calling super
:
-(void)tintColorDidChange { [super tintColorDidChange]; }
In a more elaborate case, you might have to apply the tintColor
explicitly to some aspect of your view subclass’s interface. In this example, I apply the tintColor
to my button subclass’s attributed string title:
-(void)tintColorDidChange { NSMutableAttributedString* mas = [[self attributedTitleForState:UIControlStateNormal] mutableCopy]; [mas addAttribute:NSForegroundColorAttributeName value:self.tintColor range:NSMakeRange(0,mas.length)]; [self setAttributedTitle:mas forState:UIControlStateNormal]; }
Don’t set the tintColor
from within tintColorDidChange
without taking precautions against an infinite recursion.
Instead of sending messages that customize the look of an interface object to the object itself, you can send them to an appearance proxy for that object’s class. The appearance proxy then passes that same message along to the actual future instances of that class. You’ll usually configure your appearance proxies very early in the lifetime of the app, and never again. The app delegate’s application:didFinishLaunchingWithOptions:
, before the app’s window has been displayed, is the most obvious and common location.
Thus, for example, instead of sending setTitleTextAttributes:forState:
to a particular UIBarButtonItem, you could send it to a UIBarButtonItem appearance proxy. All actual UIBarButtonItems from then on would have the text attributes you specified.
Like the tintColor
that I discussed in the previous section, this architecture helps you give your app a consistent appearance, as well as saving you from having to write a lot of code; instead of having to send setTitleTextAttributes:forState:
to every UIBarButtonItem your app ever instantiates, you send it once to the appearance proxy, and it is sent to all future UIBarButtonItems for you.
Also, the appearance proxy sometimes provides access to interface objects that might otherwise be difficult to refer to. For example, you don’t get direct access to a search bar’s external Cancel button, but it is a UIBarButtonItem and you can customize it through the UIBarButtonItem appearance proxy.
There are two class methods for obtaining an appearance proxy:
appearance
appearanceWhenContainedIn:
When configuring appearance proxy objects, specificity trumps generality. Thus, you could call appearance
to say what should happen for most instances of some class, and call appearanceWhenContainedIn:
to say what should happen instead for certain instances of that class. Similarly, longer appearanceWhenContainedIn:
chains are more specific than shorter ones.
For example, here’s some code from my Latin flashcard app (myGolden
and myPaler
are methods defined by a category on UIColor):
[[UIBarButtonItem appearance] setTintColor: [UIColor myGolden]];[[UIBarButtonItem appearanceWhenContainedIn: [UIToolbar class], nil] setTintColor: [UIColor myPaler]];
[[UIBarButtonItem appearanceWhenContainedIn: [UIToolbar class], [DrillViewController class], nil] setTintColor: [UIColor myGolden]];
That means:
(If you’re looking at this book’s figures in color, you can see this difference made manifest in Figure 6-3 and Figure 6-5.)
Sometimes, in order to express sufficient specificity, I find myself defining subclasses for no other purpose than to refer to them when obtaining an appearance proxy. For example, here’s some more code from my Latin flashcard app:
[[UINavigationBar appearance] setBackgroundImage:marble2 forBarMetrics:UIBarMetricsDefault]; // counteract the above for the black navigation bar [[BlackNavigationBar appearance] setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault];
In that code, BlackNavigationBar is a UINavigationBar subclass that does nothing whatever. Its sole purpose is to tag one navigation bar in my interface so that I can refer to it in that code! Thus, I’m able to say, in effect, “All navigation bars in this app should have marble2
as their background image, unless they are instances of BlackNavigationBar.”
The ultimate in specificity is, of course, to customize the look of an instance directly. Thus, for example, if you set one particular UIBarButtonItem’s tintColor
property, then setTintColor:
sent to a UIBarButtonItem appearance proxy will have no effect on that particular bar button item.
The appearance proxy object returned by appearance
or appearanceWhenContainedIn
is, in reality, an instance of some hidden class that performs some deep trickery called method swizzling. The API has to cover this fact somehow, so this object was typed as an id
in earlier systems. In iOS 7, however, it’s an instancetype
. This means that, regardless of its real class, the compiler sees it as equivalent to an instance of the class to which the message was sent. This is extremely convenient, for several reasons:
The compiler won’t let you send the appearance proxy a message that you can’t send to an instance of that class. This won’t compile:
[[UINavigationBar appearance] setWidth: 7];
You can use dot-notation with properties. Previously, you had to say this:
[[UINavigationBar appearance] setBarTintColor:[UIColor redColor]];
But in iOS 7, you can say this:
[UINavigationBar appearance].barTintColor = [UIColor redColor];
However, just because your code will compile doesn’t mean it’s legal. Not every message that can be sent to an instance of a class can be sent to that class’s appearance proxy. For example:
[UINavigationBar appearance].translucent = NO;
That code is legal in the sense that it compiles, but we crash when it is encountered (“Illegal type for appearance setter”).
So how are you supposed to know which messages can be sent to the appearance proxies for which classes? One way is to try it and see whether you crash! Another way is to look in the header for that class (or a superclass); an appropriate property or method should be tagged UI_APPEARANCE_SELECTOR
. For example, here’s how the barTintColor
property is declared in UINavigationBar.h:
@property(nonatomic,retain) UIColor *barTintColor NS_AVAILABLE_IOS(7_0) UI_APPEARANCE_SELECTOR;
Finally, you can look in the class documentation; there should be a subsection of the Tasks section that lists the properties and methods applicable to the appearance proxy for this class. For example, the UINavigationBar class documentation has a section called “Customizing the Bar Appearance”, the UIBarButtonItem class documentation has a section called “Customizing Appearance”, and so forth.
In iOS 7, tintColor
is a legal appearance proxy message. I stress this because the headers do not make this fact clear (it is not tagged with UI_APPEARANCE_SELECTOR
). Moreover, early in the iOS 7 beta process it wasn’t a legal appearance proxy message, and you may encounter claims, on the Internet and even in the documentation, to that effect. Ignore them.