Chapter 12. Controls and Other Views

This chapter discusses all UIView subclasses provided by UIKit that haven’t been discussed already. It’s remarkable how few of them there are; UIKit exhibits a notable 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 examples in Part III.

UIActivityIndicatorView

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 true (the default), it is visible only while spinning.

An activity indicator comes in a style; if it is created in code, you’ll set its style with init(style:). Your choices (UIActivityIndicatorView.Style) are:

  • .whiteLarge

  • .white

  • .gray

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 of the spokes assigned through the style. An activity indicator is a UIView, so you can also 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).

pios 2501
Figure 12-1. A large activity indicator

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:

override func setSelected(_ selected: Bool, animated: Bool) {
    if selected {
        let v = UIActivityIndicatorView(style:.whiteLarge)
        v.color = .yellow
        DispatchQueue.main.async {
            v.backgroundColor = UIColor(white:0.2, alpha:0.6)
        }
        v.layer.cornerRadius = 10
        v.frame = v.frame.insetBy(dx: -10, dy: -10)
        v.center = self.contentView.convert(self.bounds.center, from: self)
        v.tag = 1001
        self.contentView.addSubview(v)
        v.startAnimating()
    } else {
        if let v = self.viewWithTag(1001) {
            v.removeFromSuperview()
        }
    }
    super.setSelected(selected, animated: animated)
}

If activity involves the network, you might want to set the UIApplication’s isNetworkActivityIndicatorVisible to true. 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 false 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. Another solution is a CAReplicatorLayer, a layer that makes multiple copies of its sublayer; by animating the sublayer, you animate the copies. This is a very common approach (in fact, it wouldn’t surprise me to learn that UIActivityIndicatorView is implemented using CAReplicatorLayer). For example:

let lay = CAReplicatorLayer()
lay.frame = CGRect(0,0,100,20)
let bar = CALayer()
bar.frame = CGRect(0,0,10,20)
bar.backgroundColor = UIColor.red.cgColor
lay.addSublayer(bar)
lay.instanceCount = 5
lay.instanceTransform = CATransform3DMakeTranslation(20, 0, 0)
let anim = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
anim.fromValue = 1.0
anim.toValue = 0.2
anim.duration = 1
anim.repeatCount = .greatestFiniteMagnitude
bar.add(anim, forKey: nil)
lay.instanceDelay = anim.duration / Double(lay.instanceCount)
self.view.layer.addSublayer(lay)
lay.position = CGPoint(
    self.view.layer.bounds.midX, self.view.layer.bounds.midY)

Our single red vertical bar (bar) is replicated to make five red vertical bars. We repeatedly fade the bar from opaque to transparent, but because we’ve set the replicator layer’s instanceDelay, the replicated bars fade in sequence, so that the darkest bar appears to be marching repeatedly to the right (Figure 12-2).

pios 2501b
Figure 12-2. A custom activity indicator

UIProgressView

A progress view (UIProgressView) is a “thermometer” graphically displaying a percentage. This may be a static percentage, or it might 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). In one of my apps I use a progress view to show how many cards are left in the deck; in another app I use a progress view to show the current position within the song being played by the built-in music player.

A progress view comes in a style, its progressViewStyle; if the progress view is created in code, you’ll set its style with init(progressViewStyle:). Your choices (UIProgressView.Style) are:

  • .default

  • .bar

A .bar progress view is intended for use in a UIBarButtonItem, as the title view of a navigation item, and so on. Both styles draw the thermometer extremely thin — just 2 pixels and 3 pixels, respectively. (Figure 12-3 shows a .default 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). Alternatively, subclass UIProgressView and override sizeThatFits(_:).

pios 2502
Figure 12-3. A progress view

The fullness of the thermometer is the progress view’s progress property. This is a value between 0 and 1, inclusive; you’ll usually do some elementary arithmetic to convert from the actual value you’re reflecting to a value within that range. (It is also a Float; in Swift, you may have to coerce explicitly.) A change in progress value can be animated by calling setProgress(_:animated:). For example, to reflect the number of cards remaining in a deck of 52 cards:

let r = self.deck.cards.count
self.prog.setProgress(Float(r)/52, animated: true)

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 .default progress view and transparent for a .bar progress view. You can customize the colors; set the progress view’s progressTintColor and trackTintColor, respectively. This can also be done in the nib editor.

Alternatively, you can customize the image used to draw the filled portion of the progress view, its progressImage, along with the image used to draw the unfilled portion, the trackImage. This can also be done in the nib editor. 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-4):

pios 2502b
Figure 12-4. A thicker progress view using a custom progress image
self.prog.trackTintColor = .black
let r = UIGraphicsImageRenderer(size:CGSize(10,10))
let im = r.image { ctx in
    let con = ctx.cgContext
    con.setFillColor(UIColor.yellow.cgColor)
    con.fill(CGRect(0, 0, 10, 10))
    let r = con.boundingBoxOfClipPath.insetBy(dx: 1,dy: 1)
    con.setLineWidth(2)
    con.setStrokeColor(UIColor.black.cgColor)
    con.stroke(r)
    con.strokeEllipse(in: r)
}.resizableImage(
    withCapInsets:UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4),
    resizingMode:.stretch)
self.prog.progressImage = im

Progress View Alternatives

For maximum flexibility, you can design your own UIView subclass that draws something similar to a thermometer. Figure 12-5 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 draw(_:) code:

override func draw(_ rect: CGRect) {
    let c = UIGraphicsGetCurrentContext()!
    UIColor.white.set()
    let ins : CGFloat = 2
    let r = self.bounds.insetBy(dx: ins, dy: ins)
    let radius : CGFloat = r.size.height / 2
    let d90 = CGFloat.pi/2
    let path = CGMutablePath()
    path.move(to:CGPoint(r.maxX - radius, ins))
    path.addArc(center:CGPoint(radius+ins, radius+ins),
        radius: radius, startAngle: -d90, endAngle: d90, clockwise: true)
    path.addArc(center:CGPoint(r.maxX - radius, radius+ins),
        radius: radius, startAngle: d90, endAngle: -d90, clockwise: true)
    path.closeSubpath()
    c.addPath(path)
    c.setLineWidth(2)
    c.strokePath()
    c.addPath(path)
    c.clip()
    c.fill(CGRect(r.origin.x, r.origin.y, r.width * self.value, r.height))
}
pios 2503
Figure 12-5. A custom progress view

Your custom progress view doesn’t have to look like a thermometer. A common interface (as in Apple’s App Store app during a download) is to draw the arc of a circle. This effect is easily achieved by setting the strokeEnd of a CAShapeLayer with a circular path. Here’s a UIButton subclass that implements it (Figure 12-6):

pios 2503b
Figure 12-6. A circular custom progress view
class MyCircularProgressButton : UIButton {
    var progress : Float = 0 {
        didSet {
            if let layer = self.shapelayer {
                layer.strokeEnd = CGFloat(self.progress)
            }
        }
    }
    private var shapelayer : CAShapeLayer!
    private var didLayout = false
    override func layoutSubviews() {
        super.layoutSubviews()
        guard !self.didLayout else {return}
        self.didLayout = true
        let layer = CAShapeLayer()
        layer.frame = self.bounds
        layer.lineWidth = 2
        layer.fillColor = nil
        layer.strokeColor = UIColor.red.cgColor
        let b = UIBezierPath(ovalIn: self.bounds.insetBy(dx: 3, dy: 3))
        b.apply(CGAffineTransform(
            translationX: -self.bounds.width/2, y: -self.bounds.height/2))
        b.apply(CGAffineTransform(
            rotationAngle: -.pi/2.0))
        b.apply(CGAffineTransform(
            translationX: self.bounds.width/2, y: self.bounds.height/2))
        layer.path = b.cgPath
        self.layer.addSublayer(layer)
        layer.zPosition = -1
        layer.strokeStart = 0
        layer.strokeEnd = 0
        self.shapelayer = layer
    }
}

