22. Introduction to TextKit

Both the iPhone and, later, the iPad have supported numerous text presentation elements from their inception. Text fields, labels, text views, and Web views have been with the OS since its release. Over time these classes have been expanded and improved with the goal of giving developers more flexibility and power with regard to text rendering.

In the early days of iOS (then called iPhone OS), the only practical way to display attributed text was to use a UIWebView and use HTML to render custom attributes; however, this was difficult to implement and carried with it terrible performance. iOS 3.2 introduced Core Text, which brought the full power of NSAttributedString to the mobile platform from the Mac. Core Text, however, was complex and unwieldy and was largely shunned by developers who were not coming from the Mac or did not have an abundance of time to invest in text rendering for their apps.

Enter TextKit. First announced as part of iOS 7, TextKit is not a framework in the traditional sense. Instead, TextKit is the nomenclature for a set of enhancements to existing text-displaying objects to easily render and work with attributed strings. Although TextKit adds several new features and functionalities beyond what Core Text offered, a lot of that functionality is re-created in TextKit, albeit in a much simpler-to-work-with fashion. Existing Core Text code likewise is easily portable to TextKit, often needing no changes or only very minor changes through the use of toll-free bridges.

An introduction to TextKit is laid out over the following pages. It will demonstrate some of the basic principles of text handling on iOS 7; however, working with text on modern devices is a vast topic, worthy of its own publication. Apple has put considerable time and effort into making advanced text layout and rendering easier than it has ever been in the past. The techniques and tools described will provide a stepping-stone into a world of virtually limitless text presentation.

The Sample App

The sample app (shown in Figure 22.1) is a simple table view–based app that will enable the user to explore four popular features of TextKit. There is little overhead for the sample app not directly related to working with the new TextKit functionality. It consists of a main view built on a UINavigationController and a table view that offers the selection of one of four items. The sample app provides demos for Dynamic Link Detection, which will automatically detect and highlight various data types; Hit Detection, which enables the user to select a word from a UITextView; and Content Specific Highlighting, which demos TextKit’s capability to work with attributed strings. Lastly, the sample app exhibits Exclusion Paths, which offers the capability to wrap text around objects or Bézier paths.

Image

Figure 22.1 A look at the sample app showing a table view with options for different TextKit functionalities.

Introducing NSLayoutManager

NSLayoutManager was first introduced as part of the TextKit additions in iOS 7. It can be used to coordinate the layout and display of characters held in an NSTextStore, which is covered in the following section. NSLayoutManager can be used to render multiple NSTextViews together to create a complex text layout. NSLayoutManager contains numerous classes for adding, removing, aligning, and otherwise working with NSTextContainer, which are covered more in depth in a later section.

NSTextStore

Each NSLayoutManager has an associated NSTextStorage that acts as a subclass of NSMutableAttributedString. Readers familiar with Core Text or Mac OS X text rendering might be familiar with an attributed string, which is used for storage of stylized text. An NSTextStorage provides an easy-to-interact-with wrapper for easily adding and removing attributes from text.

NSTextStorage can be used with setAttributes:range: to add new attributes to a string; for a list of attributes see Table 22.1. Polling the text for currently enabled attributes can be done using attributesAtIndex:effectiveRange:.

Image
Image
Image

Table 22.1 Available Text Attributes

NSLayoutManagerDelegate

NSLayoutManager also has an associated delegate that can be used to handle how the text is rendered. One of the most useful sets of methods deals with the handling of line fragments that can be used to specify exactly how the line and paragraphs break. Additionally, methods are available when the text has finished rendering.

NSTextContainer

The NSTextContainer is another important new addition to iOS 7’s TextKit. An NSTextContainer defines a region in which text is laid out; NSLayoutManagers discussed in the preceding section can control multiple NSTextContainers. NSTextContainers have support for number of lines, text wrapping, and resizing in a text view. Additional support for exclusion paths is discussed later in the section “Exclusion Paths.”

