Chapter 10. Text

Drawing text into your app’s interface is one of the most complex and powerful things that iOS does for you. But iOS also shields you from much of that complexity; all you need is some text to draw, and possibly an interface object to draw it for you.

Text to appear in your app’s interface will be an NSString (bridged from Swift String) or an NSAttributedString. NSAttributedString adds text styling to an NSString, including runs of different character styles, along with paragraph-level features such as alignment, line spacing, and margins.

To make your NSString or NSAttributedString appear in the interface, you can draw it into a graphics context, or hand it to an interface object that knows how to draw it:

Self-drawing text

Both NSString and NSAttributedString have methods for drawing themselves into any graphics context.

Text-drawing interface objects

Interface objects that know how to draw an NSString or NSAttributedString are:

UILabel

Displays text, possibly consisting of multiple lines; neither scrollable nor editable.

UITextField

Displays a single line of user-editable text; may have a border, a background image, and overlay views at its right and left end.

UITextView

Displays multiline text, possibly scrollable, possibly user-editable.

Deep under the hood, all text drawing is performed through a low-level technology with a C API called Core Text. At a higher level, iOS provides Text Kit, a middle-level technology lying on top of Core Text. UITextView is largely just a lightweight wrapper around Text Kit, and Text Kit can also draw directly into a graphics context. By working with Text Kit, you can readily do all sorts of useful text-drawing tricks without having to sweat your way through Core Text.

(Another way of drawing text is to use a web view, a scrollable view displaying rendered HTML. A web view can also display various additional document types, such as PDF, RTF, and .doc. Web views draw their text using a somewhat different technology, and are discussed in Chapter 11. For display of PDFs, see also the discussion of PDF Kit in Chapter 22.)

Fonts and Font Descriptors

There are two ways of describing a font: as a UIFont (suitable for use with an NSString or a UIKit interface object) or as a CTFont (suitable for Core Text). Most font transformations can be performed through UIFontDescriptor, and if you need to convert between UIFont and CTFont, you can easily do so by passing through CTFontDescriptor, which is toll-free bridged to UIFontDescriptor so that you can cast between them.

Fonts

A font (UIFont) is a simple object. You specify a font by its name and size by calling the UIFont initializer init(name:size:), and you can also transform a font to the same font in a different size by calling the withSize(_:) instance method. UIFont also provides some properties for learning a font’s various metrics, such as its lineHeight and capHeight.

To ask for a font by name, you have to know the font’s name. Every font variant (bold, italic, and so on) counts as a different font, and font variants are clumped into families. UIFont has class methods that tell you the names of the families and the names of the fonts within them. To learn, in the console, the name of every installed font, you would say:

UIFont.familyNames.forEach {
    UIFont.fontNames(forFamilyName:$0).forEach {print($0)}}

When calling init(name:size:), you can specify a font by its family name or by its font name (technically, its PostScript name). For example, "Avenir" is a family name; the plain font within that family is "Avenir-Book". Either is legal as the name: argument. The initializer is failable, so you’ll know if you’ve specified the font incorrectly — you’ll get nil.

System font

The system font (used, for example, by default in a UIButton) can be obtained by calling systemFont(ofSize:weight:). A UIFont class property such as buttonFontSize will give you the standard size. Possible weights, in order from lightest to heaviest, are (UIFont.Weight):

  • .ultraLight

  • .thin

  • .light

  • .regular

  • .medium

  • .semibold

  • .bold

  • .heavy

  • .black

Starting in iOS 9, the system font (which was formerly Helvetica) is San Francisco, and comes in all of those weights, except at sizes smaller than 20 points, where the extreme ultralight, thin, and black are missing. A variety of the system font whose digits are monospaced can be obtained by calling monospacedDigitSystemFont(ofSize:weight:). I’ll talk later about how to obtain additional variants.

Dynamic type

If you have text for the user to read or edit — in a UILabel, a UITextField, or a UITextView (all discussed later in this chapter) — you are encouraged to take advantage of dynamic type. If a font is linked to dynamic type, then:

Text size is up to the user

The user specifies a dynamic type size using a slider in the Settings app, under Display & Brightness → Text Size. Additional sizes may be enabled under General → Accessibility → Larger Text. Possible sizes (UIContentSizeCategory) are:

  • .unspecified

  • .extraSmall

  • .small

  • .medium

  • .large

  • .extraLarge

  • .extraExtraLarge

  • .extraExtraExtraLarge

  • .accessibilityMedium

  • .accessibilityLarge

  • .accessibilityExtraLarge

  • .accessibilityExtraExtraLarge

  • .accessibilityExtraExtraExtraLarge

You specify a role

You specify a dynamic type font in terms of the role it is to play in your layout. The size and weight are determined for you by the system, based on the user’s text size preference. Possible roles that you can specify (UIFont.TextStyle) are:

  • .largeTitle

  • .title1

  • .title2

  • .title3

  • .headline

  • .subheadline

  • .body

  • .callout

  • .footnote

  • .caption1

  • .caption2

You’ll probably want to experiment with specifying various roles for your individual pieces of text, to see which looks appropriate in context. (In Figure 6-1, the headlines are .subheadline and the blurbs are .caption1.)

When dynamic type was first introduced, in iOS 7, it wasn’t actually dynamic. The user could change the preferred text size, but responding to that change, by refreshing the fonts of your interface objects, was completely up to you. Starting in iOS 10, however, you can set the adjustsFontForContentSizeCategory property of your UILabel, UITextField, or UITextView to true (in code or in the nib editor); if this interface object uses dynamic type, it will then respond automatically if the user changes the Text Size preference in the Settings app.

One way to make your text use dynamic type is to specify a dynamic type font supplied by the system. In the nib editor, set the font to one of the text styles. In code, call the UIFont class method preferredFont(forTextStyle:). For example:

self.label.font = UIFont.preferredFont(forTextStyle: .headline)
self.label.adjustsFontForContentSizeCategory = true

The font, in that case, is effectively the system font in another guise. But you might prefer to use some other font. Starting in iOS 11, there’s an easy way to do that: instantiate a UIFontMetrics object by calling init(forTextStyle:) (or use the default class property, which corresponds to the .body text style); then call scaledFont(for:) with your base font. In this example, I convert an existing label to adopt a dynamic type size, even though its font is not the system font:

let f = self.label2.font
self.label2.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: f)
self.label2.adjustsFontForContentSizeCategory = true

Adoption of dynamic type means that your interface must now respond to the possibility that text will grow and shrink, with interface objects changing size in response. Obviously, autolayout can be a big help here (Chapter 1). A standard vertical spacing constraint between labels, from the upper label’s last baseline to the lower label’s first baseline, will respond to dynamic type size changes. You can configure this in the nib editor, or in code with constraint(equalToSystemSpacingBelow:multiplier:). If the distance you want is not identically the standard system spacing, adjust the constraint’s multiplier.

Sometimes, more radical adjustments of the overall layout may be needed, especially when we get into the five very large .accessibility text sizes. You’ll have to respond to text size changes in code in order to make those adjustments. To do so, implement traitCollectionDidChange(_:). The text size preference is reported through the trait collection’s preferredContentSizeCategory. UIContentSizeCategory overloads the comparison operators so that you can determine easily whether one size is larger than another; also, the isAccessibilityCategory property tells you whether this size is one of the .accessibility text sizes. To help you scale actual numeric values, the UIFontMetrics instance method scaledValue(for:) adjusts a CGFloat with respect to the user’s current text size preferences.

Tip

In the Simulator, there’s no need to keep switching to the Settings app in order to play the role of the user adjusting the Text Size slider. Instead, choose Xcode → Open Developer Tool → Accessibility Inspector and, in the inspector window, choose Simulator from the first pop-up menu at the top left; now click the button at the top right (it looks like a gear). The “Font size” slider corresponds to the accessibility text size slider; change it to change the Simulator’s dynamic type size system setting.

Adding fonts

You are not limited to the fonts installed by default as part of the system. There are two ways to obtain additional fonts:

Include a font in your app bundle

A font included at the top level of your app bundle will be loaded at launch time if your Info.plist lists it under the “Fonts provided by application” key (UIAppFonts).

Download a font in real time

All macOS fonts are available for download from Apple’s servers; you can obtain and install one while your app is running.

pios 2301aa
Figure 10-1. Embedding a font in an app bundle

Figure 10-1 shows a font included in the app bundle, along with the Info.plist entry that lists it. Observe that what you’re listing here is the name of the font file.

To download a font in real time, you’ll have to specify the font as a font descriptor (discussed in the next section) and drop down to the level of Core Text (import CoreText) to call CTFontDescriptorMatchFontDescriptorsWithProgressHandler. It takes a function which is called repeatedly at every stage of the download process; it will be called on a background thread, so if you want to use the downloaded font immediately in the interface, you must step out to the main thread (see Chapter 24).

In this example, I’ll attempt to use Nanum Brush Script as my UILabel’s font; if it isn’t installed, I’ll attempt to download it and then use it as my UILabel’s font. I’ve inserted a lot of unnecessary logging to mark the stages of the download process (using NSLog because print isn’t thread-safe):

let name = "NanumBrush"
let size : CGFloat = 24
let f : UIFont! = UIFont(name:name, size:size)
if f != nil {
    self.lab.font = f
    print("already installed")
    return
}
print("attempting to download font")
let desc = UIFontDescriptor(name:name, size:size)
CTFontDescriptorMatchFontDescriptorsWithProgressHandler(
    [desc] as CFArray, nil, { state, prog in
        switch state {
        case .didBegin:
            NSLog("%@", "matching did begin")
        case .willBeginDownloading:
            NSLog("%@", "downloading will begin")
        case .downloading:
            let d = prog as NSDictionary
            let key = kCTFontDescriptorMatchingPercentage
            let cur = d[key]
            if let cur = cur as? NSNumber {
                NSLog("progress: %@%%", cur)
            }
        case .didFinishDownloading:
            NSLog("%@", "downloading did finish")
        case .didFailWithError:
            NSLog("%@", "downloading failed")
        case .didFinish:
            NSLog("%@", "matching did finish")
            DispatchQueue.main.async {
                let f : UIFont! = UIFont(name:name, size:size)
                if f != nil {
                    NSLog("%@", "got the font!")
                    self.lab.font = f
                }
            }
        default:break
        }
        return true
})

Font Descriptors

A font descriptor (UIFontDescriptor, toll-free bridged to Core Text’s CTFontDescriptor) describes a font in terms of its features. You can then use those features to convert between font descriptors, and ultimately to derive a new font. For example, given a font descriptor desc, you can ask for a corresponding italic font descriptor like this:

let desc2 = desc.withSymbolicTraits(.traitItalic)

If desc was originally a descriptor for Avenir-Book 15, desc2 is now a descriptor for Avenir-BookOblique 15. However, it is not the font Avenir-BookOblique 15; a font descriptor is not a font. The question, therefore, is how to get from a font to a corresponding font descriptor, and vice versa:

To convert from a font to a font descriptor

Ask for the font’s fontDescriptor property. Alternatively, you can obtain a font descriptor directly just as you would obtain a font, by calling its initializer init(name:size:) or its class method preferredFontDescriptor(withTextStyle:).

To convert from a font descriptor to a font

Call the UIFont initializer init(descriptor:size:), typically supplying a size of 0 to signify that the size should not change. This can be slow, because the entire font collection must be searched; so don’t do it in speed-sensitive situations.

This will be a common pattern in your code, as you convert from font to font descriptor to perform some transformation, and then back to font:

let f = UIFont(name: "Avenir", size: 15)!
let desc = f.fontDescriptor
let desc2 = desc.withSymbolicTraits(.traitItalic)
let f2 = UIFont(descriptor: desc2!, size: 0) // Avenir-BookOblique 15

The same technique is useful for obtaining styled variants of the dynamic type fonts. In this example, I prepare to form an NSAttributedString whose font is mostly UIFont.TextStyle.body, but with one italicized word (Figure 10-2):

pios 2301a
Figure 10-2. A dynamic type font with an italic variant
let body = UIFontDescriptor.preferredFontDescriptor(withTextStyle:.body)
let emphasis = body.withSymbolicTraits(.traitItalic)!
fbody = UIFont(descriptor: body, size: 0)
femphasis = UIFont(descriptor: emphasis, size: 0)

You can explore a font’s features by way of a UIFontDescriptor. Some features are available directly as properties, such as postscriptName and symbolicTraits. The symbolicTraits is expressed as a bitmask:

let f = UIFont(name: "GillSans-BoldItalic", size: 20)!
let d = f.fontDescriptor
let traits = d.symbolicTraits
let isItalic = traits.contains(.traitItalic) // true
let isBold = traits.contains(.traitBold) // true

For other types of information, start with the name of an attribute whose value you want, as a UIFontDescriptor.AttributeName, and call object(forKey:). For example:

let f = UIFont(name: "GillSans-BoldItalic", size: 20)!
let d = f.fontDescriptor
let vis = d.object(forKey:.visibleName)!
// Gill Sans Bold Italic

Another use of font descriptors is to access hidden built-in typographical features of individual fonts. To do so, you construct a dictionary whose keys (UIFontDescriptor.FeatureKey) specify two pieces of information: the feature type (.featureIdentifier) and the feature selector (.typeIdentifer). In this example, I’ll obtain a variant of the Didot font that draws its minuscules as small caps (Figure 10-3):