The Progress Class

A progress view has an observedProgress property which you can set to a Progress object. Progress is a Foundation class that abstracts the notion of task progress: it has a totalUnitCount property and a completedUnitCount property, and their ratio generates its fractionCompleted, which is read-only and observable with KVO.

If you assign a Progress object to a progress view’s observedProgress property and configure and update it, the progress view will automatically use the changes in the Progress object’s fractionCompleted to update its own progress. That’s useful because you might already have a time-consuming process that maintains and vends its own Progress object. (For a case in point, see “Slow Data Delivery”.)

How should your progress view’s observedProgress be related to the Progress object vended by the time-consuming process? There are two possibilities:

  • In simple cases, you might assign the process’s Progress object directly to your progress view’s observedProgress.

  • Alternatively, you can configure your progress view’s observedProgress as the parent of the process’s Progress object.

When Progress objects stand in a parent–child relationship, the progress of an operation reported to the child automatically forms an appropriate fraction of the progress reported by the parent; this allows a single Progress object, acting as the ultimate parent, to conglomerate the progress of numerous individual operations. There are two ways to put two Progress objects into a parent–child relationship:

Explicit parent

Call the parent’s addChild(_:withPendingUnitCount:) method. Alternatively, create the child by initializing it with reference to the parent, by calling init(totalUnitCount:parent:pendingUnitCount:).

Implicit parent

This approach uses the notion of the current Progress object. The rule is that while a Progress object is current, any new Progress objects will become its child automatically. The whole procedure thus comes down to doing things in the right order:

  1. Tell the prospective parent Progress object to becomeCurrent(withPendingUnitCount:).

  2. Create the child Progress object without an explicit parent, by calling init(totalUnitCount:). As if by magic, it becomes the other Progress object’s child (because the other Progress object is current).

  3. Tell the parent to resignCurrent. This balances the earlier becomeCurrent(withPendingUnitCount:) and completes the configuration.

UIPickerView

A picker view (UIPickerView) displays selectable choices using a rotating drum metaphor. Its default height is adaptive — 162 in an environment with a .compact vertical size class (an iPhone in landscape orientation) and 216 otherwise — but you are free to set its height to something else. Its width is generally 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 some Big Questions similar to those posed by a UITableView (Chapter 8):

numberOfComponents(in:)

How many components (drums) does this picker view have?

pickerView(_:numberOfRowsInComponent:)

How many rows does this component have? The first component is numbered 0.

pickerView(_:titleForRow:forComponent:)
pickerView(_:attributedTitleForRow:forComponent:)
pickerView(_:viewForRow:forComponent:reusing:)

What should this row of this component display? The first row is numbered 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.

Warning

In pickerView(_:viewForRow:forComponent:reusing:), the reusing: parameter, if not nil, is supposed to be 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. In actual fact, the reusing: parameter is always nil. Views don’t leak — they go out of existence in good order when they are no longer visible — but they aren’t reused. I regard this as a bug.

Here’s the code for a UIPickerView (Figure 12-7) that displays the names of the 50 U.S. states, stored in an array (self.states). We implement pickerView(_:viewForRow:forComponent:reusing:) just because it’s the most interesting case; as our views, we supply UILabel instances. The view parameter is always nil, so we ignore it and make a new UILabel every time we’re called. The state names appear centered because the labels are centered within the picker view:

func numberOfComponents(in pickerView: UIPickerView) -> Int {
    return 1
}
func pickerView(_ pickerView: UIPickerView,
    numberOfRowsInComponent component: Int) -> Int {
        return self.states.count
}
func pickerView(_ pickerView: UIPickerView,
    viewForRow row: Int,
    forComponent component: Int,
    reusing view: UIView?) -> UIView {
        let lab = UILabel()
        lab.text = self.states[row]
        lab.backgroundColor = .clear
        lab.sizeToFit()
        return lab
}
pios 2504
Figure 12-7. A picker view

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:), so as 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 selectedRow(inComponent:).

You can set the value to which any drum is turned by calling 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 structure:

  • reloadComponent(_:)

  • reloadAllComponents

  • numberOfComponents

  • numberOfRows(inComponent:)

  • view(forRow:forComponent:)

By implementing pickerView(_:didSelectRow:inComponent:) and calling 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 extending 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.

UISearchBar

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-8). 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 in a table view, and the UISearchController class makes this easy to do (see Chapter 8).

pios 2505
Figure 12-8. A search bar with a search results button

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(_:shouldChangeTextIn:replacementText:)

  • searchBarShouldEndEditing(_:)

  • searchBarTextDidEndEditing(_:)

A search bar has a barStyle (UIBarStyle):

  • .default, a flat light gray background and a white search field

  • .black, a black background and a black search field

In addition, there’s a searchBarStyle property (UISearchBar.Style):

  • .default, as already described

  • .prominent, identical to .default

  • .minimal, transparent background and dark transparent search field

Alternatively, you can set a search bar’s barTintColor to change its background color; if the bar style is .black, the barTintColor will also tint the search field itself. The tintColor property, meanwhile, whose value may be inherited from higher up the view hierarchy, 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 is setBackgroundImage(_:for:barMetrics:); I’ll talk later about what the parameters mean. 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 isTranslucent is false, 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(_:for:); the second parameter is a UIControl.State (even though a search bar is not a control). According to the documentation, the possible states are .normal and .disabled; 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 text field, and disable that:

for v in self.sb.subviews[0].subviews {
    if let tf = v as? UITextField {
        tf.isEnabled = false
        break
    }
}

The search field image will be drawn vertically centered 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 it is up to you choose an appropriate height, and to ensure an appropriate background color so that 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 (isSearchResultsButtonSelected), 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(_:for: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 icon in question (the for: parameter) is specified as follows (UISearchBar.Icon):

  • .search

  • .clear (the internal cancel button)

  • .bookmark

  • .resultsList

The documentation says that the possible state: values are .normal and .disabled, but this is wrong; the choices are .normal and .highlighted. 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 isSearchResultsButtonSelected to true reverses the search results button’s behavior: it displays the highlighted image, but when the user taps it, it displays the normal image. To change an icon’s location, call setPositionAdjustment(_:for:).

A search bar may also display scope buttons. 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:)

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(_:for:); the states (the for: parameter) are .normal and .selected. 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 text attributes of the titles of the scope buttons can be customized by calling setScopeBarButtonTitleTextAttributes(_:for:). The attributes are specified like the attributes dictionary of an NSAttributedString (Chapter 10).

Tip

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-9). Let’s be careful out there.

pios 2506
Figure 12-9. A horrible search bar

The problem of allowing the keyboard to appear without covering 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.

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 search bar can be embedded in a toolbar or navigation bar as a bar button item’s custom view, or in a navigation bar as a titleView. See also the discussion of the UINavigationItem searchController property in Chapter 8. (When used in this way, you may encounter some limitations on the extent to which the search bar’s appearance can be customized.) Alternatively, a UISearchBar can itself function as a top bar, without being inside any other bar. In that case, you’ll want the search bar’s height to be extended automatically under the status bar; I’ll explain later in this chapter how to arrange that.

UIControl