Detecting Links Dynamically

Dynamic Link Detection is extremely easy to implement and provides a great user experience if the user is working with addresses, URLs, phone numbers, or dates in a text view. The easiest way to turn on these properties is through Interface Builder (shown in Figure 22.2).

Image

Figure 22.2 Dynamic Link Detection controls in Xcode 6.

These properties can also be toggled on and off using code.

[textView setDataDetectorTypes: UIDataDetectorTypePhoneNumber | UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent];

TexKit added a new delegate method as part of UITextViewDelegate to intercept the launching of events. The following example detects the launch URL event on a URL and provides an alert to the user:

- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange
{
    toBeLaunchedURL = URL;

    if([[URL absoluteString] hasPrefix:@"http://"])
    {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"URL Launching" message:[NSString stringWithFormat:@"About to launch %@", [URL absoluteString]] delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Launch", nil];

        [alert show];
        return NO;
    }

    return YES;
}

Detecting Hits

Hit detection has traditionally been complex to implement and often required for elaborate text-driven apps. TextKit added support for per-character hit detection. To support this functionality, a subclassed UITextView is created, called ICFCustomTextView in the sample project. The UITextView implements a touchesBegan: event method.

When a touch begins, the location in the view is captured and it is adjusted down the y axis by ten to line up with the text elements. A method is invoked on the layoutManager that is a property of the text view, characterIndexForPoint: inTextContainer: fractionOfDistanceBetweenInsertionPoints:. This returns the index of the character that was selected.

After the character index has been determined, the beginning and end of the word that it is contained within are calculated by searching forward and backward for the next whitespace character. The full word is then displayed in a UIAlertView to the user.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint touchPoint = [touch locationInView:self];

    touchPoint.y -= 10;

    NSInteger characterIndex = [self.layoutManager characterIndexForPoint:touchPoint inTextContainer:self.textContainer fractionOfDistanceBetweenInsertionPoints:0];

    if(characterIndex != 0)
    {
        NSRange start = [self.text rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet] options:NSBackwardsSearch range:NSMakeRange(0,characterIndex)];

        NSRange stop = [self.text rangeOfCharacterFromSet: [NSCharacterSet whitespaceAndNewlineCharacterSet] options:NSCaseInsensitiveSearch range:NSMakeRange(characterIndex,self.text.length- characterIndex)];

        int length =  stop.location - start.location;

        NSString *fullWord = [self.text substringWithRange:NSMakeRange (start.location, length)];


        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Selected Word" message:fullWord delegate:nil cancelButtonTitle:@"Dismiss" otherButtonTitles: nil];

        [alert show];
    }

    [super touchesBegan: touches withEvent: event];
}

Exclusion Paths

Exclusion Paths (shown in Figure 22.3) enable text to wrap around images or other objects that appear inline. TextKit added a simple property in order to add an exclusion path to any text container.

Image

Figure 22.3 Text wrapping around a UIImage using iOS 7’s exclusion paths.

To specify an exclusion path, a UIBezierPath representing the area to be excluded is first created. To set an exclusion path, an array of the avoided areas is passed to the exclusionPaths property of a textContainer. The text container can be found as a property of the UITextView.

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIBezierPath *circle = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(110, 100, 100, 102)];

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(110, 110, 100, 102)];

    [imageView setImage: [UIImage imageNamed: @"DF.png"]];
    [imageView setContentMode:UIViewContentModeScaleToFill];
    [self.myTextView addSubview: imageView];

    self.myTextView.textContainer.exclusionPaths = @[circle];
}

Content Specific Highlighting

One of the most interesting features of TextKit is Content Specific Highlighting. Before iOS 7, using CoreText to modify the appearance of specific strings inside of a text view was elaborate and cumbersome. TextKit brings many improvements to rich text rendering and definition.