pios 2301b
Figure 10-3. A small caps font variant
let desc = UIFontDescriptor(name:"Didot", size:18)
let d = [
    UIFontDescriptor.FeatureKey.featureIdentifier: kLowerCaseType,
    UIFontDescriptor.FeatureKey.typeIdentifier: kLowerCaseSmallCapsSelector
]
let desc2 = desc.addingAttributes([.featureSettings:[d]])
let f = UIFont(descriptor: desc2, size: 0)

The system (and dynamic type) font can also portray small caps; in fact, it can do this in two different ways: in addition to kLowerCaseType and kLowerCaseSmallCapsSelector, where lowercase characters are shown as small caps, it implements kUpperCaseType and kUpperCaseSmallCapsSelector, where uppercase characters are shown as small caps.

Another system (and dynamic type) font feature is an alternative set of glyph forms designed for legibility, with a type of kStylisticAlternativesType. If the selector is kStylisticAltOneOnSelector, the 6 and 9 glyphs have straight tails. If the selector is kStylisticAltSixOnSelector, certain letters also have special distinguishing shapes; for example, the lowercase “l” (ell) has a curved bottom, to distinguish it from capital “I” which has a top and bottom bar.

Typographical feature identifier constants such as kLowerCaseSmallCapsSelector come from the Core Text header SFNTLayoutTypes.h. What isn’t so clear is how you’re supposed to discover what features a particular font supports. The simple answer is that you have to drop down to the level of Core Text. For example:

let desc = UIFontDescriptor(name: "Didot", size: 20) as CTFontDescriptor
let f = CTFontCreateWithFontDescriptor(desc,0,nil)
let arr = CTFontCopyFeatures(f)

The resulting array of dictionaries includes entries [CTFeatureTypeIdentifier:37], which is kLowerCaseType, and [CTFeatureSelectorIdentifier:1], which is kLowerCaseSmallCapsSelector.

A more practical (and fun) approach to exploring a font’s features is to obtain a copy of the font on the desktop, install it, launch TextEdit, choose Format → Font → Show Fonts, select the font, and open the Typography panel, thus exposing the font’s various features. Now you can experiment on selected text.

Attributed Strings

Styled text — that is, text consisting of multiple style runs, with different font, size, color, and other text features in different parts of the text — is expressed as an attributed string (NSAttributedString and its mutable subclass, NSMutableAttributedString). An NSAttributedString consists of an NSString (its string) plus the attributes, applied in ranges.

For example, if the string “one red word” is blue except for the word “red” which is red, and if these are the only changes over the course of the string, then there are three distinct style runs — everything before the word “red,” the word “red” itself, and everything after the word “red.” However, we can apply the attributes in two steps, first making the whole string blue, and then making the word “red” red, just as you would expect.

Attributed String Attributes

The attributes applied to a range of an attributed string are described in dictionaries. Each possible attribute has a predefined name, used as a key in these dictionaries; here are some of the most important attributes (NSAttributedString.Key):

.font

A UIFont. The default is Helvetica 12 (not San Francisco, the system font).

.foregroundColor

The text color, a UIColor.

.backgroundColor

The color behind the text, a UIColor. You could use this to highlight a word, for example.

.ligature

An NSNumber wrapping 0 or 1, expressing whether or not you want ligatures used. Some fonts, such as Didot, have ligatures that are on by default.

.kern

An NSNumber wrapping the floating-point amount of kerning. A negative value brings a glyph closer to the following glyph; a positive value adds space between them.

.strikethroughStyle
.underlineStyle

An NSNumber wrapping one of these values (NSUnderlineStyle, an option set) describing the line weight:

  • .none

  • .single

  • .double

  • .thick

In addition, you may specify a line pattern; the line pattern settings have names that start with pattern, such as .patternDot, .patternDash, and so on. Also, you may specify .byWord; if you do not, then if the underline or strikethrough range spans multiple words, the whitespace between the words will be underlined or struck through.

.strikethroughColor
.underlineColor

A UIColor. If not defined, the foreground color is used.

.strokeWidth

An NSNumber wrapping a Float. The stroke width is peculiarly coded. If it’s positive, then the text glyphs are stroked but not filled, giving an outline effect, and the foreground color is used unless a separate stroke color is defined. If it’s negative, then its absolute value is the width of the stroke, and the glyphs are both filled (with the foreground color) and stroked (with the stroke color).

.strokeColor

The stroke color, a UIColor.

.shadow

An NSShadow object. An NSShadow is just a value class, combining a shadowOffset, shadowColor, and shadowBlurRadius.

.textEffect

An NSAttributedString.TextEffectStyle. The only text effect style you can specify is .letterpressStyle.

.attachment

An NSTextAttachment object. A text attachment is basically an inline image. I’ll discuss text attachments later on.

.link

A URL. This may give the style range a default appearance, such as color and underlining, but you can override this by adding attributes to the same style range. In a noneditable, selectable UITextView, the link is tappable to go to the URL (as I’ll explain later in this chapter).

.baselineOffset
.obliqueness
.expansion

An NSNumber wrapping a Float.

.paragraphStyle

An NSParagraphStyle object. This is basically just a value class, assembling text features that apply properly to paragraphs as a whole, not merely to characters, even if your string consists only of a single paragraph. Here are its most important properties:

  • alignment (NSTextAlignment)

    • .left

    • .center

    • .right

    • .justified

    • .natural (left-aligned or right-aligned depending on the localization; a right-to-left language will be right-aligned)

  • lineBreakMode (NSLineBreakMode)

    • .byWordWrapping

    • .byCharWrapping

    • .byClipping

    • .byTruncatingHead

    • .byTruncatingTail

    • .byTruncatingMiddle

  • firstLineHeadIndent, headIndent (left margin), tailIndent (right margin)

  • lineHeightMultiple, maximumLineHeight, minimumLineHeight

  • lineSpacing

  • paragraphSpacing, paragraphSpacingBefore

  • hyphenationFactor (0 or 1)

  • defaultTabInterval, tabStops (the tab stops are an array of NSTextTab objects; I’ll give an example in a moment)

  • allowsDefaultTighteningForTruncation (if true, permits some negative kerning to be applied automatically to a truncating paragraph if this would prevent truncation)

To construct an NSAttributedString, you can call init(string:attributes:) if the entire string has the same attributes; otherwise, you’ll use its mutable subclass NSMutableAttributedString, which lets you set attributes over a range.

To construct an NSParagraphStyle, you’ll use its mutable subclass NSMutableParagraphStyle. You should apply a paragraph style to the first character of a paragraph; that dictates how the whole paragraph is rendered. (Applying a paragraph style to a character other than the first character of a paragraph can cause the paragraph style to be ignored.)

Both NSAttributedString and NSParagraphStyle come with default values for all attributes, so you only have to set the attributes you care about. However, Apple says that explicitly supplying a font, foreground color, and paragraph style makes attributed strings more efficient.

Making an Attributed String

We now know enough for an example! I’ll draw my attributed strings in a disabled (noninteractive) UITextView; its background is white, but its superview’s background is gray, so you can see the text view’s bounds relative to the text. (Ignore the text’s vertical positioning, which is configured independently as a feature of the text view itself.)

First, two words of my attributed string are made extra-bold by stroking in a different color. I start by dictating the entire string and the overall style of the text; then I apply the special style to the two stroked words (Figure 10-4):

let s1 = """
    The Gettysburg Address, as delivered on a certain occasion 
    (namely Thursday, November 19, 1863) by A. Lincoln
    """
let content = NSMutableAttributedString(string:s1, attributes:[
    .font: UIFont(name:"Arial-BoldMT", size:15)!,
    .foregroundColor: UIColor(red:0.251, green:0.000, blue:0.502, alpha:1)
])
let r = (content.string as NSString).range(of:"Gettysburg Address")
content.addAttributes([
    .strokeColor: UIColor.red,
    .strokeWidth: -2.0
], range: r)
self.tv.attributedText = content
pios 2301c
Figure 10-4. An attributed string

Carrying on from the previous example, I’ll also make the whole paragraph centered and indented from the edges of the text view. To do so, I create a paragraph style and apply it to the first character. Note how the margins are dictated: the tailIndent is negative, to bring the right margin leftward, and the firstLineHeadIndent must be set separately, as the headIndent does not automatically apply to the first line (Figure 10-5):

let para = NSMutableParagraphStyle()
para.headIndent = 10
para.firstLineHeadIndent = 10
para.tailIndent = -10
para.lineBreakMode = .byWordWrapping
para.alignment = .center
para.paragraphSpacing = 15
content.addAttribute(
    .paragraphStyle,
    value:para, range:NSMakeRange(0,1))
self.tv.attributedText = content
pios 2301d
Figure 10-5. An attributed string with a paragraph style
Tip

When working temporarily with a value class such as NSMutableParagraphStyle, it feels clunky to be forced to instantiate the class and configure the instance before using it for the one and only time. So I’ve written a little Swift generic function, lend (see Appendix B), that lets me do all that in an anonymous function at the point where the value class is actually used.

In this next example, I’ll enlarge the first character of a paragraph. I assign the first character a larger font size, I expand its width slightly, and I reduce its kerning (Figure 10-6):

let s2 = """
    Fourscore and seven years ago, our fathers brought forth 
    upon this continent a new nation, conceived in liberty and 
    dedicated to the proposition that all men are created equal.
    """
content2 = NSMutableAttributedString(string:s2, attributes: [
    .font: UIFont(name:"HoeflerText-Black", size:16)!
])
content2.addAttributes([
    .font: UIFont(name:"HoeflerText-Black", size:24)!,
    .expansion: 0.3,
    .kern: -4
], range:NSMakeRange(0,1))
self.tv.attributedText = content2
pios 2301e
Figure 10-6. An attributed string with an expanded first character

Carrying on from the previous example, I’ll once again construct a paragraph style and add it to the first character. My paragraph style (applied using the lend function from Appendix B) illustrates full justification and automatic hyphenation (Figure 10-7):

content2.addAttribute(.paragraphStyle,
    value:lend { (para:NSMutableParagraphStyle) in
        para.headIndent = 10
        para.firstLineHeadIndent = 10
        para.tailIndent = -10
        para.lineBreakMode = .byWordWrapping
        para.alignment = .justified
        para.lineHeightMultiple = 1.2
        para.hyphenationFactor = 1.0
    }, range:NSMakeRange(0,1))
self.tv.attributedText = content2
pios 2301f
Figure 10-7. An attributed string with justification and autohyphenation

Now we come to the Really Amazing Part. I can make a single attributed string consisting of both paragraphs, and a single text view can portray it (Figure 10-8):