UIControl is a subclass of UIView whose chief purpose is to be the superclass of several further built-in classes (controls) and to endow them with common behavior.

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 control events (UIControl.Event) are:

  • .touchDown

  • .touchDownRepeat

  • .touchDragInside

  • .touchDragOutside

  • .touchDragEnter

  • .touchDragExit

  • .touchUpInside

  • .touchUpOutside

  • .touchCancel

  • .valueChanged

  • .editingDidBegin

  • .editingChanged

  • .editingDidEnd

  • .editingDidEndOnExit

  • .allTouchEvents

  • .allEditingEvents

  • .allEvents

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.).

  • The user has edited text (Editing Did Begin, Editing Changed, etc.).

  • The user has 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:

UIButton

All Touch events.

UIDatePicker

Value Changed.

UIPageControl

All Touch events, Value Changed.

UIRefreshControl

Value Changed.

UISegmentedControl

Value Changed.

UISlider

All Touch events, Value Changed.

UISwitch

All Touch events, Value Changed.

UIStepper

All Touch events, Value Changed.

UITextField

All Touch events except the Up events, and all Editing events (see Chapter 10 for details).

UIControl (generic)

All Touch events.

A control also has a primary control event called .primaryActionTriggered, presumably to save you from having to remember what the primary control event is. The primary control event is Value Changed for all controls except for UIButton, where it is Touch Up Inside, and UITextField, where it is Did End On Exit.

For each control event that you want to hear about, you attach to the control one or more target–action pairs. You can do this in the nib editor or in code.

For any given control, each control event and its target–action pairs form a dispatch table. The following methods and properties permit you to manipulate and query the dispatch table:

  • addTarget(_:action:for:)

  • removeTarget(_:action:for:)

  • actions(forTarget:forControlEvent:)

  • allTargets

  • allControlEvents (a bitmask of control events with at least one target–action pair 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 control and the UIEvent

  • The control only

  • No parameters

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. 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. (I suppose you might, on rare occasions, 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.

Note

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:for:) method. This in turn calls the shared application instance’s sendAction(_:to:from:for:), 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 sendActions(for:) method (which is never called automatically by the runtime). 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 sendActions(for:), like this:

self.sw.setOn(true, animated: true)
self.sw.sendActions(for:.valueChanged)

You might also use sendActions(for:) in a subclass to customize the circumstances under which a control reports control events. I’ll give an example later in this chapter.

A control has isEnabled, isSelected, and isHighlighted properties; any of these can be true or false independently of the others. Together, they correspond to the control’s state, a bitmask of three possible values (UIControl.State):

  • .highlighted (isHighlighted is true)

  • .disabled (isEnabled is false)

  • .selected (isSelected is true)

A fourth state, .normal, corresponds to a zero state bitmask, meaning that isEnabled is true and that isSelected and isHighlighted are both false.

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, by default, gives the user no obvious 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 (I’ll say more about that later in this chapter).

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.

UISwitch

A switch (UISwitch, Figure 12-10) portrays a Bool value: it looks like a sliding switch, and its isOn property is either true or false. The user can slide or tap to toggle the switch’s setting. When the user changes the switch’s setting, the switch reports a Value Changed control event. To change the isOn property’s value with accompanying animation, call setOn(_:animated:).

pios 2507
Figure 12-10. A switch

A switch has only one size; any attempt to set its size will be ignored.

You can customize a switch’s appearance by setting these properties:

onTintColor

The color of the track when the switch is at the On setting.

thumbTintColor

The color of the slidable button.

tintColor

The color of the outline when the switch is at the Off setting.

There is no offTintColor property. A switch’s track when the switch is at the Off setting is transparent, and its color can’t be customized. I regard this as a bug. Merely changing the switch’s backgroundColor is not a successful workaround, because the background color shows outside the switch’s outline. An obvious (but hacky) workaround is to put a colored switch-shaped image behind the switch:

func putColor(_ color: UIColor, behindSwitch sw: UISwitch) {
    guard sw.superview != nil else {return}
    let onswitch = UISwitch()
    onswitch.isOn = true
    let r = UIGraphicsImageRenderer(bounds:sw.bounds)
    let im = r.image { ctx in
        onswitch.layer.render(in: ctx.cgContext)
    }.withRenderingMode(.alwaysTemplate)
    let iv = UIImageView(image:im)
    iv.tintColor = color
    sw.superview!.insertSubview(iv, belowSubview: sw)
    iv.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        iv.topAnchor.constraint(equalTo: sw.topAnchor),
        iv.bottomAnchor.constraint(equalTo: sw.bottomAnchor),
        iv.leadingAnchor.constraint(equalTo: sw.leadingAnchor),
        iv.trailingAnchor.constraint(equalTo: sw.trailingAnchor),
    ])
}
Warning

The UISwitch properties onImage and offImage, added in iOS 6 after much clamoring (and hacking) by developers, have no effect in iOS 7 and later.

UIStepper

A stepper (UIStepper, Figure 12-11) 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; any attempt to set its size will be ignored. 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 true, in which case the value goes beyond the maximum by starting again at the minimum, and vice versa.

pios 2510
Figure 12-11. A stepper

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 func doStep(_ sender: Any) {
    let step = sender as! UIStepper
    self.prog.setProgress(
        Float(step.value / (step.maximumValue - step.minimumValue)),
        animated:true)
}

If a stepper’s isContinuous is true (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 false, the updated value is not reported as a Value Changed control event until the entire interaction with the stepper ends; the default is true.

The appearance of a stepper can be customized. 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:

  • setDecrementImage(_:for:)

  • setIncrementImage(_:for:)

  • setDividerImage(_:forLeftSegmentState:rightSegmentState:)

  • setBackgroundImage(_:for:)

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 .disabled background image; otherwise, it displays the .normal background image, except that it displays the .highlighted 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 of one or neither segment being highlighted. The increment and decrement images, replacing the default minus and plus signs, are composited on top of the background image; they are treated as template images, colored by the tintColor, unless you explicitly provide an .alwaysOriginal image. If you provide only a .normal image, it will be adjusted automatically for the other two states. Figure 12-12 shows a customized stepper.

pios 2511
Figure 12-12. A customized stepper

UIPageControl

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 isPagingEnabled set to true. 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 bounds size required to accommodate a given number of dots, call size(forNumberOfPages:). 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.

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); you will almost certainly want to do this, as the default dot color is white, which under normal circumstances may be hard to see.

It is possible to set a page control’s backgroundColor; you might do this to show the user the tappable area, or to make the dots more clearly visible by contrast.

If a page control’s hidesForSinglePage is true, the page control becomes invisible when its numberOfPages changes to 1.

If a page control’s defersCurrentPageDisplay is true, 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, and you don’t want the current page dot to change until the animation ends.

UIDatePicker

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 (UIDatePicker.Mode):

.time

The date picker displays a time; for example, it has an hour component and a minutes component.

.date

The date picker displays a date; for example, it has a month component, a day component, and a year component.

.dateAndTime

The date picker displays a date and time; for example, it has a component showing day of the week, month, and day, plus an hour component and a minutes component.

.countDownTimer

The date picker displays a number of hours and minutes; for example, it has an hours component and a minutes component.

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 → Language & Region → Region). 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 a Calendar and a TimeZone; 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).

Warning

Don’t change the timeZone of a .countDownTimer 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 the minuteInterval to a value that doesn’t divide evenly into 60 will be silently ignored.

The date represented by a date picker (unless its mode is .countDownTimer) is its date property, a Date. The default date is now, at the time the date picker is instantiated. For a .date date picker, the time by default is 12 AM (midnight), local time; for a .time 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 (dp) 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:

dp.datePickerMode = .date
var dc = DateComponents(year:1954, month:1, day:1)
let c = Calendar(identifier:.gregorian)
let d1 = c.date(from: dc)!
dp.minimumDate = d1
dp.date = d1
dc.year = 1955
let d2 = c.date(from: dc)!
dp.maximumDate = d2
Warning

Don’t set the maximumDate and minimumDate properties values for a .countDownTimer date picker; if you do, you might cause a crash with an out-of-range exception.

To convert between a Date and a string, you’ll need a DateFormatter (see Apple’s Date and Time Programming Guide in the documentation archive):

@IBAction func dateChanged(_ sender: Any) {
    let dp = sender as! UIDatePicker
    let d = dp.date
    let df = DateFormatter()
    df.timeStyle = .full
    df.dateStyle = .full
    print(df.string(from: d))
    // Tuesday, August 10, 1954 at 3:16:00 AM GMT-07:00
}

The value displayed in a .countDownTimer date picker is its countDownDuration; this is a TimeInterval, which is a Double representing a number of seconds, even though the minimum interval displayed is a minute. A .countDownTimer date picker does not actually do any counting down! You are expected to count down in some other way, and to use some other interface to display the countdown. The Timer tab of Apple’s Clock app shows a typical interface; the user configures a picker view to set the countDownDuration initially, but once the counting starts, the picker view is hidden and a label displays the remaining time.

Warning

A nasty bug makes the Value Changed event from a .countDownTimer 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.

UISlider

A slider (UISlider) is an expression of a continuously settable value (its value, a Float) 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 isContinuous is true, the default) or only when the user releases the thumb (if isContinuous is false). While the user is pressing on the thumb, the slider is in the .highlighted state. To change a slider’s value with animation of the thumb, call setValue(_:animated:) in an animations function; I’ll show an example in a moment.

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; no control event is reported. However, with a gesture recognizer, most things are possible; here’s the action method for a UITapGestureRecognizer attached to a UISlider:

@objc func tapped(_ g:UIGestureRecognizer) {
    let s = g.view as! UISlider
    if s.isHighlighted {
        return // tap on thumb, let slider deal with it
    }
    let pt = g.location(in:s)
    let track = s.trackRect(forBounds: s.bounds)
    if !track.insetBy(dx: 0, dy: -10).contains(pt) {
        return // not on track, forget it
    }
    let percentage = pt.x / s.bounds.size.width
    let delta = Float(percentage) * (s.maximumValue - s.minimumValue)
    let value = s.minimumValue + delta
    delay(0.1) {
        UIView.animate(withDuration: 0.15) {
            s.setValue(value, animated:true) // animate sliding the thumb
        }
    }
}

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. You can change the color of the thumb with the thumbTintColor property.

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 editor), 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 these methods in a subclass:

  • minimumValueImageRect(forBounds:)

  • maximumValueImageRect(forBounds:)

  • trackRect(forBounds:)

The bounds passed in are the slider’s bounds. In this example (Figure 12-13), 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 background color so you can see how the track and images are related to its bounds:

pios 2512
Figure 12-13. Repositioning a slider’s images and track
override func maximumValueImageRect(forBounds bounds: CGRect) -> CGRect {
    return super.maximumValueImageRect(
        forBounds:bounds).offsetBy(dx: 31, dy: 0)
}
override func minimumValueImageRect(forBounds bounds: CGRect) -> CGRect {
    return super.minimumValueImageRect(
        forBounds: bounds).offsetBy(dx: -31, dy: 0)
}
override func trackRect(forBounds bounds: CGRect) -> CGRect {
    var result = super.trackRect(forBounds: 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(_:for:). There are two chiefly relevant states, .normal and .highlighted. 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 thumbRect(forBounds:trackRect:value:) in a subclass. In this example, the image is repositioned slightly upward (Figure 12-14):

pios 2513
Figure 12-14. Replacing a slider’s thumb
override func thumbRect(forBounds bounds: CGRect,
    trackRect rect: CGRect, value: Float) -> CGRect {
        return super.thumbRect(forBounds: bounds,
            trackRect: rect, value: value).offsetBy(dx: 0, dy: -7)
}

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). One solution is to increase the slider’s height; if you’re using autolayout, you can add an explicit height constraint in the nib editor, or override intrinsicContentSize in code (Chapter 1). Another solution is to subclass and use hit-test munging (Chapter 5):

override func hitTest(_ point: CGPoint, with e: UIEvent?) -> UIView? {
    let tr = self.trackRect(forBounds: self.bounds)
    if tr.contains(point) { return self }
    let r = self.thumbRect(
        forBounds: self.bounds, trackRect: tr, value: self.value)
    if r.contains(point) { return self }
    return nil
}

The track is two images, one appearing to the left of the thumb, the other to its right. They are set with setMinimumTrackImage(_:for:) and setMaximumTrackImage(_:for:). 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-15 shows a track derived from a single 15×15 image of a circular object (a coin):

pios 2514
Figure 12-15. Replacing a slider’s track
let coinEnd = UIImage(named:"coin")!.resizableImage(withCapInsets:
    UIEdgeInsets(top: 0, left: 7, bottom: 0, right: 7), resizingMode: .stretch)
self.setMinimumTrackImage(coinEnd, for:.normal)
self.setMaximumTrackImage(coinEnd, for:.normal)

UISegmentedControl

A segmented control (UISegmentedControl, Figure 12-16) is a row of tappable segments; a segment is rather like a button. The user taps a segment to choose among options. By default (isMomentary is false), the most recently tapped segment remains selected. Alternatively (isMomentary is true), 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.

pios 2515
Figure 12-16. A segmented control

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 an isMomentary 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 segmented control’s change of selection is animatable; change the selection in an animations function, like this:

UIView.animateWithDuration(0.4, animations: {
    self.seg.selectedSegmentIndex = 1
})

To animate the change more slowly when the user taps on a segment, set the segmented control’s layer’s speed to a fractional value.

A segment can be separately enabled or disabled with setEnabled(_:forSegmentAt:), and its enabled state can be retrieved with isEnabledForSegment(at:). A disabled segment, by default, is drawn faded; the user can’t tap it, but it can still be selected in code.

The color of a segmented control’s outline and selection are dictated by its tintColor, which may be inherited from further up the view hierarchy.

A segment has either a title or an image; when one is set, the other becomes nil. An image is treated as a template image, colored by the tintColor, unless you explicitly provide an .alwaysOriginal image. The title is colored by the tintColor unless you set its attributes to include a different color (as I’ll explain later). The methods for setting and fetching the title and image for existing segments are:

  • setTitle(_:forSegmentAt:), titleForSegment(at:)

  • setImage(_:forSegmentAt:), imageForSegment(at:)

If you’re creating the segmented control in code, configure the segments with init(items:), which takes an array, each item being either a string or an image:

let seg = UISegmentedControl(items:
    [UIImage(named:"one")!.withRenderingMode(.alwaysOriginal), "Two"])
seg.frame.origin = CGPoint(30,30)
self.view.addSubview(seg)

Methods for managing segments dynamically are:

  • insertSegment(withTitle:at:animated:)

  • insertSegment(with:at:animated:) (the first parameter is a UIImage)

  • removeSegment(at:animated:)

  • removeAllSegments

The number of segments can be retrieved with the read-only numberOfSegments property.

If the segmented control’s apportionsSegmentWidthsByContent property is false, segment sizes will be made equal to one another; if it is true, each segment’s width will be sized individually to fit its content. Alternatively, you can set a segment’s width explicitly with setWidth(_:forSegmentAt:) (and retrieve it with widthForSegment(at:)); setting a segment’s width to 0 means that this segment is to be sized automatically.

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. A segmented control’s height does not automatically increase to accommodate a segment image that’s too tall; instead, the image’s height is squashed to fit the segmented control’s height.

To change the position of the content (title or image) within a segment, call setContentOffset(_:forSegmentAt:) (and retrieve it with contentOffsetForSegment(at:)).

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(_:for:barMetrics:)

  • setDividerImage(_:forLeftSegmentState:rightSegmentState:barMetrics:)

  • setTitleTextAttributes(_:for:)

  • 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.

In the setContentPositionAdjustment method, the segmentType: parameter 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 (UISegmentedControl.Segment) lets you distinguish among the possibilities:

  • .any

  • .left

  • .center

  • .right

  • .alone

Figure 12-17 shows a heavily customized segmented control.

pios 2516
Figure 12-17. A segmented control, customized

UIButton

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 initializer is init(type:). The types (UIButton.ButtonType) are:

.system

The title text appears in the button’s 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 an .alwaysOriginal 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.

.detailDisclosure, .infoLight, .infoDark, .contactAdd

Basically, these are all .system buttons whose image is set automatically to a standard image. 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 .detailDisclosure only in that their showsTouchWhenHighlighted is true by default.

.custom

There’s no automatic coloring of the title or image, and the image is a normal image by default.

There is no built-in button type with an outline (border), comparable to the Rounded Rect style of iOS 6 and before. You can provide an outline by using a background color or a background image, along with some manipulation of the button’s layer, as in Figure 12-20.

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 image, which is an internal image, from its background image. The background image, if any, is stretched, if necessary, to fill the button’s bounds (technically, its backgroundRect(forBounds:)). The internal image, on the other hand, if smaller than the button, is not resized. The button can have both a title and an image, provided the image is small enough, in which case the image is shown to the left of the title by default; if the image is too large, the title won’t appear.

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: .highlighted, .selected, .disabled, and .normal. The button can be in more than one state at once, except for .normal 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(_:for:)

  • setTitleColor(_:for:)

  • setTitleShadowColor(_:for:)

  • setAttributedTitle(_:for:)

  • setImage(_:for:)

  • setBackgroundImage(_:for:)

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:

  • title(for:), currentTitle

  • titleColor(for:), currentTitleColor

  • titleShadowColor(for:), currentTitleShadowColor

  • attributedTitle(for:), currentAttributedTitle

  • image(for:), currentImage

  • backgroundImage(for:), currentBackgroundImage

When configuring these features with the set methods (or in the nib editor), 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:

  • If you specify a feature for a particular state (highlighted, selected, or disabled), and the button is in only that state, that feature will be used.

  • If you don’t specify a feature for a particular state (highlighted, selected, or disabled), and the button is in only that state, the normal version of that feature will be used as fallback. (That’s why many examples earlier in this book have assigned a title for .normal only; that’s sufficient to give the button a title in every state.)

  • Combinations of states often cause the button to fall back on the feature for normal state. For example, if a button is both highlighted and selected, the button will display its normal title, even if it has a highlighted title, a selected title, or both.

A .system 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 .custom 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

If true, then the button projects a circular white glow when highlighted. If the button has an internal image, the glow is centered behind it. This feature is suitable particularly if the button image is small and circular (as in an .infoLight or .infoDark 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

In a .custom button, if this property is true (the default), then if there is no separate highlighted image (and if showsTouchWhenHighlighted is false), the normal image is darkened when the button is highlighted. This applies equally to the internal image and the background image. (A .system button is already tinting its highlighted image, so this property doesn’t apply.)

adjustsImageWhenDisabled

If true, then if there is no separate disabled image, the normal image is shaded when the button is disabled. This applies equally to the internal image and the background image. The default is true for a .custom button and false for a .system 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 may have size .zero, making it invisible. Creating a zero-size button and then wondering why the button isn’t visible in the interface is a common beginner mistake.

The title is displayed in a UILabel (Chapter 10), and the label features of the title can be accessed through the button’s titleLabel. For example, beginners often wonder how to make a button’s title consist of more than one line; the answer is obvious, once you remember that the title is displayed in a label: set the button’s titleLabel.numberOfLines. In general, the label’s properties may be set, provided they do not conflict with existing UIButton features. For example, you can use the label to set the title’s font and shadowOffset; but the title’s text, color, and shadow color should be set using the appropriate button methods specifying a button state. If the title is given a shadow in this way, then the button’s reversesTitleShadowWhenHighlighted property also applies: if true, the shadowOffset values are replaced with their additive inverses when the button is highlighted. The modern way, however, is to do that sort of thing through the button’s attributed title.

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:

  • titleRect(forContentRect:)

  • imageRect(forContentRect:)

  • contentRect(forBounds:)

  • backgroundRect(forBounds:)

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, the content rect and the background rect are the same. 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 (Figure 12-18). In a UIButton subclass, we increase the button’s intrinsicContentSize to give it larger margins around its content, and we configure the background rect to shrink the button slightly when highlighted as a way of providing feedback (for sizeByDelta, see Appendix B):

override func backgroundRect(forBounds bounds: CGRect) -> CGRect {
    var result = super.backgroundRect(forBounds:bounds)
    if self.isHighlighted {
        result = result.insetBy(dx: 3, dy: 3)
    }
    return result
}
override var intrinsicContentSize : CGSize {
    return super.intrinsicContentSize.sizeByDelta(dw:25, dh: 20)
}

The button, which is a .custom button, is assigned an internal image and a background image from the same resizable image, along with attributed titles for the .normal and .highlighted states. The internal image glows when highlighted, thanks to adjustsImageWhenHighlighted.

pios 2517
Figure 12-18. A custom button

Custom Controls

If you create your own UIControl subclass, you automatically get the built-in Touch events; in addition, there are several methods that you can override in order to customize touch tracking, along with properties that tell you whether touch tracking is going on:

  • beginTracking(_:with:)

  • continueTracking(_:with:)

  • endTracking(_:with:)

  • cancelTracking(with:)

  • isTracking

  • isTouchInside

The main reason for using a custom UIControl subclass — rather than, say, a UIView subclass 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 touch 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.

Here’s a simple example. We’ll build a simplified knob control (Figure 12-19). 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). 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.

pios 2519
Figure 12-19. A custom control

Our control is a UIControl subclass, MyKnob. It has a public CGFloat angle property, and a private CGFloat property self.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:

override func draw(_ rect: CGRect) {
    UIImage(named:"knob")!.draw(in: 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:

func pToA (_ t:UITouch) -> CGFloat {
    let loc = t.location(in: self)
    let c = CGPoint(self.bounds.midX, self.bounds.midY)
    return atan2(loc.y - c.y, loc.x - c.x)
}

Now we’re ready to override the tracking methods. beginTracking simply notes down the angle of the initial touch location. continueTracking 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. endTracking triggers the Value Changed control event. So our first draft looks like this:

override func beginTracking(_ t: UITouch, with _: UIEvent?) -> Bool {
    self.initialAngle = pToA(t)
    return true
}
override func continueTracking(_ t: UITouch, with _: UIEvent?) -> Bool {
    let ang = pToA(t) - self.initialAngle
    let absoluteAngle = self.angle + ang
    self.transform = self.transform.rotated(by: ang)
    self.angle = absoluteAngle
    return true
}
override func endTracking(_: UITouch?, with _: UIEvent?) {
    self.sendActions(for: .valueChanged)
}

That 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 method is called.

However, our class needs modification. When the angle is set programmatically, we should respond by rotating the knob; at the same time, we need to clamp the incoming value to the allowable minimum or maximum:

var angle : CGFloat = 0 {
    didSet {
        self.angle = min(max(self.angle, 0), 5) // clamp
        self.transform = CGAffineTransform(rotationAngle: self.angle)
    }
}

Now we should revise continueTracking. We no longer need to perform the rotation, since setting the angle will do that for us. On the other hand, we do need to clamp the gesture when the minimum or maximum rotation is exceeded. My solution is simply to stop tracking; in that case, endTracking will never be called, so we also need to trigger the Value Changed control event. Also, 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 isContinuous Bool property and obey it:

override func continueTracking(_ t: UITouch, with _: UIEvent?) -> Bool {
    let ang = pToA(t) - self.initialAngle
    let absoluteAngle = self.angle + ang
    switch absoluteAngle {
    case -CGFloat.greatestFiniteMagnitude...0:
        self.angle = 0
        self.sendActions(for: .valueChanged)
        return false
    case 5...CGFloat.greatestFiniteMagnitude:
        self.angle = 5
        self.sendActions(for: .valueChanged)
        return false
    default:
        self.angle = absoluteAngle
        if self.isContinuous {
            self.sendActions(for: .valueChanged)
        }
        return true
    }
}

Bars

There are three bar types: navigation bar (UINavigationBar), toolbar (UIToolbar), and tab bar (UITabBar). They can be used independently, but are often used in conjunction with a built-in view controller (Chapter 6):

UINavigationBar

A navigation bar should appear only at the top of the screen. It is usually used in conjunction with a UINavigationController.

UIToolbar

A toolbar may appear at the bottom or at the top of the screen, though the bottom is more common. It is usually used in conjunction with a UINavigationController, where it appears at the bottom.

UITabBar

A tab bar should appear only at the bottom of the screen. It is usually used in conjunction with a UITabBarController.

This section summarizes the facts about the three bar types — along with UISearchBar, which can act independently as a top bar — and about the items that populate them.

Bar Position and Bar Metrics

If a bar is to occupy the top of the screen, its apparent height should be increased to underlap the transparent status bar. This is taken care of for you in the case of a UINavigationBar owned by a UINavigationController; otherwise, it’s up to you. To make this possible, iOS provides the notion of a bar position. The UIBarPositioning protocol, adopted by UINavigationBar, UIToolbar, and UISearchBar (the bars that can go at the top of the screen), defines one property, barPosition, whose possible values (UIBarPosition) are:

  • .any

  • .bottom

  • .top

  • .topAttached

But barPosition is read-only, so how are you supposed to set it? Use the bar’s delegate! The delegate protocols UINavigationBarDelegate, UIToolbarDelegate, and UISearchBarDelegate all conform to UIBarPositioningDelegate, which defines one method, position(for:). This provides a way for a bar’s delegate to dictate the bar’s barPosition:

class ViewController: UIViewController, UINavigationBarDelegate {
    @IBOutlet weak var navbar: UINavigationBar!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.navbar.delegate = self
    }
    func position(for bar: UIBarPositioning) -> UIBarPosition {
        return .topAttached
    }
}

The bar’s apparent height will be extended upward so as to underlap the status bar if the bar’s delegate returns .topAttached from its implementation of position(for:). To get the final position right, the bar’s top should also have a zero-constant constraint to the safe area layout guide’s top. Similarly, a toolbar or tab bar whose bottom has a zero-constant constraint to the safe area layout guide bottom will have its apparent height extended downward behind the home indicator on the iPhone X.

Tip

I say that a bar’s apparent height is extended, because in fact its height remains untouched. It is drawn extended, and this drawing is visible because the bar’s clipsToBounds is false. For this reason (and others), you should not set a bar’s clipsToBounds to true.

A bar’s height is reflected also by its bar metrics. This refers to a change in the standard height of the bar in response to a change in the orientation of the app. This change is not a behavior of the bar itself; rather, it is performed automatically by a parent view controller in a .compact horizontal size class environment:

UINavigationController

A UINavigationController adjusts the heights of its navigation bar and toolbar to be 44 (.regular vertical size class) or 32 (.compact vertical size class).

UITabBarController

A UITabBarController adjusts the height of its tab bar to be 49 (.regular vertical size class) or 32 (.compact vertical size class).

Possible bar metrics values are (UIBarMetrics):

  • .default

  • .compact

  • .defaultPrompt

  • .compactPrompt

The compact metrics apply in a .compact vertical size class environment. The prompt metrics apply to a bar whose height is extended downward to accommodate prompt text (and to a search bar whose scope buttons are showing).

When you’re customizing a feature of a bar (or a bar button item), 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 position and the bar metrics. But 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), UIBarPosition.any and UIBarMetrics.default are treated as defaults that encompass any positions and metrics you don’t specify.

Bar Appearance

A bar can be styled at three levels:

barStyle, isTranslucent

The barStyle options are (UIBarStyle):

  • .default (flat white)

  • .black (flat black)

The isTranslucent property toggles the characteristic blurry translucency.

barTintColor

This property tints the bar with a solid color.

backgroundImage

The background image is set with setBackgroundImage(_:for:barMetrics:). If the image is too large, it is sized down to fit; if it is too small, it is tiled by default, but you can change that behavior by supplying a resizable image. If a bar’s isTranslucent is false, then the barTintColor may appear behind the background image, but if its isTranslucent is true, the bar is transparent behind the image.

The degree of translucency and the interpretation of the bar tint color may vary from system to system and even from device to device, so the color you specify might not be quite the color you see. An opaque background image, however, is a reliable way to color a bar.

A UINavigationController uses the navigation bar’s barStyle in its implementation of preferredStatusBarStyle. A barStyle of .default results in a status bar style of .default (dark text); a barStyle of .black results in a status bar style of .lightContent (light text). So even if you are configuring the navigation bar’s appearance in some other way, you might still want to set its bar style as a way of setting the status bar’s text color.

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. To do so, set the shadowImage property — except that a toolbar can be either at the top or the bottom, so its setter is setShadowImage(_:forToolbarPosition:), and the UIBarPosition determines 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. You won’t see the shadow if the bar’s clipsToBounds is true (as I’ve already said, it shouldn’t be). Here’s an example for a navigation bar:

do { // must set the background image if you want a shadow image
    let sz = CGSize(20,20)
    let r = UIGraphicsImageRenderer(size:sz)
    self.navbar.setBackgroundImage( r.image { ctx in
        UIColor(white:0.95, alpha:0.85).setFill()
        ctx.fill(CGRect(0,0,20,20))
    }, for:.any, barMetrics: .default)
}
do { // now we can set the shadow image
    let sz = CGSize(4,4)
    let r = UIGraphicsImageRenderer(size:sz)
    self.navbar.shadowImage = r.image { ctx in
        UIColor.gray.withAlphaComponent(0.3).setFill()
        ctx.fill(CGRect(0,0,4,2))
        UIColor.gray.withAlphaComponent(0.15).setFill()
        ctx.fill(CGRect(0,2,4,2))
    }
}

UIBarButtonItem

You don’t add subviews to a bar. Instead, you populate the bar with bar items. For a toolbar or navigation bar, these will be bar button items (UIBarButtonItem, a subclass of UIBarItem). A bar button item is not a UIView, but you can still put an arbitrary view into a bar, because a bar button item can contain a custom view.

A bar button item may be instantiated with any of five methods:

  • init(barButtonSystemItem:target:action:)

  • init(title:style:target:action:)

  • init(image:style:target:action:)

  • init(image:landscapeImagePhone:style:target:action:)

  • init(customView:)

The style: options (UIBarButtonItem.Style) are .plain and .done; the only difference is that .done title text is bold. If you provide both an image and a landscapeImagePhone, the latter is used when the bar metrics has compact in its name (this is one of several aspects of a bar button item that can be made dependent upon the bar metrics of the containing bar). A bar button item’s image is treated by default as a template image, unless you explicitly provide an .alwaysOriginal image.

A bar button item inherits from UIBarItem the ability to adjust the image position with imageInsets (and landscapeImagePhoneInsets), plus the isEnabled and tag properties.

You can set a bar button item’s width property, but if the bar button item has a custom view, you can and should size the view from the inside out using constraints.

A bar button item’s tintColor property 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 an attributes dictionary to a bar button item’s title, and you can give a bar button item a background image:

  • setTitleTextAttributes(_:for:) (inherited from UIBarItem)

  • setTitlePositionAdjustment(_:for:)

  • setBackgroundImage(_:for:barMetrics:)

  • setBackgroundImage(_:for:style:barMetrics:)

  • setBackgroundVerticalPositionAdjustment(_:for:)

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(_:for:)

  • setBackButtonBackgroundImage(_:for:barMetrics:)

  • setBackButtonBackgroundVerticalPositionAdjustment(_:for:)

No bar button item style supplies an outline (border). (The .bordered style is deprecated, and its appearance is identical to .plain.) If you want an outline, you have to supply it yourself. For the left bar button item in the settings view of my Zotz! app (Figure 12-20), I use a custom view that’s a UIButton with a background image.

pios 2519c
Figure 12-20. A bar button item with a border

UINavigationBar

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

The title (string) or titleView (UIView) of the topItem appears in the center of the navigation bar. You can and should size the titleView from the inside out using constraints.

prefersLargeTitles

Allows the title to appear by itself at the bottom of the navigation bar, which will appear extended downward to accommodate it. In that case, both the title and the titleView can appear simultaneously. Whether the title will in fact be displayed in this way depends upon the navigation item’s largeTitleDisplayMode.always, .never, or .automatic (inherited from further down the stack).

prompt

The prompt (string) appears at the top of the navigation bar, whose height increases to accommodate it.

rightBarButtonItem, rightBarButtonItems
leftBarButtonItem, leftBarButtonItems

The 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

The 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 true, the back button is suppressed. Also, the back button is suppressed if the topItem has a leftBarButtonItem, unless the topItem also has its leftItemsSupplementBackButton set to true.

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-21, the back button title, which is sliding out to the left, 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.

pios 2519b
Figure 12-21. A back button animating to the left

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:

let sz = CGSize(10,20)
self.navbar.backIndicatorImage =
    UIGraphicsImageRenderer(size:sz).image { ctx in
        ctx.fill(CGRect(6,0,4,20))
    }
self.navbar.backIndicatorTransitionMaskImage =
    UIGraphicsImageRenderer(size:sz).image {_ in}

Changes to the navigation bar’s buttons can be animated by sending its topItem any of these messages:

  • setRightBarButton(_:animated:)

  • setLeftBarButton(_:animated:)

  • setRightBarButtonItems(_:animated:)

  • setLeftBarButtonItems(_:animated:)

  • setHidesBackButton(_:animated:)

UINavigationItems are pushed and popped with pushItem(_:animated:) and popItemAnimated(_:), or you can call setItems(_:animated:) to set all items on the stack at once.

You can determine the attributes dictionary for the title by setting the navigation bar’s titleTextAttributes, and you can shift the title’s vertical position by calling setTitleVerticalPositionAdjustment(for:). You can determine the large title’s attributes dictionary by setting the navigation bar’s largeTitleTextAttributes.

When a UINavigationBar is 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(_:shouldPush:)

  • navigationBar(_:didPush:)

  • navigationBar(_:shouldPop:)

  • navigationBar(_:didPop:)

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-22, which also shows the custom back indicator and the custom shadow I described earlier):