To work with custom attributed text, a subclass of an NSTextStorage is created, called ICFDynamicTextStorage in the sample project. This approach will enable the developer to set tokens for different attributed strings to be rendered per string encountered. A classwide NSMutableAttributedString is created, which will hold on to all the associated attributes for the displayed text.

- (id)init
{
    self = [super init];

    if (self)
    {
        backingStore = [[NSMutableAttributedString alloc] init];
    }

    return self;
}

A convenience method for returning the string is also created, as well as one for returning the attributes at an index.

- (NSString *)string
{
    return [backingStore string];
}

- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
{
    return [backingStore attributesAtIndex:location effectiveRange:range];
}

The next four methods deal with the actual inputting and setting of attributes, from replacing the characters to making sure that text is being properly updated.

- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
{
    [self beginEditing];
    [backingStore replaceCharactersInRange:range withString:str];

    [self edited:NSTextStorageEditedCharacters| NSTextStorageEditedAttributes range:range changeInLength:str.length - range.length];

    textNeedsUpdate = YES;
    [self endEditing];
}

- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range
{
    [self beginEditing];
    [backingStore setAttributes:attrs range:range];

    [self edited:NSTextStorageEditedAttributes range:range changeInLength:0];

    [self endEditing];
}

- (void)performReplacementsForCharacterChangeInRange: (NSRange)changedRange
{
    NSRange extendedRange = NSUnionRange(changedRange, [[self string] lineRangeForRange:NSMakeRange(changedRange.location, 0)]);

    extendedRange = NSUnionRange(changedRange, [[self string] lineRangeForRange:NSMakeRange(NSMaxRange(changedRange), 0)]);

    [self applyTokenAttributesToRange:extendedRange];
}

-(void)processEditing
{
    if(textNeedsUpdate)
    {
        textNeedsUpdate = NO;
        [self performReplacementsForCharacterChangeInRange:[self editedRange]];
    }

    [super processEditing];
}

The last method in the subclassed NSTextStore applies the actual tokens that will be set using a property on the NSTextStore to the string. The tokens are passed as an NSDictionary, which defines the substring they should be applied for. When the substring is detected using the enumerateSubstringsInRange: method, the attribute is applied using the previous addAttribute:range: method. This system also allows for default tokens to be set when a specific attribute has not been set.

- (void)applyTokenAttributesToRange:(NSRange)searchRange
{
    NSDictionary *defaultAttributes = [self.tokens objectForKey:defaultTokenName];

    [[self string] enumerateSubstringsInRange:searchRange options:NSStringEnumerationByWords usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop)
    {
        NSDictionary *attributesForToken = [self.tokens objectForKey:substring];

        if(!attributesForToken)
        {
            attributesForToken = defaultAttributes;
        }

        [self addAttributes:attributesForToken range:substringRange];

    }];
}

After the subclass of NSTextStore is created, modifying text itself becomes fairly trivial, the results of which are shown in Figure 22.4. A new instance of the customized text store is allocated and initialized, followed by a new instance of NSLayoutManager, and lastly an NSTextContainer is created. The text container is set to share its frame and bounds with the text view, and is then added to the layoutManager. The text store then adds the layout manager.

Image

Figure 22.4 Content Specific Highlighting showing updated attributes for several keywords.

A new NSTextView is created and set to the frame of the view, and its text container is set to the previously created one. Next, the auto-resizing mask for the text view is configured to be scalable for screen sizes and other adjustments. Finally, scrolling and keyboard behavior for the text view are configured, and the text view is added as a subview of the main view.

The tokens property of the customized text field is used to set a dictionary of dictionaries for the attributes to be assigned to each substring encountered. The first example, Mary, will set the NSForegroundColorAttributeName attribute to red. A complete list of attributes was given earlier, in Table 22.1. The sample demonstrates multiple types of attributes on various keywords. The example for was shows how to add multiple attributes together using a custom font, color, and underlining the text. A default token is also set that specifies how text not specifically assigned will be displayed.