let end = content.length
content.replaceCharacters(in:NSMakeRange(end, 0), with:"
")
content.append(content2)
self.tv.attributedText = content
pios 2301g
Figure 10-8. A single attributed string comprising differently styled paragraphs

Tab stops

A tab stop is an NSTextTab, a value class whose initializer lets you set its location (points from the left edge) and alignment.

The initializer also lets you include an options: dictionary whose key (NSTextTab.OptionKey) is .columnTerminators, as a way of setting the tab stop’s column terminator characters. A common use is to create a decimal tab stop, for aligning currency values at their decimal point. You can obtain a value appropriate to a given locale by calling NSTextTab’s class method columnTerminators(for:).

Here’s an example (Figure 10-9); I have deliberately omitted the last digit from the second currency value, to prove that the tab stop really is aligning the numbers at their decimal points:

let s = "Onions	$2.34
Peppers	$15.2
"
let mas = NSMutableAttributedString(string:s, attributes:[
    .font:UIFont(name:"GillSans", size:15)!,
    .paragraphStyle:lend { (p:NSMutableParagraphStyle) in
        let terms = NSTextTab.columnTerminators(for:Locale.current)
        let tab = NSTextTab(textAlignment:.right, location:170,
            options:[.columnTerminators:terms])
        p.tabStops = [tab]
        p.firstLineHeadIndent = 20
    }
])
self.tv.attributedText = mas
pios 2301h
Figure 10-9. Tab stops in an attributed string

The tabStops array can also be modified by calling addTabStop(_:) or removeTabStop(_:) on the paragraph style. Note that a paragraph style comes with some default tab stops.

Text attachments

A text attachment is basically an inline image. To make one, you need an instance of NSTextAttachment initialized with image data; the easiest way is to start with a UIImage and assign it directly to the NSTextAttachment’s image property. You must also give the NSTextAttachment a nonzero bounds; the image will be scaled to the size of the bounds you provide, and a .zero origin places the image on the text baseline.

A text attachment is attached to an NSAttributedString using the .attachment key; the text attachment itself is the value. The range of the string that has this attribute must be a special nonprinting character whose UTF-16 codepoint is NSTextAttachment.character (0xFFFC). The simplest way to arrange that is to call the NSAttributedString initializer init(attachment:); you hand it an NSTextAttachment and it hands you an attributed string consisting of the NSTextAttachment character with its .attachment attribute set to that text attachment. You can then insert this attributed string into your own attributed string at the point where you want the image to appear.

To illustrate, I’ll add an image of onions and an image of peppers just after the words “Onions” and “Peppers” in the attributed string (mas) that I created in the previous example (Figure 10-10):

let onions = // ... get image ...
let peppers = // ... get image ...
let onionatt = NSTextAttachment()
onionatt.image = onions
onionatt.bounds = CGRect(0,-5,onions.size.width,onions.size.height)
let onionattchar = NSAttributedString(attachment:onionatt)
let pepperatt = NSTextAttachment()
pepperatt.image = peppers
pepperatt.bounds = CGRect(0,-1,peppers.size.width,peppers.size.height)
let pepperattchar = NSAttributedString(attachment:pepperatt)
let r = (mas.string as NSString).range(of:"Onions")
mas.insert(onionattchar, at:(r.location + r.length))
let r2 = (mas.string as NSString).range(of:"Peppers")
mas.insert(pepperattchar, at:(r2.location + r2.length))
self.tv.attributedText = mas
pios 2301i
Figure 10-10. Text attachments in an attributed string

Other ways to create an attributed string

The nib editor provides an ingenious interface for letting you construct attributed strings wherever built-in interface objects (such as UILabel or UITextView) accept them as a property; it’s not perfect, however, and isn’t suitable for lengthy or complex text.

It is also possible to import an attributed string from text in some other standard format, such as HTML or RTF. (There are also corresponding export methods.) To import, get the target text into a Data object and call init(data:options:documentAttributes:); alternatively, start with a file and call init(url:options:documentAttributes:). The options: allow you to specify the target text’s format. For example, here we read an RTF file from the app bundle as an attributed string and show it in a UITextView (self.tv):

let url = Bundle.main.url(forResource: "test", withExtension: "rtf")!
let opts : [NSAttributedString.DocumentReadingOptionKey : Any] =
    [.documentType : NSAttributedString.DocumentType.rtf]
let s = try! NSAttributedString(
    url: url, options: opts, documentAttributes: nil)
self.tv.attributedText = s

Modifying and Querying an Attributed String

We can coherently modify just the character content of a mutable attributed string by calling replaceCharacters(in:with:), which takes an NSRange and a substitute string. This method can do two different kinds of thing, depending whether the range has zero length:

Replacement

If the range has nonzero length, we’re replacing characters. The replacement characters all take on the attributes of the first replaced character.

Insertion

If the range has zero length, we’re inserting characters. The inserted characters all take on the attributes of the character preceding the insertion — except that, if we insert at the start, there is no preceding character, so the inserted characters take on the attributes of the character following the insertion.

You can query an attributed string about the attributes applied to a single character, asking either about all attributes at once with attributes(at:effectiveRange:), or about a particular attribute by name with attribute(_:at:effectiveRange:). The effectiveRange: argument is a pointer to an NSRange variable, which will be set by indirection to the range over which the same attribute value, or set of attribute values, applies.

In this example, we ask about the last character of our content attributed string:

var range : NSRange = NSMakeRange(0,0)
let d = content.attributes(at:content.length-1, effectiveRange:&range)

From that code we learn (in d) that the last character’s .font attribute is Hoefler Text 16, and (in range) that that attribute is applied over a stretch of 175 characters starting at character 111.

Because style runs are something of an artifice, the effectiveRange might not be what you would think of as the entire style run. The methods with longestEffectiveRange: parameters work out the entire style run range for you; but this comes at the cost of some efficiency, and in practice you typically won’t need this information anyway, because you’re cycling through ranges — so that speed, even at the cost of more iterations, matters more than getting the longest effective range on every iteration.

In this example, I start with the content attributed string and change all the size 15 material to Arial Bold 20. I don’t care whether I’m handed longest effective ranges (and my code explicitly says so); I just want to cycle efficiently:

content.enumerateAttribute(.font,
    in:NSMakeRange(0,content.length),
    options:.longestEffectiveRangeNotRequired) { value, range, stop in
        let font = value as! UIFont
        if font.pointSize == 15 {
            content.addAttribute(.font,
                value:UIFont(name: "Arial-BoldMT", size:20)!,
                range:range)
        }
    }

Custom Attributes

You are permitted to apply your own custom attributes to a stretch of text in an attributed string. Your attributes won’t directly affect how the string is drawn, because the text engine doesn’t know what to make of them; but it doesn’t object to them either. In this way, you can mark a stretch of text invisibly for your own future use.

In this example, I have a UILabel whose text includes a date. Every so often, I want to replace the date by the current date. The problem is that when the moment comes to replace the date, I don’t know where it is: I know neither its length nor the length of the text that precedes it. The solution is to use an attributed string where the date part is marked with a custom attribute.

My custom attribute is defined by extending NSAttributedString.Key:

extension NSAttributedString.Key {
    static let myDate = NSAttributedString.Key(rawValue:"myDate")
}

I’ve applied this attribute to the date part of my label’s attributed text, with an arbitrary value of 1. Now I can readily find the date again later, because the text engine will tell me where it is:

let mas = NSMutableAttributedString(
    attributedString: self.lab.attributedText!)
mas.enumerateAttribute(.myDate, in: NSMakeRange(0, mas.length)) {
    value, r, stop in
    if let value = value as? Int, value == 1 {
        mas.replaceCharacters(in: r, with: Date().description)
        stop.pointee = true
    }
}
self.lab.attributedText = mas

Drawing and Measuring an Attributed String

You can draw an attributed string yourself, rather than having a built-in text display interface object do it for you; and sometimes this will prove to be the most reliable approach. An NSString can be drawn into a rect with draw(in:withAttributes:) and related methods; an NSAttributedString can be drawn with draw(at:), draw(in:), and draw(with:options:context:).

Here, I draw an attributed string (content) into an image graphics context and extract the image, which might then be displayed by an image view:

let rect = CGRect(0,0,280,250)
let r = UIGraphicsImageRenderer(size:rect.size)
let im = r.image { ctx in
    UIColor.white.setFill()
    ctx.cgContext.fill(rect)
    content.draw(in:rect)
}

Similarly, you can draw an attributed string directly in a UIView’s draw(_:) override. For example, imagine that we have a UIView subclass called StringDrawer that has an attributedText property. The idea is that we just assign an attributed string to that property and the StringDrawer redraws itself:

self.drawer.attributedText = content

And here’s StringDrawer:

class StringDrawer : UIView {
    @NSCopying var attributedText : NSAttributedString! {
        didSet {
            self.setNeedsDisplay()
        }
    }
    override func draw(_ rect: CGRect) {
        let r = rect.offsetBy(dx: 0, dy: 2)
        let opts : NSStringDrawingOptions = .usesLineFragmentOrigin
        self.attributedText.draw(with:r, options: opts, context: context)
    }
}

The .usesLineFragmentOrigin option is crucial here. Without it, the string is drawn with its baseline at the rect origin (so that it appears above that rect), and it doesn’t wrap. The rule is that .usesLineFragmentOrigin is the implicit default for simple draw(in:), but with draw(with:options:context:) you must specify it explicitly.

NSAttributedString also provides methods to measure an attributed string, such as boundingRect(with:options:context:). Again, the .usesLineFragmentOrigin option is crucial; without it, the measured text doesn’t wrap and the returned height will be very small. The documentation warns that the returned height can be fractional and that you should round up to an integer if the height of a view is going to depend on this result.

The context: parameter of methods such as draw(with:options:context:) lets you attach an instance of NSStringDrawingContext, a simple value class whose totalBounds property tells you where you just drew.

Warning

Other features of NSStringDrawingContext, such as its minimumScaleFactor, appear to be nonfunctional.

Labels

A label (UILabel) is a simple built-in interface object for displaying text. I listed some of its chief properties in Chapter 8 (“Built-In Cell Styles”).

If you’re displaying a plain NSString in a label, by way of the label’s text property, then you are likely also to set the label’s font, textColor, and textAlignment properties, and possibly its shadowColor and shadowOffset properties. The label’s text can have an alternate highlightedTextColor, to be used when its isHighlighted property is true — as happens, for example, when the label is in a selected cell of a table view.

On the other hand, if you’re using an NSAttributedString, then you’ll set just the label’s attributedText property and let the attributes dictate things like color, alignment, and shadow. In general, if your intention is to display text in a single font, size, color, and alignment, you probably won’t bother with attributedText; but if you do set the attributedText, you should let it do all the work of dictating text style features. Those other UILabel properties do mostly work when you have set the attributedText, but they’re going to change the attributes of your entire attributed string, in ways that you might not intend. Setting the text of a UILabel that has attributedText will effectively override the attributes.

Warning

The highlightedTextColor property affects the attributedText only if the latter is the same color as the textColor.

Number of Lines

A UILabel’s numberOfLines property is extremely important. Together with the label’s line breaking behavior and resizing behavior, it determines how much of the text will appear. The default is 1 — a single line — which can come as a surprise. To make a label display more than one line of text, you must explicitly set its numberOfLines to a value greater than 1, or to 0 to indicate that there is to be no maximum.

Line break characters in a label’s text are honored. Thus, for example, in a single-line label, you won’t see whatever follows the first line break character.

Wrapping and Truncation

UILabel line breaking (wrapping) and truncation behavior, which applies to both single-line and multiline labels, is determined by the lineBreakMode (of the label or the attributed string). The options (NSLineBreakMode) are those that I listed earlier in discussing NSParagraphStyle, but their behavior within a label needs to be described:

.byClipping

Lines break at word-end, but the last line can continue past its boundary, even if this leaves a character showing only partially.

.byWordWrapping

Lines break at word-end, but if this is a single-line label, indistinguishable from .byClipping.

.byCharWrapping

Lines break in midword in order to maximize the number of characters in each line.

.byTruncatingHead
.byTruncatingMiddle
.byTruncatingTail

Lines break at word-end; if the text is too long for the label, then the last line displays an ellipsis at the start, middle, or end of the line respectively, and text is omitted at the point of the ellipsis.

The allowsDefaultTighteningForTruncation property, if true, permits some negative kerning to be applied automatically to a truncating label if this would prevent truncation.

A UILabel’s line break behavior is not the same as what happens when an NSAttributedString draws itself into a graphics context. Here are some key differences between them:

  • The default line break mode for an NSAttributedString’s NSParagraphStyle is .byWordWrapping, but the default line break mode for a new label is .byTruncatingTail.

  • An NSAttributedString whose NSParagraphStyle’s lineBreakMode doesn’t have wrapping in its name doesn’t wrap when it draws itself (it consists of a single line), but a multiline UILabel always wraps, regardless of its line break mode.

Resizing a Label to Fit Its Text

If a label is too small for its text, the entire text won’t show. If a label is too big for its text, the text is vertically centered in the label, with space above and below. Either of those might be undesirable; you might prefer the label to fit its text.

If you’re not using autolayout, in most simple cases sizeToFit will do the right thing; I believe that behind the scenes it is calling boundingRect(with:options:context:).

If you’re using autolayout, a label will correctly configure its own intrinsicContentSize automatically, based on its contents — and therefore, all other things being equal, the label will size itself to fit its contents with no code at all. Every time you reconfigure the label in a way that affects its contents (setting its text, changing its font, setting its attributed text, and so forth), the label automatically invalidates and recalculates its intrinsic content size, and thus resizes itself to fit. There are two general cases to consider:

Short single-line label

You might give the label no width or height constraints; you’ll constrain its position, but you’ll let the label’s intrinsicContentSize provide both the label’s width and its height.

Multiline label

Most likely, you’ll want to dictate the label’s width, while letting the label’s height change automatically to accommodate its contents. There are two ways to do this:

Set the label’s internal width constraint

This is appropriate particularly when the label’s width is to remain fixed ever after.

Set the label’s preferredMaxLayoutWidth

This property is a hint to help the label’s calculation of its intrinsicContentSize. It is the width at which the label, as its contents increase, will stop growing horizontally to accommodate those contents, and start growing vertically instead.

Consider a label whose top, left, and right edges are pinned to its superview, while its height is free to change based on its intrinsicContentSize. Presume also that the superview’s width can change, possibly due to rotation, thus changing the width of the label. Then the label’s height will always perfectly fit its contents, provided that, after every such change, the label’s preferredMaxLayoutWidth is adjusted to match its actual width.

How can we make that happen? It’s easy. It turns out that if we simply set the label’s preferredMaxLayoutWidth to 0, that will be taken as a signal that the label should change its preferredMaxLayoutWidth to match its width automatically whenever its width changes. Moreover, that happens to be the default preferredMaxLayoutWidth value! (In the nib editor, at the top of a label’s Size inspector, when the Explicit checkbox is unchecked and the Desired Width field says “Automatic,” that means the label’s preferredMaxLayoutWidth is 0; again, this is the default.)

Thus, by default, a label in this configuration will always fit its contents, with no effort on your part.

Instead of letting a label grow, you can permit its text font size to shrink if this would allow more of the text to fit. How the text is repositioned when the font size shrinks is determined by the label’s baselineAdjustment property. For this feature to operate, all of the following conditions must be the case:

  • The label’s adjustsFontSizeToFitWidth property must be true.

  • The label’s minimumScaleFactor must be less than 1.0.

  • The label’s size must be limited.

  • Either this must be a single-line label (numberOfLines is 1) or the line break mode (of the label or the attributed string) must not have wrapping in its name.

Customized Label Drawing

Methods that you can override in a subclass to modify a label’s drawing are drawText(in:) and textRect(forBounds:limitedToNumberOfLines:).

For example, this is the code for a UILabel subclass that outlines the label with a black rectangle and puts a five-point margin around the label’s contents:

class BoundedLabel: UILabel {
    override func awakeFromNib() {
        super.awakeFromNib()
        self.layer.borderWidth = 2.0
        self.layer.cornerRadius = 3.0
    }
    override func drawText(in rect: CGRect) {
        super.drawText(in: rect.insetBy(dx: 5, dy: 5).integral)
    }
}
Tip

A CATextLayer (Chapter 3) is like a lightweight, layer-level version of a UILabel. If the width of the layer is insufficient to display the entire string, we can get truncation behavior with the truncationMode property. If the isWrapped property is set to true, the string will wrap. We can also set the alignment with the alignmentMode property. And its string property can be an NSAttributedString.

Text Fields

A text field (UITextField) is for brief user text entry. It portrays just a single line of text; any line break characters in its text are treated as spaces. It has many of the same properties as a label. You can provide it with a plain NSString, setting its text, font, textColor, and textAlignment, or provide it with an attributed string, setting its attributedText. You can learn (and set) a text field’s overall text attributes as an attributes dictionary through its defaultTextAttributes property.

UITextField adopts the UITextInput protocol, which itself adopts the UIKeyInput protocol. These protocols endow a text field with methods for such things as obtaining the text field’s current selection and inserting text at the current selection. I’ll give examples later in this section.

Under autolayout, a text field’s intrinsicContentSize will attempt to set its width to fit its contents; if its width is fixed, you can set its adjustsFontSizeToFitWidth and minimumFontSize properties to allow the text size to shrink somewhat.

Text that is too long for the text field is displayed with an ellipsis at the end. A text field has no lineBreakMode, but you can change the position of the ellipsis by assigning the text field an attributed string with different truncation behavior, such as .byTruncatingHead. When long text is being edited, the ellipsis (if any) is removed, and the text shifts horizontally to show the insertion point.

Regardless of whether you originally supplied a plain string or an attributed string, if the text field’s allowsEditingTextAttributes property is true, the user, when editing in the text field, can summon a menu toggling the selected text’s bold, italics, or underline features.

A text field has a placeholder property, which is the text that appears faded within the text field when it has no text (its text or attributedText has been set to nil, or the user has removed all the text); the idea is that you can use this to suggest to the user what the text field is for. It has a styled text alternative, attributedPlaceholder.

If a text field’s clearsOnBeginEditing property is true, it automatically deletes its existing text (and displays the placeholder) when editing begins within it. If a text field’s clearsOnInsertion property is true, then when editing begins within it, the text remains, but is invisibly selected, and will be replaced by the user’s typing.

A text field’s border drawing is determined by its borderStyle property. Your options (UITextField.BorderStyle) are:

.none

No border.

.line

A plain black rectangle.

.bezel

A gray rectangle, where the top and left sides have a very slight, thin shadow.

.roundedRect

A larger rectangle with slightly rounded corners and a flat, faded gray color.

You can supply a background image (background); if you combine this with a borderStyle of .none, or if the image has no transparency, you get to supply your own border — unless the borderStyle is .roundedRect, in which case the background is ignored. The image is automatically resized as needed (and you will probably supply a resizable image). A second image (disabledBackground) can be displayed when the text field’s isEnabled property, inherited from UIControl, is false. The user can’t interact with a disabled text field, but without a disabledBackground image, the user may lack a sufficient visual clue to this fact. You can’t set the disabledBackground unless you have also set the background.

A text field may contain one or two ancillary overlay views, its leftView and rightView, and possibly a Clear button (a gray circle with a white X). The automatic visibility of each of these is determined by the leftViewMode, rightViewMode, and clearButtonMode, respectively. The view mode values (UITextField.ViewMode) are:

.never

The view never appears.

.whileEditing

A Clear button appears if there is text in the field and the user is editing. A left or right view appears if the user is editing, even if there is no text in the field.

.unlessEditing

A Clear button appears if there is text in the field and the user is not editing. A left or right view appears if the user is not editing, or if the user is editing but there is no text in the field.

.always

A Clear button appears if there is text in the field. A left or right view always appears.

Depending on what sort of view you use, your leftView and rightView may have to be sized manually so as not to overwhelm the text view contents. If a right view and a Clear button appear at the same time, the right view may cover the Clear button unless you reposition it.

The positions and sizes of any of the components of the text field can be set in relation to the text field’s bounds by overriding the appropriate method in a subclass:

  • clearButtonRect(forBounds:)

  • leftViewRect(forBounds:)

  • rightViewRect(forBounds:)

  • borderRect(forBounds:)

  • textRect(forBounds:)

  • placeholderRect(forBounds:)

  • editingRect(forBounds:)

You should make no assumptions about when or how frequently these methods will be called; the same method might be called several times in quick succession.

You can also override in a subclass the methods drawText(in:) and drawPlaceholder(in:). You should either draw the specified text or call super to draw it; if you do neither, the text won’t appear. Both these methods are called with a parameter whose size is the dimensions of the text field’s text area, but whose origin is .zero. In effect what you’ve got is a graphics context for just the text area; any drawing you do outside the given rectangle will be clipped.

Summoning and Dismissing the Keyboard

The presence or absence of the virtual keyboard is intimately tied to a text field’s editing state. They both have to do with the text field’s status as the first responder:

  • When a text field is first responder, it is being edited and the keyboard is present.

  • When a text field is no longer first responder, it is no longer being edited, and if no other text field (or text view) becomes first responder, the keyboard is not present. The keyboard is not dismissed if one text field takes over first responder status from another.

When the user taps in a text field, by default it is first responder, and so the keyboard appears automatically if it was not already present. You can also control the presence or absence of the keyboard in code, together with a text field’s editing state, by way of the text field’s first responder status:

Becoming first responder

To make the insertion point appear within a text field and to cause the keyboard to appear, you send becomeFirstResponder to that text field. An example appeared in Chapter 8 (“Inserting Cells”).

Resigning first responder

To make a text field stop being edited and to cause the keyboard to disappear, you send resignFirstResponder to that text field. (Actually, resignFirstResponder returns a Bool, because a responder might return false to indicate that for some reason it refuses to obey this command.)

Alternatively, call the UIView endEditing(_:) method on the first responder or any superview (including the window) to ask or compel the first responder to resign first responder status.

The endEditing(_:) method is useful particularly because there may be times when you want to dismiss the keyboard without knowing who the first responder is. You can’t send resignFirstResponder if you don’t know who to send it to. And, amazingly, there is no simple way to learn what view is first responder!

Tip

In a view presented in the .formSheet modal presentation style on the iPad (Chapter 6), the keyboard, by default, does not disappear when a text field resigns first responder status. This is presumably because a form sheet is intended primarily for text input, so the keyboard is felt as accompanying the form as a whole, not individual text fields. Optionally, you can prevent this exceptional behavior: in your UIViewController subclass, override disablesAutomaticKeyboardDismissal to return false.

Once the user has tapped in a text field and the keyboard has automatically appeared, how is the user supposed to get rid of it? On the iPad, the keyboard may contain a button that dismisses the keyboard. Otherwise, this is an oddly tricky issue. You would think that the Return key in the keyboard would dismiss the keyboard, since you can’t enter a Return character in a text field; but, of itself, it doesn’t.

One solution is to be the text field’s delegate and to implement a text field delegate method, textFieldShouldReturn(_:). When the user taps the Return key in the keyboard, we hear about it through this method, and we receive a reference to the text field; we can respond by telling the text field to resign its first responder status, which dismisses the keyboard:

func textFieldShouldReturn(_ tf: UITextField) -> Bool {
    tf.resignFirstResponder()
    return false
}

Certain virtual keyboards, however, lack a Return key. In that case, you’ll need some other way to allow the user to dismiss the keyboard, such as a button elsewhere in the interface. Alternatively, if there’s a scroll view in the interface, you can set its keyboardDismissMode to provide a way of letting the user dismiss the keyboard. The options (UIScrollView.KeyboardDismissMode) are:

.none

The default; if the keyboard doesn’t contain a button that lets the user dismiss it, we must use code to dismiss it.

.interactive

The user can dismiss the keyboard by dragging it down.

.onDrag

The keyboard dismisses itself if the user scrolls the scroll view.

A scroll view with a keyboardDismissMode that isn’t .none also calls resignFirstResponder on the text field when it dismisses the keyboard.

Keyboard Covers Text Field

The keyboard, having appeared from offscreen, occupies a position “docked” at the bottom of the screen. This may cover the text field in which the user wants to type, even if it is first responder. You’ll typically want to do something to reveal the text field.

To help with this, you can register for keyboard-related notifications:

  • UIResponder.keyboardWillShowNotification

  • UIResponder.keyboardDidShowNotification

  • UIResponder.keyboardWillHideNotification

  • UIResponder.keyboardDidHideNotification

Those notifications all have to do with the docked position of the keyboard. On the iPhone, keyboard docking and keyboard visibility are equivalent: the keyboard is visible if and only if it is docked. On the iPad, where the user can undock the keyboard and slide it up and down the screen, the keyboard is said to show if it is being docked, whether that’s because it is appearing from offscreen or because the user is docking it, and it is said to hide if it is being undocked, whether that’s because it is moving offscreen or because the user is undocking it.

Two additional notifications are sent both when the keyboard enters and leaves the screen and (on the iPad) when the user drags it, splits or unsplits it, and docks or undocks it:

  • UIResponder.keyboardWillChangeFrameNotification

  • UIResponder.keyboardDidChangeFrameNotification

The most important situations to respond to are those corresponding to the willShow and willHide notifications, when the keyboard is attaining or leaving its docked position at the bottom of the screen. You might think that it would be necessary to handle the willChangeFrame notification too, in case the keyboard changes its height — as can happen, for example, if user switches from the text keyboard to the emoji keyboard on the iPhone. But in fact the willShow notification is sent in that situation as well.

Each notification’s userInfo dictionary contains information describing what the keyboard will do or has done, under these keys:

  • UIResponder.keyboardFrameBeginUserInfoKey

  • UIResponder.keyboardFrameEndUserInfoKey

  • UIResponder.keyboardAnimationDurationUserInfoKey

  • UIResponder.keyboardAnimationCurveUserInfoKey

When you receive a UIResponder.keyboardWillShowNotification, you can look at the UIResponder.keyboardFrameEndUserInfoKey to learn what position the keyboard is moving to. It is an NSValue wrapping a CGRect in screen coordinates. By converting the coordinate system as appropriate, you can compare the keyboard’s new frame with the frame of your interface items. For example, if the keyboard’s new frame intersects a text field’s frame (in the same coordinates), the keyboard is going to cover that text field.

A natural-looking response, in that case, is to slide the entire interface upward as the keyboard appears, just enough to expose the text field being edited above the top of the keyboard. The simplest way to do that is for the entire interface to be inside a scroll view — which is, after all, a view that knows how to slide its contents.

This scroll view need not be ordinarily scrollable by the user, who may be completely unaware of its existence. But after the keyboard appears, the scroll view should be scrollable by the user, so that the user can inspect the entire interface at will, even while the keyboard is covering part of it. We can ensure that by adjusting the scroll view’s contentInset.

This behavior is in fact implemented automatically by a UITableViewController. When a text field inside a table cell is first responder, the table view controller adjusts the bottom of the table view’s adjustedContentInset to compensate for the keyboard. The result is that the entire table view content is available within the space between the top of the table view and the top of the keyboard.

Moreover, a scroll view has two additional bits of built-in behavior that will help us:

  • It scrolls automatically to reveal the first responder. This will make it easy for us to expose the text field being edited.

  • It has a keyboardDismissMode, governing what will happen to the keyboard when the user scrolls. This can give us an additional way to allow the user to dismiss the keyboard, as I mentioned in the preceding section.

Let’s imitate UITableViewController’s behavior with a scroll view containing text fields. In particular, our interface consists of a scroll view containing a content view; the content view contains several text fields.

In viewDidLoad, we register for keyboard notifications:

NotificationCenter.default.addObserver(self,
    selector: #selector(keyboardShow),
    name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self,
    selector: #selector(keyboardHide),
    name: UIResponder.keyboardWillHideNotification, object: nil)

We are the delegate of any text fields, so that we can hear about it when the user taps the Return key in the keyboard. We use that as a signal to dismiss the keyboard, as I suggested earlier:

func textFieldShouldReturn(_ tf: UITextField) -> Bool {
    tf.resignFirstResponder()
    return false
}

To implement the notification methods keyboardShow and keyboardHide, it will help to have on hand a utility function that works out the geometry based on the notification’s userInfo dictionary and the bounds of the view we’re concerned with (which will be the scroll view). If the keyboard wasn’t within the view’s bounds and now it will be, it is entering; if it was within the view’s bounds and now it won’t be, it is exiting. We return that information, along with the keyboard’s frame in the view’s bounds coordinates:

enum KeyboardState {
    case unknown
    case entering
    case exiting
}
func keyboardState(for d:[AnyHashable:Any], in v:UIView?)
    -> (KeyboardState, CGRect?) {
        var rold = d[UIResponder.keyboardFrameBeginUserInfoKey] as! CGRect
        var rnew = d[UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
        var ks : KeyboardState = .unknown
        var newRect : CGRect? = nil
        if let v = v {
            let co = UIScreen.main.coordinateSpace
            rold = co.convert(rold, to:v)
            rnew = co.convert(rnew, to:v)
            newRect = rnew
            if !rold.intersects(v.bounds) && rnew.intersects(v.bounds) {
                ks = .entering
            }
            if rold.intersects(v.bounds) && !rnew.intersects(v.bounds) {
                ks = .exiting
            }
        }
        return (ks, newRect)
}

When the keyboard shows, we check whether it is initially appearing on the screen; if so, we store the scroll view’s current content offset, content inset, and scroll indicator insets. Then we alter the scroll view’s insets appropriately, allowing the scroll view itself to scroll the first responder into view if needed:

@objc func keyboardShow(_ n:Notification) {
    let d = n.userInfo!
    let (state, rnew) = keyboardState(for:d, in:self.scrollView)
    if state == .entering {
        self.oldContentInset = self.scrollView.contentInset
        self.oldIndicatorInset = self.scrollView.scrollIndicatorInsets
        self.oldOffset = self.scrollView.contentOffset
    }
    if let rnew = rnew {
        let h = rnew.intersection(self.scrollView.bounds).height
        self.scrollView.contentInset.bottom = h
        self.scrollView.scrollIndicatorInsets.bottom = h
    }
}

When the keyboard hides, we reverse the process, restoring the saved values:

@objc func keyboardHide(_ n:Notification) {
    let d = n.userInfo!
    let (state, _) = keyboardState(for:d, in:self.scrollView)
    if state == .exiting {
        self.scrollView.contentOffset = self.oldOffset
        self.scrollView.scrollIndicatorInsets = self.oldIndicatorInset
        self.scrollView.contentInset = self.oldContentInset
    }
}

Behind the scenes, we are inside an animations function at the time that our notifications arrive. This means that our changes to the scroll view are nicely animated in coordination with the keyboard appearing and disappearing.

Under iPad multitasking (Chapter 9), your app can receive keyboard show and hide notifications if another app summons or dismisses the keyboard. This makes sense because the keyboard is, after all, covering your app. You can distinguish whether your app was responsible for summoning the keyboard by examining the show notification userInfo dictionary’s UIResponder.keyboardIsLocalUserInfoKey; but in general you won’t have to. If you were handling keyboard notifications coherently before iPad multitasking came along, you are probably still handling them coherently.

Text Field Delegate and Control Event Messages

As editing begins and proceeds in a text field, various messages are sent to the text field’s delegate, adopting the UITextFieldDelegate protocol. Some of these messages are also available as notifications. Using them, you can customize the text field’s behavior during editing:

textFieldShouldBeginEditing(_:)

Return false to prevent the text field from becoming first responder.

textFieldDidBeginEditing(_:)
UITextField.textDidBeginEditingNotification

The text field has become first responder.

textFieldShouldClear(_:)

Return false to prevent the operation of the Clear button or of automatic clearing on entry (clearsOnBeginEditing). This event is not sent when the text is cleared because clearsOnInsertion is true, presumably because the user is not clearing the text but rather changing it.

textFieldShouldReturn(_:)

The user has tapped the Return button in the keyboard. We have already seen that this can be used as a signal to dismiss the keyboard.

textField(_:shouldChangeCharactersIn:replacementString:)
UITextField.textDidChangeNotification

The notification is a signal that the user has edited the text, but the delegate method is your chance to interfere with the user’s editing before it takes effect. You can return false to prevent the proposed change; if you’re going to do that, you can replace the user’s edit with your own, by changing the text field’s text directly (there is no circularity, as this delegate method is not called when you do that).

In this example, the user can enter only lowercase characters (the insertText method comes from the UIKeyInput protocol, which UITextField adopts):

func textField(_ textField: UITextField,
    shouldChangeCharactersIn range: NSRange,
    replacementString string: String) -> Bool {
        if string.isEmpty { // backspace
            return true
        }
        let lc = string.lowercased()
        textField.insertText(lc)
        return false
}

As the example shows, you can distinguish whether the user is typing or pasting, on the one hand, or backspacing or cutting, on the other; in the latter case, the replacement string will be empty. You are not notified when the user changes text styling through the Bold, Italics, or Underline menu items.

textFieldShouldEndEditing(_:)

Return false to prevent the text field from resigning first responder (even if you just sent resignFirstResponder to it). You might do this, for example, because the text is invalid or unacceptable in some way. The user will not know why the text field is refusing to end editing, so the usual thing is to put up an alert (Chapter 13) explaining the problem.

textFieldDidEndEditing(_:)
UITextField.textDidEndEditingNotification

The text field has resigned first responder. See Chapter 8 (“Editable Content in Cells”) for an example of using the delegate method to fetch the text field’s current text and store it in the model.

A text field is also a control (UIControl; see also Chapter 12). That means you can attach a target–action pair to any of the events that it reports in order to receive a message when that event occurs. Of the various control event messages emitted by a text field, the two most useful (in my experience) are:

Editing Changed (.editingChanged)

Sent after the user performs any editing. If your goal is to respond to changes, rather than to forestall them, this is a better way than the delegate method textField(_:shouldChangeCharactersIn:replacementString:), because it arrives at the right moment, namely after the change has occurred, and because it can detect attributes changes, which the delegate method can’t do.

Did End on Exit (.editingDidEndOnExit)

Sent when the user taps the Return button in the text field’s keyboard. The keyboard is dismissed automatically — even if the action method does nothing. (However, the text field’s delegate can prevent this by implementing textFieldShouldReturn(_:) to return false.)

In fact, the action method doesn’t even have to exist! The action can be nil-targeted. There is no penalty for implementing a nil-targeted action that walks up the responder chain without finding a method that handles it. In this example, I create a UITextField subclass that automatically dismisses itself when the user taps Return:

@objc protocol Dummy {
    func dummy(_ sender: Any?)
}
class MyTextField: UITextField {
    required init?(coder aDecoder: NSCoder) {
        super.init(coder:aDecoder)
        self.addTarget(nil,
            action:#selector(Dummy.dummy), for:.editingDidEndOnExit)
    }
}

You can configure the same thing in the nib editor. Edit the First Responder proxy object in the Attributes inspector, adding a new First Responder Action; call it dummy:. Now hook the Did End on Exit event of the text field to the dummy: action of the First Responder proxy object.

Text Field Menu

When the user double taps or long presses in a text field, the menu appears. It contains menu items such as Select, Select All, Paste, Copy, Cut, and Replace; which menu items appear depends on the circumstances. Many of the selectors for these standard menu items are listed in the UIResponderStandardEditActions protocol. Commonly used standard actions are:

  • cut(_:)

  • copy(_:)

  • select(_:)

  • selectAll(_:)

  • paste(_:)

  • delete(_:)

  • toggleBoldface(_:)

  • toggleItalics(_:)

  • toggleUnderline(_:)

Some other menu items are known only through their Objective-C selectors:

  • _promptForReplace:

  • _define:

  • _showTextStyleOptions:

The menu can be customized; just as with a table view cell’s menus (Chapter 8), this involves setting the shared UIMenuController object’s menuItems property to an array of UIMenuItem instances representing the menu items that may appear in addition to those that the system puts there.

Actions for menu items are nil-targeted, so they percolate up the responder chain. You can thus implement a menu item’s action anywhere up the responder chain; if you do this for a standard menu item at a point in the responder chain before the system receives it, you can interfere with and customize what it does. You govern the presence or absence of a menu item by implementing the UIResponder method canPerformAction(_:withSender:) in the responder chain.

As an example, we’ll devise a text field whose menu includes our own menu item, Expand. I’m imagining a text field where the user can select a U.S. state’s two-letter abbreviation (such as “CA”) and can then summon the menu and tap Expand to replace it with the state’s full name (such as “California”). I’ll implement this in a UITextField subclass called MyTextField, in order to guarantee that the Expand menu item will be available when an instance of this subclass is first responder, but at no other time.

At some moment before the user taps in an instance of MyTextField (such as our view controller’s viewDidLoad), we modify the global menu:

let mi = UIMenuItem(title:"Expand", action:#selector(MyTextField.expand))
let mc = UIMenuController.shared
mc.menuItems = [mi]

The text field subclass has a property, self.list, which has been set to a dictionary whose keys are state name abbreviations and whose values are the corresponding state names. A utility function looks up an abbreviation in the dictionary:

func state(for abbrev:String) -> String? {
    return self.list[abbrev.uppercased()]
}

We implement canPerformAction(_:withSender:) to govern the contents of the menu. Let’s presume that we want our Expand menu item to be present only if the selection consists of a two-letter state abbreviation. UITextField conforms to the UITextInput protocol, which lets us learn the selected text:

override func canPerformAction(_ action: Selector,
    withSender sender: Any?) -> Bool {
        if action == #selector(expand) {
            if let r = self.selectedTextRange, let s = self.text(in:r) {
                return (s.count == 2 && self.state(for:s) != nil)
            }
        }
        return super.canPerformAction(action, withSender:sender)
}

When the user chooses the Expand menu item, the expand message is sent up the responder chain. We catch it in our UITextField subclass and obey it by replacing the selected text with the corresponding state name:

@objc func expand(_ sender: Any?) {
    if let r = self.selectedTextRange, let s = self.text(in:r) {
        if let ss = self.state(for:s) {
            self.replace(r, withText:ss)
        }
    }
}

We can also implement the selector for, and thus modify the behavior of, any of the standard menu items. For example, I’ll implement copy(_:) and modify its behavior. First we call super to get standard copying behavior; then we modify what’s now on the pasteboard:

override func copy(_ sender:Any?) {
    super.copy(sender)
    let pb = UIPasteboard.general
    if let s = pb.string {
        let ss = // ... alter s here ...
        pb.string = ss
    }
}

Drag and Drop

A text field implements drag and drop (Chapter 9) by way of the UITextDraggable and UITextDroppable protocols. By default a text field’s text is draggable on an iPad and not draggable on an iPhone, but you can set the isEnabled property of its textDragInteraction to change that. If a text field’s text is draggable, then by default its dragged text can be dropped within the same text field.

To customize a text field’s drag and drop behavior, provide a textDragDelegate (UITextDragDelegate) or textDropDelegate (UITextDropDelegate) and implement any of their various methods. For example, you can change the drag preview, change the drag items, and so forth. To turn a text field’s droppability on or off depending on some condition, give it a textDropDelegate and implement textDroppableView(_:proposalForDrop:) to return an appropriate UITextDropProposal.

Keyboard and Input Configuration

There are various ways to configure the virtual keyboard that appears when a text field becomes first responder. This configuration is performed through properties, not of the keyboard, but of the text field.

Text input traits

A UITextField adopts the UITextInputTraits protocol. This protocol’s properties customize physical features and behaviors of the keyboard, as well as the text field’s response to input. (These properties can also be set in the nib editor.) For example:

  • Set the keyboardType to choose one of many alternate built-in keyboard layouts. For example, set it to .numberPad to make the virtual keyboard for this text field consist of digits.

  • Set the returnKeyType to determine the text of the Return key (if the keyboard is of a type that has one).

  • Give the keyboard a dark or light shade (keyboardAppearance).

  • Turn off autocapitalization or autocorrection (autocapitalizationType, autocorrectionType).

  • Use or don’t use smart quotes, smart dashes, and smart spaces during insertion and deletion (smartQuotesType, smartDashesType, smartInsertDeleteType).

  • Make the Return key disable itself if the text field has no content (enablesReturnKeyAutomatically).

  • Make the text field a password field (secureTextEntry).

  • Set the textContentType to assist the system in making appropriate spelling and autofill suggestions.

Accessory view

You can attach an accessory view to the top of the keyboard by setting the text field’s inputAccessoryView. For instance, an accessory view containing a button can serve as a way to let the user dismiss keyboards whose type has no Return key, such as .numberPad, .phonePad, and .decimalPad.

pios 2303a
Figure 10-11. A phonePad keyboard with an accessory view

Figure 10-11 shows a .phonePad keyboard. It has no Return key, so we’ve added a Done button in its accessory view. The accessory view itself is designed in a view .xib file. We (the view controller) are the text field’s delegate; when the text field becomes first responder, we configure the keyboard:

func textFieldDidBeginEditing(_ tf: UITextField) {
    self.currentField = tf // keep track of first responder
    let arr =
        UINib(nibName:"AccessoryView", bundle:nil).instantiate(withOwner:nil)
    let accessoryView = arr[0] as! UIView
    let b = accessoryView.subviews[0] as! UIButton
    b.addTarget(self, action:#selector(doNextButton), for:.touchUpInside)
    let b2 = accessoryView.subviews[1] as! UIButton
    b2.addTarget(self, action:#selector(doDone), for:.touchUpInside)
    tf.inputAccessoryView = accessoryView
    tf.keyboardAppearance = .dark
    tf.keyboardType = .phonePad
}

When the Done button is tapped, we dismiss the keyboard:

@objc func doDone(_ sender: Any) {
    self.currentField = nil
    self.view.endEditing(false)
}

The Next button lets the user navigate to the next text field. I have an array property (self.textFields) populated with references to all the text fields in the interface. My textFieldDidBeginEditing implementation stores a reference to the current text field in a property (self.currentField), because in order to determine the next text field, I need to know which one is this text field:

@objc func doNextButton(_ sender: Any) {
    var ix = self.textFields.firstIndex(of:self.currentField)!
    ix = (ix + 1) % self.textFields.count
    let v = self.textFields[ix]
    v.becomeFirstResponder()
}

Input view

Going even further, you can replace the system keyboard entirely with a view of your own creation. This is done by setting the text field’s inputView. For best results, the custom view should be a UIInputView, and ideally it should be the inputView (and view) of a UIInputViewController. The input view controller needs to be retained, but not as a child view controller in the view controller hierarchy; the keyboard is not one of your app’s views, but is layered by the system in front of your app. The input view’s contents might imitate a standard system keyboard, or may consist of any interface you like.

Tip

An input view controller, used in this way, is also the key to supplying other apps with a keyboard. See the “Custom Keyboard” chapter of Apple’s App Extension Programming Guide in the documentation archive.

To illustrate, I’ll implement a standard beginner example: I’ll replace a text field’s keyboard with a UIPickerView. Here’s the input view controller, MyPickerViewController. Its viewDidLoad puts the UIPickerView into the inputView and positions it with autolayout constraints:

class MyPickerViewController : UIInputViewController {
    override func viewDidLoad() {
        let iv = self.inputView!
        iv.translatesAutoresizingMaskIntoConstraints = false
        let p = UIPickerView()
        p.delegate = self
        p.dataSource = self
        iv.addSubview(p)
        p.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            p.topAnchor.constraint(equalTo: iv.topAnchor),
            p.bottomAnchor.constraint(equalTo: iv.bottomAnchor),
            p.leadingAnchor.constraint(equalTo: iv.leadingAnchor),
            p.trailingAnchor.constraint(equalTo: iv.trailingAnchor),
        ])
    }
}
extension MyPickerViewController : UIPickerViewDelegate,
                                   UIPickerViewDataSource {
    // ...
}

The text field itself is configured in our main view controller:

class ViewController: UIViewController {
    @IBOutlet weak var tf: UITextField!
    let pvc = MyPickerViewController()
    override func viewDidLoad() {
        super.viewDidLoad()
        self.tf.inputView = self.pvc.inputView
    }
}

It is also possible to use an input view controller to manage a text field’s inputAccessoryView. To do that, you set the text field’s inputAccessoryViewController instead of its inputAccessoryView. To do that, you have to subclass UITextField to give it a writable inputAccessoryViewController (because this property, as inherited from UIResponder, is read-only):

class MyTextField : UITextField {
    var _iavc : UIInputViewController?
    override var inputAccessoryViewController: UIInputViewController? {
        get {
            return self._iavc
        }
        set {
            self._iavc = newValue
        }
    }
}

Let’s use that feature to give the user a way to dismiss the “keyboard” consisting entirely of a UIPickerView. We’ll attach a Done button as the text field’s accessory input view and manage it with another input view controller, MyDoneButtonViewController. I’ll configure the button much as I configured the picker view, by putting it into the input view controller’s inputView:

class MyDoneButtonViewController : UIInputViewController {
    weak var delegate : UIViewController?
    override func viewDidLoad() {
        let iv = self.inputView!
        iv.translatesAutoresizingMaskIntoConstraints = false
        iv.allowsSelfSizing = true // crucial
        let b = UIButton(type: .system)
        b.tintColor = .black
        b.setTitle("Done", for: .normal)
        b.sizeToFit()
        b.addTarget(self, action: #selector(doDone), for: .touchUpInside)
        b.backgroundColor = UIColor.lightGray
        iv.addSubview(b)
        b.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            b.topAnchor.constraint(equalTo: iv.topAnchor),
            b.bottomAnchor.constraint(equalTo: iv.bottomAnchor),
            b.leadingAnchor.constraint(equalTo: iv.leadingAnchor),
            b.trailingAnchor.constraint(equalTo: iv.trailingAnchor),
        ])
    }
    @objc func doDone() {
        if let del = self.delegate {
            (del as AnyObject).doDone?()
        }
    }
}

Now our main view controller configures the text field like this:

class ViewController: UIViewController {
    @IBOutlet weak var tf: UITextField!
    let pvc = MyPickerViewController()
    let mdbvc = MyDoneButtonViewController()
    override func viewDidLoad() {
        super.viewDidLoad()
        self.tf.inputView = self.pvc.inputView
        (self.tf as! MyTextField).inputAccessoryViewController = self.mdbvc
        self.mdbvc.delegate = self
    }
}

When the Done button is tapped, MyDoneButtonViewController’s doDone method is called. It, in turn, calls the doDone method of its delegate, if there is one. Its delegate is our original ViewController, which can implement doDone to set the text of the text field and dismiss the keyboard.

An important advantage of using an input view controller is that it is a view controller. Despite not being part of the app’s view controller hierarchy, it is sent standard view controller messages such as viewDidLayoutSubviews and traitCollectionDidChange, allowing you to respond coherently to rotation and other size changes.

Input view without a text field

With only a slight modification, you can use the techniques described in the preceding section to present a custom input view to the user without the user editing any text field. For example, suppose we have a label in our interface; we can allow the user to tap a button to summon our custom input view and use that input to change the text of the label. (See Figure 10-12; as usual, my example revolves around letting the user specify one of the Pep Boys.)

The trick here is that the relevant UITextField properties and methods are all inherited from UIResponder — and a UIViewController is a UIResponder. All we have to do is override our view controller’s canBecomeFirstResponder to return true, and then call its becomeFirstResponder — just like a text field. If the view controller has overridden inputView, our custom input view will appear as the virtual keyboard. If the view controller has overridden inputAccessoryView or inputAccessoryViewController, the accessory view will be attached to that keyboard.

Here’s an implementation of that scenario. Normally, our view controller’s canBecomeFirstResponder returns false, so that the input view won’t appear. But when the user taps the button in our interface, we switch to returning true and call becomeFirstResponder. Presto, the input view appears along with the accessory view, because we’ve also overridden inputView and inputAccessoryViewController. When the user taps the Done button in the accessory view, we update the label and dismiss the keyboard:

pios 2302aaaaa
Figure 10-12. Editing a label with a custom input view
class ViewController: UIViewController {
    @IBOutlet weak var lab: UILabel!
    let pvc = MyPickerViewController()
    let mdbvc = MyDoneButtonViewController()
    override func viewDidLoad() {
        super.viewDidLoad()
        self.mdbvc.delegate = self // for dismissal
    }
    var showKeyboard = false
    override var canBecomeFirstResponder: Bool {
        return showKeyboard
    }
    override var inputView: UIView? {
        return self.pvc.inputView
    }
    override var inputAccessoryViewController: UIInputViewController? {
        return self.mdbvc
    }
    @IBAction func doPickBoy(_ sender: Any) { // button in the interface
        self.showKeyboard = true
        self.becomeFirstResponder()
    }
    @objc func doDone() { // user tapped Done button in accessory view
        self.lab.text = pvc.currentPep // update label
        self.resignFirstResponder() // dismiss keyboard
        self.showKeyboard = false
    }
}

Shortcuts bar

On the iPad, the shortcuts bar appears along with spelling suggestions at the top of the keyboard. You can customize it by adding bar button items.

The shortcuts bar is the text field’s inputAssistantItem (inherited from UIResponder), and it has leadingBarButtonGroups and trailingBarButtonGroups. A button group is a UIBarButtonItemGroup, an array of UIBarButtonItems along with an optional representativeItem to be shown if there isn’t room for the whole array; if the representative item has no target–action pair, tapping it will summon a popover containing the actual group.

In this example, we add a Camera bar button item to the right (trailing) side of the shortcuts bar for our text field (self.tf):

let bbi = UIBarButtonItem(
    barButtonSystemItem: .camera, target: self, action: #selector(doCamera))
let group = UIBarButtonItemGroup(
    barButtonItems: [bbi], representativeItem: nil)
let shortcuts = self.tf.inputAssistantItem
shortcuts.trailingBarButtonGroups.append(group)

Keyboard language

Suppose your app performs a Russian dictionary lookup. It would be nice to be able to force the keyboard to appear as Russian in conjunction with your text field. But you can’t. You can’t access the Russian keyboard unless the user has explicitly enabled it; and even if the user has explicitly enabled it, your text field can only express a preference as to the language in which the keyboard initially appears. To do so, override your view controller’s textInputMode property along these lines:

override var textInputMode: UITextInputMode? {
    for tim in UITextInputMode.activeInputModes {
        if tim.primaryLanguage == "ru-RU" {
            return tim
        }
    }
    return super.textInputMode
}

Another keyboard language–related property is textInputContextIdentifier. You can use this to ensure that the runtime remembers the language to which the keyboard was set the last time each text field was edited. To do so, override textInputContextIdentifier in your view controller as a computed variable whose getter fetches the value of a stored variable, and set that stored variable to some appropriate unique value whenever the editing context changes, whatever that may mean for your app.

Text Views

A text view (UITextView) is a scroll view subclass (UIScrollView); it is not a control. It displays multiline text, possibly scrollable, possibly user-editable. Many of its properties are similar to those of a text field:

  • A text view has text, font, textColor, and textAlignment properties.

  • A text view has attributedText, allowsEditingTextAttributes, and typingAttributes properties, as well as clearsOnInsertion.

  • An editable text view governs its keyboard just as a text field does: when it is first responder, it is being edited and shows the keyboard, and it adopts the UITextInput protocol and has inputView and inputAccessoryView properties.

  • A text view’s menu works the same way as a text field’s.

  • A text view implements drag and drop similarly to a text field.

A text view can be user-editable or not, according to its isEditable property. You can do things with a noneditable text view that you can’t do otherwise, as I’ll explain later. The user can still interact with a noneditable text view’s text, provided its isSelectable property is true; for example, in a selectable noneditable text view, the user can select text and copy it.

A text view is a scroll view, so everything you know about scroll views applies (see Chapter 7). It can be user-scrollable or not. Its contentSize is maintained for you automatically as the text changes, so as to contain the text exactly; thus, if the text view is scrollable, the user can see any of its text. The text view’s delegate (UITextViewDelegate) is its scroll view delegate (UIScrollViewDelegate). A text view has a scrollRangeToVisible(_:) method so that you can scroll in terms of a range of its text.

A text view provides information about, and control of, its selection: it has a selectedRange property which you can get and set.

A text view’s delegate messages (UITextViewDelegate) and notifications are similar to those of a text field. The following delegate methods and notifications should have a familiar ring:

  • textViewShouldBeginEditing(_:)

  • textViewDidBeginEditing(_:)
    UITextView.textDidBeginEditingNotification

  • textViewShouldEndEditing(_:)

  • textViewDidEndEditing(_:)
    UITextView.textDidEndEditingNotification

  • textView(_:shouldChangeTextIn:replacementText:)

Some differences are:

textViewDidChange(_:)
UITextView.textDidChangeNotification

Sent when the user changes text or attributes. A text field has no corresponding delegate method, though the Editing Changed control event is similar.

textViewDidChangeSelection(_:)

Sent when the user changes the selection. In contrast, a text field is officially uninformative about the selection (though you can learn about and manipulate a UITextField’s selection by way of the UITextInput protocol).

Links, Text Attachments, and Data

The default appearance of links in a text view is determined by the text view’s linkTextAttributes. By default, this is a bluish color with no underline, but you can change it. Alternatively, you can apply any desired attributes to the individual links in the attributed string that you set as the text view’s attributedText; in that case, set the linkTextAttributes to an empty dictionary to prevent it from overriding the individual link attributes.

A text view’s delegate can decide how to respond when the user taps on a text attachment or a link. The text view must have its isSelectable property set to true, and its isEditable property set to false:

textView(_:shouldInteractWith:in:interaction:)

The third parameter is a range. The last parameter tells you what the user is doing (UITextItemInteraction): .invokeDefaultAction means tap, .presentActions means long press, .preview means 3D touch. Comes in two forms:

The second parameter is a URL

The user is interacting with a link. The default is true.

The second parameter is an NSTextAttachment

The user is interacting with an inline image. The default is false.

Return true to get a default response, depending on what the user is interacting with and how. By returning false, you can substitute your own response, effectively treating the link or image as a button.

Default responses when the second parameter is a URL are:

.invokeDefaultAction

The URL is opened in Safari.

.presentActions

An action sheet is presented, with menu items Open, Add to Reading List, Copy, and Share.

.preview

A peek and pop preview of the web page is presented, along with menu items Open Link, Add to Reading List, Copy Link, and Share.

Default responses when the second parameter is a text attachment are:

.invokeDefaultAction

Nothing happens.

.presentActions

An action sheet is presented, with menu items Copy Image and Save to Camera Roll.

.preview

Nothing happens, and the event arrives again as .presentActions.

A text view also has a dataDetectorTypes property; this, too, if the text view is selectable but not editable, allows text of these types, specified as a UIDataDetectorTypes bitmask (and presumably located using NSDataDetector), to be treated as tappable links.

textView(_:shouldInteractWith:in:interaction:) will catch these taps as well; the second parameter will be a URL, but it won’t necessarily be much use to you. You can distinguish a phone number through the URL’s scheme (it will be "tel"), and the rest of the URL is the phone number; but other types will be more or less opaque (the scheme is "x-apple-data-detectors"). However, you have the range, so you can obtain the tapped text. Again, you can return false and substitute your own response, or return true for the default responses.

In addition to .link, some common UIDataDetectorTypes are:

.phoneNumber

Default responses are:

.invokeDefaultAction

An alert is presented, with an option to call the number.

.presentActions

An action sheet is presented, with menu items Call, FaceTime, Send Message, Add to Contacts, and Copy.

.preview

A preview is presented, looking up the phone number in the user’s Contacts database, along with menu items Call, Message, Add to Existing Contact, and Create New Contact.

.address

Default responses are:

.invokeDefaultAction

The address is looked up in the Maps app.

.presentActions

An action sheet is presented, with menu items Get Directions, Open in Maps, Add to Contacts, and Copy Address.

.preview

A preview is presented, combining the preceding two.

.calendarEvent

Default responses are:

.invokeDefaultAction

An action sheet is presented, with menu items Create Event, Show in Calendar, and Copy Event.

.presentActions

Same as the preceding.

.preview

A preview is presented, displaying the relevant time from the user’s Calendar, along with the same menu items.

(There are three more data detector types: .shipmentTrackingNumber, .flightNumber, and .lookupSuggestion.)

Self-Sizing Text View

On some occasions, you may want a self-sizing text view — that is, a text view that adjusts its height automatically to embrace the amount of text it contains.

The simplest approach, under autolayout, is to prevent the text view from scrolling by setting its isScrollEnabled to false. The text view now has an intrinsic content size and will behave just like a label (“Resizing a Label to Fit Its Text”). Pin the top and sides of the text view, and the bottom will shift automatically to accomodate the content as the user types. In effect, you’ve made a cross between a label (there are multiple lines and the height adjusts to fit the text) and a text field (the user can edit).

To put a limit on how tall a self-sizing text view can grow, keep track of the height of its contentSize and, if it gets too big, set the text view’s isScrollEnabled to true and constrain its height.

Text View and Keyboard

The fact that a text view is a scroll view comes in handy when the keyboard partially covers a text view. The text view quite often dominates the screen, and you can respond to the keyboard partially covering it by adjusting the text view’s contentInset and scrollIndicatorInsets, exactly as we did earlier in this chapter with a scroll view containing a text field (“Keyboard Covers Text Field”). There is no need to worry about the text view’s contentOffset: the text view will scroll as needed to reveal the insertion point as the keyboard shows, and will scroll itself correctly as the keyboard hides.

How is the keyboard to be dismissed? The Return key is meaningful for character entry, so you won’t want to use it to dismiss the keyboard. On the iPad, there is usually a separate button in the keyboard that dismisses the keyboard, thus solving the problem. On the iPhone, however, there might be no such button.

On the iPhone, the interface might well consist of a text view and the keyboard, which is always showing. Instead of dismissing the keyboard, the user dismisses the entire interface. For example, in Apple’s Mail app on the iPhone, when the user is composing a message, the keyboard is present; if the user taps Cancel or Send, the mail composition interface is dismissed and so is the keyboard.

Alternatively, you can provide interface for dismissing the keyboard explicitly. For example, in Apple’s Notes app, when a note is being edited, the keyboard is present and a Done button appears; the user taps the Done button to dismiss the keyboard. If there’s no good place to put a Done button in the interface, you could attach an accessory view to the keyboard itself, as I did in an earlier example.

Also, being a scroll view, a text view has a keyboardDismissMode. By setting this to .interactive or .onDrag, you can permit the user to hide the keyboard by scrolling or dragging. Apple’s Notes app is a case in point.

Text Kit

Text Kit comes originally from macOS, where you may already be more familiar with it than you realize. For example, much of the text-editing “magic” of Xcode itself is due to Text Kit; and TextEdit is just a thin wrapper around Text Kit. Text Kit comprises a small group of classes that are responsible for drawing text; simply put, they turn an NSAttributedString into graphics. You can take advantage of Text Kit to modify text drawing in ways that were once possible only by dipping down to the low-level C-based world of Core Text.

Text Kit has three chief classes: NSTextStorage, NSLayoutManager, and NSTextContainer. Instances of these three classes join to form a “stack” of objects that allow Text Kit to operate. In the minimal and most common case, a text storage has a layout manager, and a layout manager has a text container, thus forming the “stack.”

Here’s what the three chief Text Kit classes do:

NSTextStorage

A subclass of NSMutableAttributedString. It is, or holds, the underlying text. It has one or more layout managers, and notifies them when the text changes.

By subclassing and delegation (NSTextStorageDelegate), a text storage’s behavior can be modified so that it applies attributes in a custom fashion.

NSLayoutManager

This is the master text drawing class. It has one or more text containers, and is owned by a text storage. It draws the text storage’s text into the boundaries defined by the text container(s).

A layout manager can have a delegate (NSLayoutManagerDelegate), and can be subclassed. This, as you may well imagine, is a powerful and sophisticated class.

NSTextContainer

Owned by a layout manager; helps that layout manager by defining the region in which the text is to be laid out. It does this in three primary ways:

Size

The text container’s top left is the origin for the text layout coordinate system, and the text will be laid out within the text container’s rectangle.

Exclusion paths

The exclusionPaths property consists of UIBezierPath objects within which no text is to be drawn.

Subclassing

By subclassing, you can place each chunk of text drawing anywhere at all (except inside an exclusion path).

Text View and Text Kit

A UITextView provides direct access to the underlying Text Kit engine. It has the following Text Kit–related properties:

textContainer

The text view’s text container (an NSTextContainer instance). UITextView’s designated initializer is init(frame:textContainer:); the textContainer: can be nil to get a default text container, or you can supply your own custom text container.

textContainerInset

The margins of the text container, designating the area within the contentSize rectangle in which the text as a whole is drawn. Changing this value changes the margins immediately, causing the text to be freshly laid out. The default is a top and bottom of 8.

layoutManager

The text view’s layout manager (an NSLayoutManager instance).

textStorage

The text view’s text storage (an NSTextStorage instance).

When you initialize a text view with a custom text container, you hand it the entire “stack” of Text Kit instances, the stack is retained, and the text view is operative. Thus, the simplest case might look like this:

let r = // ... frame for the new text view
let lm = NSLayoutManager()
let ts = NSTextStorage()
ts.addLayoutManager(lm)
let tc = NSTextContainer(size:CGSize(r.width, .greatestFiniteMagnitude))
lm.addTextContainer(tc)
let tv = UITextView(frame:r, textContainer:tc)

Text Container

An NSTextContainer has a size, within which the text will be drawn.

By default, a text view’s text container’s width is the width of the text view, while its height is effectively infinite, allowing the drawing of the text to grow vertically but not horizontally beyond the bounds of the text view, and making it possible to scroll the text vertically.

NSTextContainer also has heightTracksTextView and widthTracksTextView properties, causing the text container to be resized to match changes in the size of the text view — for example, if the text view is resized because of interface rotation. By default, as you might expect, widthTracksTextView is true (the documentation is wrong about this), while heightTracksTextView is false: the text fills the width of the text view, and is laid out freshly if the text view’s width changes, but its height remains effectively infinite. The text view itself configures its own contentSize so that the user can scroll just to the bottom of the existing text.

When you change a text view’s textContainerInset, it modifies its text container’s size as necessary. In the default configuration, this means that it modifies the text container’s width; the top and bottom insets are implemented through the text container’s position within the content rect. Within the text container, additional side margins correspond to the text container’s lineFragmentPadding; the default is 5, but you can change it.

If the text view’s isScrollEnabled is false, then by default its text container’s heightTracksTextView and widthTracksTextView are both true, and the text container size is adjusted so that the text fills the text view. In that case, you can also set the text container’s lineBreakMode. This works like the line break mode of a UILabel. For example, if the line break mode is .byTruncatingTail, then the last line has an ellipsis at the end (if the text is too long for the text view). You can also set the text container’s maximumNumberOfLines, which is like a UILabel’s numberOfLines. In effect, you’ve turned the text view into a label!

But a nonscrolling text view isn’t just a label, because you’ve got access to the Text Kit stack that backs it. For example, you can apply exclusion paths to the text container. Figure 10-13 shows a case in point. The text wraps in longer and longer lines, and then in shorter and shorter lines, because there’s an exclusion path on the right side of the text container that’s a rectangle with a large V-shaped indentation.

pios 2302a
Figure 10-13. A text view with an exclusion path

In Figure 10-13, the text view (self.tv) is initially configured in the view controller’s viewDidLoad:

self.tv.attributedText = // ...
self.tv.textContainerInset =
    UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 0)
self.tv.isScrollEnabled = false

The exclusion path is then drawn and applied in viewDidLayoutSubviews:

override func viewDidLayoutSubviews() {
    let sz = self.tv.textContainer.size
    let p = UIBezierPath()
    p.move(to: CGPoint(sz.width/4.0,0))
    p.addLine(to: CGPoint(sz.width,0))
    p.addLine(to: CGPoint(sz.width,sz.height))
    p.addLine(to: CGPoint(sz.width/4.0,sz.height))
    p.addLine(to: CGPoint(sz.width,sz.height/2.0))
    p.close()
    self.tv.textContainer.exclusionPaths = [p]
}

You can also subclass NSTextContainer to modify the rectangle in which the layout manager wants to position a piece of text. (Each piece of text is actually a line fragment; I’ll explain in the next section what a line fragment is.) In Figure 10-14, the text is inside a circle.

pios 2302a2
Figure 10-14. A text view with a subclassed text container

To achieve the layout shown in Figure 10-14, I set the attributed string’s line break mode to .byCharWrapping (to bring the right edge of each line as close as possible to the circular shape), and construct the Text Kit stack by hand to include an instance of my NSTextContainer subclass:

let r = self.tv.frame
let lm = NSLayoutManager()
let ts = NSTextStorage()
ts.addLayoutManager(lm)
let tc = MyTextContainer(size:CGSize(r.width, r.height))
lm.addTextContainer(tc)
let tv = UITextView(frame:r, textContainer:tc)

Here’s my NSTextContainer subclass; it overrides just one property and one method, to dictate the rect of each line fragment:

class MyTextContainer : NSTextContainer {
    override var isSimpleRectangularTextContainer : Bool { return false }
    override func lineFragmentRect(forProposedRect proposedRect: CGRect,
        at characterIndex: Int,
        writingDirection baseWritingDirection: NSWritingDirection,
        remaining remainingRect: UnsafeMutablePointer<CGRect>?) -> CGRect {
            var result = super.lineFragmentRect(
                forProposedRect:proposedRect, at:characterIndex,
                writingDirection:baseWritingDirection,
                remaining:remainingRect)
            let r = self.size.height / 2.0
            // convert initial y so that circle is centered at origin
            let y = r - result.origin.y
            let theta = asin(y/r)
            let x = r * cos(theta)
            // convert resulting x from circle centered at origin
            let offset = self.size.width / 2.0 - r
            result.origin.x = r-x+offset
            result.size.width = 2*x
            return result
    }
}

Alternative Text Kit Stack Architectures

The default Text Kit stack is one text storage, which has one layout manager, which has one text container. But a text storage can have multiple layout managers, and a layout manager can have multiple text containers. What’s that all about?

Multiple text containers

If one layout manager has multiple text containers, the overflow from each text container is drawn in the next one. For example, in Figure 10-15, there are two text views; the text has filled the first text view, and has then continued by flowing into and filling the second text view. As far as I can tell, the text views can’t be made editable in this configuration; but clearly this is a way to achieve a multicolumn or multipage layout, or you could use text views of different sizes for a magazine-style layout.

pios 2302b
Figure 10-15. A layout manager with two text containers

It is possible to achieve that arrangement by disconnecting the layout managers of existing text views from their text containers and rebuilding the stack from below. In this example, though, I’ll build the entire stack by hand:

let r = // frame
let r2 = // frame
let mas = // content
let ts1 = NSTextStorage(attributedString:mas)
let lm1 = NSLayoutManager()
ts1.addLayoutManager(lm1)
let tc1 = NSTextContainer(size:r.size)
lm1.addTextContainer(tc1)
let tv = UITextView(frame:r, textContainer:tc1)
let tc2 = NSTextContainer(size:r2.size)
lm1.addTextContainer(tc2)
let tv2 = UITextView(frame:r2, textContainer:tc2)

Multiple layout managers

If one text storage has multiple layout managers, then each layout manager is laying out the same text. For example, in Figure 10-16, there are two text views displaying the same text. The remarkable thing is that if you edit one text view, the other changes to match. (That’s how Xcode lets you edit the same code file in different windows, tabs, or panes.)

pios 2302c
Figure 10-16. A text storage with two layout managers

Again, this arrangement is probably best achieved by building the entire text stack by hand:

let r = // frame
let r2 = // frame
let mas = // content
let ts1 = NSTextStorage(attributedString:mas)
let lm1 = NSLayoutManager()
ts1.addLayoutManager(lm1)
let lm2 = NSLayoutManager()
ts1.addLayoutManager(lm2)
let tc1 = NSTextContainer(size:r.size)
let tc2 = NSTextContainer(size:r2.size)
lm1.addTextContainer(tc1)
lm2.addTextContainer(tc2)
let tv = UITextView(frame:r, textContainer:tc1)
let tv2 = UITextView(frame:r2, textContainer:tc2)

Layout Manager

The first thing to know about a layout manager is the geometry in which it thinks. To envision a layout manager’s geometrical world, think in terms of glyphs and line fragments:

Glyph

The drawn analog of a character. The correspondence is not one-to-one; multiple glyphs can correspond to one character, and multiple characters can correspond to one glyph. The layout manager’s job is to translate the characters into glyphs, get those glyphs from a font, and draw them.

Line fragment

A rectangle in which glyphs are drawn, one after another. (The reason it’s a line fragment, and not simply a line, is that a line might be interrupted by the text container’s exclusion paths.)

A glyph has a location in terms of the line fragment into which it is drawn. A line fragment’s coordinates are in terms of the text container. The layout manager can convert between these coordinate systems, and between text and glyphs. Given a range of text in the text storage, it knows where the corresponding glyphs are drawn in the text container. Conversely, given a location in the text container, it knows what glyph is drawn there and what range of text in the text storage that glyph represents.

What’s missing from that geometry is what, if anything, the text container corresponds to in the real world. A text container is not, itself, a real rectangle in the real world; it’s just a class that tells the layout manager a size to draw into. Making that rectangle meaningful for drawing purposes is up to some other class outside the Text Kit stack.

A UITextView, for example, has a text container, which it shares with a layout manager. The text view knows how its own content is scrolled and how the rectangle represented by its text container is inset within that scrolling content. The layout manager, however, doesn’t know anything about that; it sees the text container as a purely theoretical rectangular boundary. Only when the layout manager actually draws does it make contact with the real world of some graphics context — and it must be told, on those occasions, how the text container’s rectangle is offset within that graphics context.

To illustrate, consider a text view scrolled so as to place some word at the top left of its visible bounds. I’ll use the layout manager to learn what word it is.

I can ask the layout manager what character or glyph corresponds to a certain point in the text container, but what point should I ask about? Translating from the real world to text container coordinates is up to me; I must take into account both the scroll position of the text view’s content and the inset of the text container within that content:

let off = self.tv.contentOffset
let top = self.tv.textContainerInset.top
let left = self.tv.textContainerInset.left
var tctopleft = CGPoint(off.x - left, off.y - top)

Now I’m speaking in text container coordinates, which are layout manager coordinates. One possibility is then to ask directly for the index (in the text storage’s string) of the corresponding character:

let ixx = self.tv.layoutManager.characterIndex(for:tctopleft,
    in:self.tv.textContainer,
    fractionOfDistanceBetweenInsertionPoints:nil)

That, however, does not give quite the results one might intuitively expect. If any of a word is poking down from above into the visible area of the text view, that is the word whose first character is returned. I think we intuitively expect, if a word isn’t fully visible, that the answer should be the word that starts the next line, which is fully visible. So I’ll modify that code in a simpleminded way. I’ll obtain the index of the glyph at my initial point; from this, I can derive the rect of the line fragment containing it. If that line fragment is not at least three-quarters visible, I’ll add one line fragment height to the starting point and derive the glyph index again. Then I’ll convert the glyph index to a character range:

var ix = self.tv.layoutManager.glyphIndex(for:tctopleft,
    in:self.tv.textContainer, fractionOfDistanceThroughGlyph:nil)
let frag = self.tv.layoutManager.lineFragmentRect(
    forGlyphAt:ix, effectiveRange:nil)
if tctopleft.y > frag.origin.y + 0.5*frag.size.height {
    tctopleft.y += frag.size.height
    ix = self.tv.layoutManager.glyphIndex(for:tctopleft,
        in:self.tv.textContainer, fractionOfDistanceThroughGlyph:nil)
}
let charRange = self.tv.layoutManager.characterRange(
    forGlyphRange: NSMakeRange(ix,0), actualGlyphRange:nil)

Finally, I’ll use NLTokenizer (import NaturalLanguage) to get the range of the entire word to which this character belongs:

let tokenizer = NLTokenizer(unit: .word)
tokenizer.setLanguage(.english)
let text = self.tv.text!
tokenizer.string = text
let range = NSMakeRange(charRange.location, 100)
let words = tokenizer.tokens(for: Range(range, in:text)!)
if let word = words.first {
    print(text[word])
}

Clearly, the same sort of technique could be used to formulate a custom response to a tap — answering the question, “What word did the user just tap on?”

By subclassing NSLayoutManager (and by implementing its delegate), many powerful effects can be achieved. As a simple example, I’ll carry on from the preceding code by drawing a rectangular outline around the word we just located. To make this possible, I have an NSLayoutManager subclass, MyLayoutManager, an instance of which is built into the Text Kit stack for this text view. MyLayoutManager has a public NSRange property, wordRange. Having worked out what word I want to outline, I set the layout manager’s wordRange and invalidate its drawing of that word, to force a redraw:

let range = NSRange(word, in:text)
let lm = self.tv.layoutManager as! MyLayoutManager
lm.wordRange = range
lm.invalidateDisplay(forCharacterRange:range)

In MyLayoutManager, I’ve overridden the method that draws the background behind glyphs, drawBackground(forGlyphRange:at:). At the moment this method is called, there is already a graphics context, so all we have to do is draw.

First, I call super. Then, if the range of glyphs to be drawn includes the glyphs for the range of characters in self.wordRange, I ask for the rect of the bounding box of those glyphs, and stroke it to form the rectangle. As I mentioned earlier, the bounding box is in text container coordinates, but now we’re drawing in the real world, so I have to compensate by offsetting the drawn rectangle by the same amount that the text container is supposed to be offset in the real world; fortunately, the text view tells us (through the origin: parameter) what that offset is:

override func drawBackground(forGlyphRange glyphsToShow: NSRange,
    at origin: CGPoint) {
        super.drawBackground(forGlyphRange:glyphsToShow, at:origin)
        if self.wordRange.length == 0 {
            return
        }
        var range = self.glyphRange(forCharacterRange:self.wordRange,
            actualCharacterRange:nil)
        range = NSIntersectionRange(glyphsToShow, range)
        if range.length == 0 {
            return
        }
        if let tc = self.textContainer(forGlyphAt:range.location,
            effectiveRange:nil, withoutAdditionalLayout:true) {
                var r = self.boundingRect(forGlyphRange:range, in:tc)
                r.origin.x += origin.x
                r.origin.y += origin.y
                let c = UIGraphicsGetCurrentContext()!
                c.saveGState()
                c.setStrokeColor(UIColor.black.cgColor)
                c.setLineWidth(1.0)
                c.stroke(r)
                c.restoreGState()
        }
}

Text Kit Without a Text View

UITextView is the only built-in iOS class that has a Text Kit stack to which you are given programmatic access. But that doesn’t mean it’s the only place where you can draw with Text Kit! You can draw with Text Kit anywhere you can draw — that is, in any graphics context (Chapter 2). When you do so, you should always call both drawBackground(forGlyphRange:at:) (the method I overrode in the previous example) and drawGlyphs(forGlyphRange:at:), in that order. The at: argument is the point where you consider the text container’s origin to be within the current graphics context.

To illustrate, I’ll change the implementation of the StringDrawer class that I described earlier in this chapter. Previously, StringDrawer’s draw(_:) implementation told the attributed string (self.attributedText) to draw itself:

override func draw(_ rect: CGRect) {
    let r = rect.offsetBy(dx: 0, dy: 2)
    let opts : NSStringDrawingOptions = .usesLineFragmentOrigin
    self.attributedText.draw(with:r, options: opts, context: context)
}

Instead, I’ll construct the Text Kit stack and tell its layout manager to draw the text:

override func draw(_ rect: CGRect) {
    let lm = NSLayoutManager()
    let ts = NSTextStorage(attributedString:self.attributedText)
    ts.addLayoutManager(lm)
    let tc = NSTextContainer(size:rect.size)
    lm.addTextContainer(tc)
    tc.lineFragmentPadding = 0
    let r = lm.glyphRange(for:tc)
    lm.drawBackground(forGlyphRange:r, at:CGPoint(0,2))
    lm.drawGlyphs(forGlyphRange: r, at:CGPoint(0,2))
}

Building the entire Text Kit stack by hand may seem like overkill for that simple example, but imagine what else I could do now that I have access to the entire Text Kit stack! I can use properties, subclassing, delegation, and alternative stack architectures to achieve customizations and effects that, before Text Kit was migrated to iOS, were difficult or impossible to achieve without dipping down to the level of Core Text.

For example, the two-column display of U.S. state names on the iPad shown in Figure 10-17 was a Core Text example in early editions of this book, requiring 50 or 60 lines of elaborate C code, complicated by the necessity of flipping the context to prevent the text from being drawn upside-down. Nowadays, it can be achieved easily through Text Kit — effectively just by reusing code from earlier examples in this chapter.

pios 2307
Figure 10-17. Two-column text in small caps

Furthermore, the example from previous editions went on to describe how to make the display of state names interactive, with the name of the tapped state briefly outlined with a rectangle (Figure 10-18). With Core Text, this was almost insanely difficult, not least because we had to keep track of all the line fragment rectangles ourselves. But it’s easy with Text Kit, because the layout manager knows all the answers.

pios 2308
Figure 10-18. The user has tapped on California

We have a UIView subclass, StyledText. In its layoutSubviews, it creates the Text Kit stack — a layout manager with two text containers, to achieve the two-column layout — and stores the whole stack, along with the rects at which the two text containers are to be drawn, in properties:

override func layoutSubviews() {
    super.layoutSubviews()
    var r1 = self.bounds
    r1.origin.y += 2 // a little top space
    r1.size.width /= 2.0 // column 1
    var r2 = r1
    r2.origin.x += r2.size.width // column 2
    let lm = MyLayoutManager()
    let ts = NSTextStorage(attributedString:self.text)
    ts.addLayoutManager(lm)
    let tc = NSTextContainer(size:r1.size)
    lm.addTextContainer(tc)
    let tc2 = NSTextContainer(size:r2.size)
    lm.addTextContainer(tc2)
    self.lm = lm; self.ts = ts; self.tc = tc; self.tc2 = tc2
    self.r1 = r1; self.r2 = r2
}

Our draw(_:) is just like the previous example, except that we have two text containers to draw:

override func draw(_ rect: CGRect) {
    let range1 = self.lm.glyphRange(for:self.tc)
    self.lm.drawBackground(forGlyphRange:range1, at: self.r1.origin)
    self.lm.drawGlyphs(forGlyphRange:range1, at: self.r1.origin)
    let range2 = self.lm.glyphRange(for:self.tc2)
    self.lm.drawBackground(forGlyphRange:range2, at: self.r2.origin)
    self.lm.drawGlyphs(forGlyphRange:range2, at: self.r2.origin)
}

So much for drawing the text! We now have Figure 10-17.

On to Figure 10-18. When the user taps on our view, a tap gesture recognizer’s action method is called. We are using the same layout manager subclass developed in the preceding section of this chapter: it draws a rectangle around the glyphs corresponding to the characters of its wordRange property. Thus, all we have to do in order to make the flashing rectangle around the tapped word is work out what that range is, set our layout manager’s wordRange property and redraw ourselves, and then (after a short delay) set the wordRange property back to a zero range and redraw ourselves again to remove the rectangle.

We start by working out which column the user tapped in; this tells us which text container it is, and what the tapped point is in text container coordinates (g is the tap gesture recognizer):

var p = g.location(in:self)
var tc = self.tc!
if !self.r1.contains(p) {
    tc = self.tc2!
    p.x -= self.r1.size.width
}

Now we can ask the layout manager what glyph the user tapped on, and hence the whole range of glyphs within the line fragment the user tapped in. If the user tapped to the left of the first glyph or to the right of the last glyph, no word was tapped, and we return:

var f : CGFloat = 0
let ix =
    self.lm.glyphIndex(for:p, in:tc, fractionOfDistanceThroughGlyph:&f)
var glyphRange : NSRange = NSMakeRange(0,0)
self.lm.lineFragmentRect(forGlyphAt:ix, effectiveRange:&glyphRange)
if ix == glyphRange.location && f == 0.0 {
    return
}
if ix == glyphRange.location + glyphRange.length - 1 && f == 1.0 {
    return
}

If the last glyph of the line fragment is a whitespace glyph, we don’t want to include it in our rectangle, so we subtract it from the end of our range. Then we’re ready to convert to a character range, and thus we can learn the name of the state that the user tapped on:

func lastCharIsControl () -> Bool {
    let lastCharRange = glyphRange.location + glyphRange.length - 1
    let property = self.lm.propertyForGlyph(at:lastCharRange)
    return property.contains(.controlCharacter)
}
while lastCharIsControl() {
    glyphRange.length -= 1
}
let characterRange =
    self.lm.characterRange(forGlyphRange:glyphRange, actualGlyphRange:nil)
let s = self.text.string
if let r = Range(characterRange, in:s) {
    let stateName = s[r]
    print("you tapped (stateName)")
}

Finally, we flash the rectangle around the state name by setting and resetting the wordRange property of the subclassed layout manager:

let lm = self.lm as! MyLayoutManager
lm.wordRange = characterRange
self.setNeedsDisplay()
delay(0.3) {
    lm.wordRange = NSMakeRange(0, 0)
    self.setNeedsDisplay()
}
..................Content has been hidden....................

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