pios 2521
Figure 12-22. A navigation bar
override func viewDidLoad() {
    super.viewDidLoad()
    let ni = UINavigationItem(title: "Tinker")
    let b = UIBarButtonItem(title: "Evers", style: .plain,
        target: self, action: #selector(pushNext))
    ni.rightBarButtonItem = b
    self.navbar.items = [ni]
}
@objc func pushNext(_ sender: Any) {
    let oldb = sender as! UIBarButtonItem
    let s = oldb.title!
    let ni = UINavigationItem(title:s)
    if s == "Evers" {
        let b = UIBarButtonItem(title:"Chance", style: .plain,
            target:self, action:#selector(pushNext))
        ni.rightBarButtonItem = b
    }
    self.navbar.pushItem(ni, animated:true)
}

UIToolbar

A toolbar (UIToolbar, Figure 12-23) 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 .flexibleSpace and .fixedSpace, along with the UIBarButtonItem width property.

pios 2521c
Figure 12-23. A toolbar

UITabBar

A tab bar (UITabBar) displays tab bar items (UITabBarItem), its items, each consisting of an image and a name. To change the items with animation, call setItems(_:animated:).

The tab bar maintains a current selection among its items, its selectedItem, which is a UITabBarItem, not an index number; you can set it in code, or the user can set it by tapping on a tab bar item. When the user changes the selection, tabBar(_:didSelect:) is sent to the delegate (UITabBarDelegate).

You get very little control over how the tab bar items are laid out:

itemPositioning

There are three possible values (UITabBar.ItemPositioning):

.centered

The items are crowded together at the center.

.fill

The items are spaced out evenly.

.automatic

On the iPad, the same as .centered; on the iPhone, the same as .fill.

itemSpacing

The space between items, if the positioning is .centered. For the default space, specify 0.

itemWidth

The width of the items, if the positioning is .centered. For the default width, specify 0.

You can set an image to be drawn behind the selected tab bar item to indicate that it’s selected; it is the tab bar’s selectionIndicatorImage.

A UITabBarItem is created with one of these methods:

  • init(tabBarSystemItem:tag:)

  • init(title:image:tag:)

  • init(title:image:selectedImage:)

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 isEnabled and tag properties. The UITabBarItem itself adds the selectedImage property; this image replaces the image when this item is selected.

You can assign a tab bar item an alternate landscapeImagePhone (inherited from UIBarItem) to be used on the iPhone in landscape orientation. However, doing so disables the selectedImage; I regard that as a bug. The best workaround is to supply the image only, as a PDF vector image (Chapter 2).

A tab bar item’s images are treated, by default, as template images. Its title text and template image are tinted with the tab bar’s tintColor when selected and with its unselectedItemTintColor otherwise. To get full control of the title color (and other text attributes), call setTitleTextAttributes(_:for:), inherited from UIBarItem; if you set a color for .normal and a color for .selected, the .normal color will be used when the item is deselected (unless you have set the tab bar’s unselectedItemTintColor). You can use the titlePositionAdjustment property to adjust the title’s position. To get full control of the image’s color, supply an .alwaysOriginal image for both the image and selectedImage.

Figure 12-24 is an example of a customized tab bar; I’ve set the tab bar’s selection indicator image (the checkmark) and tint color (golden) of the tab bar, and the text attributes (including the green color, when selected) of the tab bar items.

pios 2521b
Figure 12-24. A tab bar

The user can be permitted to alter the contents of the tab bar, setting its tab bar items from among a larger repertoire 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(_:willBeginCustomizing:)

  • tabBar(_:didBeginCustomizing:)

  • tabBar(_:willEndCustomizing:changed:)

  • tabBar(_:didEndCustomizing:changed:)

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 present as the last item in the tab bar; the user can tap this to access the remaining items through a table view. In this table view, 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. Figure 12-25 shows how a More list looks by default.

pios 2522
Figure 12-25. Automatically generated More list

The way this works is that the automatically provided More item corresponds to a UINavigationController with a root view controller whose view is a UITableView. When the user selects an item in the table, the corresponding child view controller 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 red with white button titles, and let’s remove the word More from its title:

let more = self.tabBarController.moreNavigationController
let list = more.viewControllers[0]
list.title = ""
let b = UIBarButtonItem()
b.title = "Back"
list.navigationItem.backBarButtonItem = b
more.navigationBar.barTintColor = .red
more.navigationBar.tintColor = .white

We can go even further by supplementing the table view’s data source with a data source of our own and 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 continue from the previous example by replacing the table view’s data source with an instance of my own MyDataSource, initializing it with a reference to the original data source object:

let tv = list.view as! UITableView
let mds = MyDataSource(originalDataSource: tv.dataSource!)
self.myDataSource = mds
tv.dataSource = mds

In MyDataSource, I’ll use message forwarding (see Apple’s Objective-C Runtime Programming Guide) so that MyDataSource acts as a front end for the original data source. MyDataSource will thus magically appear to respond to any message that the original data source responds to, with any message that MyDataSource can’t handle being forwarded to the original data source:

unowned let orig : UITableViewDataSource
init(originalDataSource:UITableViewDataSource) {
    self.orig = originalDataSource
}
override func forwardingTarget(for aSelector: Selector) -> Any? {
    if self.orig.responds(to:aSelector) {
        return self.orig
    }
    return super.forwardingTarget(for: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 the original data source (analogous to calling super); then we add our own customizations as desired. Here, as a proof of concept, I’ll change each cell’s text font (Figure 12-26):

pios 2523
Figure 12-26. Customized More list
func tableView(_ tv: UITableView, numberOfRowsInSection sec: Int) -> Int {
    return self.orig.tableView(tv, numberOfRowsInSection: sec)
}
func tableView(_ tableView: UITableView,
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = self.orig.tableView(tableView, cellForRowAt: indexPath)
        cell.textLabel!.font = UIFont(name: "GillSans-Bold", size: 14)!
        return cell
}

Tint Color

Both UIView and UIBarButtonItem have a tintColor property. This property has a remarkable built-in feature: its value, if not set explicitly (or if set to nil), is inherited from its superview. (UIBarButtonItems don’t have a superview, because they aren’t views; but for purposes of this feature, pretend that they are views, and that the containing bar is their superview.)

The idea is to simplify the task of giving your app a consistent overall appearance. Many built-in interface objects use the tintColor for some aspect of their appearance, as I’ve already described. For example, if a .system button’s tintColor is red, either because you’ve set it directly or because it has inherited that color from higher up the view hierarchy, it will have red title text by default. If the highest superview in your view hierarchy — the window — has a red tintColor, then unless you do something to prevent it, all your buttons will have red title text.

The inheritance architecture works exactly the way you would expect:

Superviews and subviews

When you set the tintColor of a view, that value is inherited by all subviews of that view. The ultimate superview is the window; thus, you can set the tintColor of your UIWindow instance, and its value will be inherited by every view that ever appears in your interface.

Overriding

The inherited tintColor can be overridden by setting a view’s tintColor explicitly. Thus, 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.

Propagation

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.

Whenever a view’s tintColor changes, including when its tintColor is initially set at launch time, and including when you set it in code, this view and all its affected subviews are sent the tintColorDidChange message. A subview whose tintColor has been explicitly set to a color of its own is not sent the tintColorDidChange message merely because its superview’s tintColor changes; that’s because the subview’s own tintColor didn’t change.

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 higher up the view hierarchy. In this way, you can always learn what the effective tint color of a view is.

A UIView also has a tintAdjustmentMode. Under certain circumstances, such as the summoning of an alert (Chapter 13) or a popover (Chapter 9), the system will set the tintAdjustmentMode of the view at the top of the view hierarchy to .dimmed. 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 or popover). See “Custom Presented View Controller Transition” for an example of my own code making this change.

By default, a 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 .normal, and that change, too, will propagate down the hierarchy.

This propagation behavior is governed by the tintAdjustmentMode of the subviews. The default tintAdjustmentMode value is .automatic, 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 (.normal or .dimmed) inherited from up the view hierarchy.

If, on the other hand, you set a view’s tintAdjustmentMode explicitly to .normal or .dimmed, 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 .automatic.

Appearance Proxy

When you want to customize the look of an interface object, instead of sending a message to the object itself, you can send that message 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 once 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 obvious place to do this, because your code runs before any instances of any interface objects are created, and thus affects all of them.

This architecture, like the tintColor that I discussed in the previous section, helps you give your app a consistent appearance, as well as saving you from having to write a lot of code. For example, instead of having to send setTitleTextAttributes(_:for:) to every UITabBarItem your app ever instantiates, you send it once to the appearance proxy, and it is sent to all future UITabBarItems for you:

UITabBarItem.appearance().setTitleTextAttributes([
    .font:UIFont(name:"Avenir-Heavy", size:14)!
], for:.normal)

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 four class methods for obtaining an appearance proxy:

appearance

Returns a general appearance proxy for the receiver class. The method you call on the appearance proxy will be applied generally to future instances of this class.

appearance(for:)

The parameter is a trait collection. The method you call on the appearance proxy will be applied to future instances of the receiver class when the environment matches the specified trait collection.

appearance(whenContainedInInstancesOf:)

The argument is an array of classes, arranged in order of containment from inner to outer. The method you call on the appearance proxy will be applied only to instances of the receiver class that are actually contained in the way you describe. The notion of what “contained” means is deliberately left vague; basically, it works the way you intuitively expect it to work.

appearance(for:whenContainedInInstancesOf:)

A combination of the preceding two.

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 the other methods to say what should happen instead for certain instances of that class. Similarly, longer whenContainedInInstancesOf: chains are more specific than shorter ones.

For example, here’s some code from my Latin flashcard app (myGolden and myPaler are class properties defined by an extension on UIColor):

UIBarButtonItem.appearance().tintColor = .myGolden 1
UIBarButtonItem.appearance(
    whenContainedInInstancesOf: [UIToolbar.self])
        .tintColor = .myPaler 2
UIBarButtonItem.appearance(
    whenContainedInInstancesOf: [UIToolbar.self, DrillViewController.self])
        .tintColor = .myGolden 3

That means:

1

In general, bar button items should be tinted golden.

2

But bar button items in a toolbar are an exception: they should be tinted paler.

3

But bar button items in a toolbar in DrillViewController’s view are an exception to the exception: they should be tinted golden.

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(marble, for:.default)
// counteract the above for the black navigation bar
BlackNavigationBar.appearance().setBackgroundImage(nil, for:.default)

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 marble as their background image, except the BlackNavigationBar.”

The ultimate in specificity is to customize the look of an instance directly. Thus, for example, if you set one particular UIBarButtonItem’s tintColor property, then setting the tint color by way of a UIBarButtonItem appearance proxy will have no effect on that particular bar button item.

Not every message that can be sent to an instance of a class can be sent to that class’s appearance proxy. Unfortunately, the compiler can’t help you here; illegal code like this will compile, but will probably crash at runtime:

UIBarButtonItem.appearance().action = #selector(configureAppearance)

The problem is not that UIBarButtonItem has no action property; in the contrary, that code compiles because it does have an action property! But that property is not one that you can set by way of the appearance proxy, and the mistake isn’t caught until that line executes and the runtime tries to configure an actual UIBarButtonItem.

When in doubt, look at the class documentation; there should be a 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.

Tip

To define your own appearance-compliant property, declare that property @objc dynamic in your UIView subclass.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.106.100