After the attributes have been set, some static text is added to the text view in the form of the poem “Mary Had a Little Lamb”; the resulting attributed text appears in Figure 22.4. Typing into the text view will update the attributes in real time and can be seen by typing out any of the substrings in which special attributes were configured.

- (void)viewDidLoad
{
    [super viewDidLoad];

    ICFDynamicTextStorage *textStorage = [[ICFDynamicTextStorage alloc] init];

    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];

    NSTextContainer *container = [[NSTextContainer alloc] initWithSize:CGSizeMake(myTextView.frame.size.width, CGFLOAT_MAX)];

    container.widthTracksTextView = YES;
    [layoutManager addTextContainer:container];
    [textStorage addLayoutManager:layoutManager];

    myTextView = [[UITextView alloc] initWithFrame:self.view.frame textContainer:container];

    myTextView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;

    myTextView.scrollEnabled = YES;

    myTextView.keyboardDismissMode =
    UIScrollViewKeyboardDismissModeOnDrag;

    [self.view addSubview:myTextView];

    textStorage.tokens = @{ @"Mary":@{ NSForegroundColorAttributeName: [UIColor redColor]}, @"lamb":@{ NSForegroundColorAttributeName:[UIColor blueColor]}, @"everywhere":@{ NSUnderlineStyleAttributeName:@1}, @"that":@{NSBackgroundColorAttributeName : [UIColor yellowColor]}, @"fleece":@{NSFontAttributeName:[UIFont fontWithName:@"Chalkduster" size:14.0f]}, @"school":@{NSStrikethroughStyleAttributeName:@1}, @"white":@{NSStrokeWidthAttributeName:@5}, @"was":@{NSFontAttributeName:[UIFont fontWithName:@"Palatino-Bold" size:10.0f], NSForegroundColorAttributeName:[UIColor purpleColor], NSUnderlineStyleAttributeName:@1}, defaultTokenName:@{ NSForegroundColorAttributeName : [UIColor blackColor], NSFontAttributeName: [UIFont systemFontOfSize:14.0f], NSUnderlineStyleAttributeName : @0, NSBackgroundColorAttributeName : [UIColor whiteColor], NSStrikethroughStyleAttributeName : @0, NSStrokeWidthAttributeName : @0}};

    NSString *maryText = @"Mary had a little lamb whose fleece was white as snow. And everywhere that Mary went, the lamb was sure to go. It followed her to school one day which was against the rule. It made the children laugh and play, to see a lamb at school.";

    [myTextView setText:[NSString stringWithFormat:@"%@ %@ %@", maryText, maryText, maryText]];
}

Changing Font Settings with Dynamic Type

TextKit brings support for Dynamic Type, which enables the user to specify a font size at an OS level. Users can access the Dynamic Type controls under the General section of iOS 8’s Settings.app (shown in Figure 22.5). When the user changes the preferred font size, the app will receive a notification named UIContentSizeCategoryDidChangeNotification. This notification should be monitored to handle updating the font size.

Image

Figure 22.5 Changing the systemwide font size using Dynamic Type settings in iOS 7’s Settings app.

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferredSizeDidChange:) name:UIContentSizeCategoryDidChangeNotification object:nil];

To display text at the user’s preferred font settings, the font should be set using one of the attributes from Font Text Styles, which are described in Table 22.2.

self.textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];

Image

Table 22.2 Font Text Styles as Defined in iOS 7

This returns a properly sized font based on the user settings.

Summary

Text rendering on iOS is a deep and complex topic made vastly easier with the introduction of TextKit. This chapter merely broke the surface of what is possible with TextKit and text rendering on the iOS platform in general. Hopefully, it has created a topic not nearly as intimidating as text render has been in the past.

Several examples were explored in this chapter, from hit detection to working with attributed strings. In addition, the building blocks that make up text rendering objects should now be much clearer. Although text rendering is a vast topic, worthy of its own dedicated book, the information in this chapter should provide a strong foot forward.

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

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