Frameworks • iOS • 49:59
Move beyond the basics and unlock the full power of Text Kit for advanced text handling in your apps. Understand how to use hit detection and pixel-perfect layout information for responding to user touches. Discover new text effects, including a sophisticated letterpress look, and dive deeper into the mechanics of Text Kit for displaying multi-page documents and custom layouts.
Speakers: Jordan Breeding, Peter Hajas, Aki Inoue
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
Good afternoon. This is Session 220. Advanced Text Layouts and Effects with Text Kit. I'm Aki Inoue, a senior text architect at Apple. Yesterday, Ian, Johannes and Jordan introduced Text Kit, a new technology in iOS 7. Today in this session, we're going to discover that you have broader and finer controls over your text through this technology.
Let's get started. These are the items we're covering today. We're going to start with text effects. As you've seen earlier in this conference, iOS 7 supports gorgeous letterpress text effects. You're going to learn how to integrate this effect in the application. Next, we're going to cover the three main Text Kit objects, NSLayoutManager, NSTextContainer and NSTextStorage, the power, the typographic enhancements in iOS 7. There are many advanced text layout features such as multiple page documents that are integral to the Text Kit architecture. Then we're going to dive deeper into NSLayoutManager. You're going to learn what you can do with some rich text layout information available to your applications.
And finally, I'm going to cover a some of the customization point provided for your applications. Of course, order of the material in this session brand new to iOS 7. Now, I would like to bring my colleague, Peter Hajas, who's going to tell you how to spice up your application, what is the cool text effects. Peter.
Good afternoon. My name is Peter Hajas. I'm a UIKit engineer. And today, I'd like to talk to you about text effects. Text effects are a beautiful new graphical effect we've added for your text in your application. And in iOS 7, we're introducing a beautiful, gorgeous letterpress effect.
In case you're not familiar, letterpress is a form of relief printing by which texts and images are pressed into the page. It's absolutely stunning. Now prior to iOS 7, if you wanted to create this effect programmatically, it was extraordinarily difficult for you to duplicate the shadows, the embossing, this beautiful, beautiful effect for your text. But now, in iOS 7, we're exposing this to you through one attributed string key and a new attributed string value. That key is NSTextEffectAttributeName.
And that value is NSTextEffectLetterpressStyle. Now, if you've used attributed strings before, and I hope you have, this works with all the standard NS attributed string keys that you're familiar with, like ForegroundColorAttributeName and others. Letterpress is a complex graphical effect and you should use it tastefully. If you look at our system applications, like reminders and notes, you'll notice that we use it in those applications for the title of a reminder's list or the title of a notes document.
Now, I'd like to give you a quick overview of the main Text Kit classes. This is a review of what you saw at the Intro to Text Kit session yesterday. In Text Kit, there are three classes that we use to represent the text in your application and turn it into glyphs which users will see on screen.
Text Storage which provides the backing store for the text in your application. Layout Manager which is in charge of how that text gets turned into glyphs and has override points for you to customize it. And Text Container, which describes the geometry about which we flow lines and line fragments in your text view.
Let's start with Text Storage. NSTextStorage is a mutable attributed string subclass. So it supports all the standard attributed string, keys and values that you're customary with including letterpress. NSTextStorage is versatile enough for a short document, like a brief note or message, and a document as long as War and Peace, so it should be totally perfect for your needs. If, however, you find the need to subclass Text Storage, you'll need to override all the primitive attributed string and mutable attributed string API.
Next, NSLayoutManager. If NSTextStorage is the thing that controls the backing store for your text and all your Unicode characters and attributes and things like that, Layout Manager is the object that translates that text into glyphs on screen. Now if you'd like to override that layout process, you can do so through delegation and this is how you can accomplish advanced layout techniques such as, for example, folding lines or other advanced text rendering. And finally, NSTextContainer.
Text Container represents one area on the display in which you'd like to draw a text. It's not the object that does the actual drawing, that's up to your text view. Instead Text Container describes the geometry about which we draw. If you were in the introduction session yesterday, you saw just how easy our declarative exclusion path support is, and this is built on Text Container. Now, as we'll see in a little bit, Text Container represents one area on which you draw a text. So if you'd like multiple pages or multiple columns for each of those pages, columns or other areas in which you flow text, you'll need an additional Text Container.
In their most basic configuration, you'll have one of each of these objects. One Text Storage providing that backing store for your document. One Layout Manager controlling how that text gets turned into glyphs on screen. And one Text Container describing the geometry of your lines. But let's say you want to do something a little more intricate like a multi-columnar or multi-page layout, that's fine. Text Kit supports this right out of the box. Just use multiple Text Containers, attached to the same Layout Manager, backed by the same Text Storage document. This is how you'd get multi-page support and multi-columnar support.
Now, let's say you want to go really advanced and you want a different layout for your text depending on if it's on the device or if it's on the printed page, hard copy. In that case, you'd use multiple Layout Managers, each with their own Text Container, but they're still backed by the same Text Storage.
So now that I've showed you just how easy it is to compose our Text Kit classes in your applications, I'd like to give you a quick demo of just how simple you can implement a document of arbitrary length, a multi-paged document. Here I've got some code and I'm working on an application where I have a document sitting in my applications bundle and I'd like to flow this document on screen.
In addition to flowing this document on the screen, I want my user, when they're scrolling through the pages of my document, to fill like they would on the rest of the system. So I'm going to use UIPageViewController. Now, the way PageViewController works is it needs a view controller to represent every page that you'd like to express. So first, we're going to need a view controller to represent a particular page.
Now, thankfully before you all came in, I had this code sitting in the oven overnight. So, it should ready to go. I'm just going to drag in a class that I wrote called Text Container Instance View Controller. And this just encapsulates a text view which is going to draw the text on screen and the page number that we can reference when the PageViewController asks us for a view controller after or before a particular view controller. Next, in our main view controller, we're going to add a method, ViewControllerForPageNumber.
Remember, a Text Container represents one area on the screen in which you'd like to draw. So for every page, we're going to need a Text Container. But that's no big deal. Text Kit's pretty performant. We're going to create a new page controller representing a particular page wired up to our Layout Manager and add a text view into the view controller.
Next, we're going to implement awakeFromNib. In awakeFromNib, we're going to create a new PageViewController and set ourselves as the delegate. Next, we're going to implement the page view controller data source methods. As I said before, the way PageViewController works is by referencing a view controller before or after a particular view controller and that's fine. Because our single PageViewController has a page number instance variable, we can just add or subtract one and return the view controller representing that page.
Finally, we're going to implement viewWillAppear in our view controller. And we're going to loop through each of our pages and create a Text Container for each page. Because the Text Container represents one area in which we need to draw, we'll need one for every page. And that's it. I'm going to build and run.
And this is the same demo application that you saw in the Intro to Text Kit session talk yesterday. So we're going to drop into the Text Container demo. And as you can see our text is flowed into this view perfectly. And as we scroll using the paging behavior our users have come to expect, we'll be able to go from page to page to page. And you'll notice we've even got a little bit of spacing in between these pages.
Now, if we had right to left text from languages such as Hebrew, Text Kit was built from the ground up to support text in all directions. So that right to left text would flow alongside this left to right text. And so, that's just how easy it is to implement multi-page support using the core Text Kit objects. Now, I'd like to hand it back to my colleague Aki, our senior textpert, who's going to go deeper into TextLayout. Thank you.
Thank you, Peter. Now, let's get close to NSLayoutManager. NSLayoutManager is not a new API for old layout. It's a Text Kit controller class that orchestrates between NSTextContainer and NSTextStorage. And it manages and stores the layout information. Using that information, NSLayoutManager measures and renders text as we pressed it.
NSLayoutManager is designed to be open. It's not just black box that serve its sibling objects. So that all the information used by Text Kit objects, the power, the magic is available to your applications. Finally, its object-oriented interface gives you excessive support for customization through the familiar design patterns such as delegation and subclassing.
Before going any deeper, I'd like to recap what a text layout is. Text layout is basically glyphs and locations. Then I'd like to look at the glyphs. Glyphs-- A glyph is a graphical representation of one word characters. It's simple. These are three examples of glyphs for a same character. As you can see, the graphical informations you have in fonts are used to convert character into glyphs, the graphical representation.
And since glyphs are graphical elements, they can be handled by the graphic subsystem such as quotes. And on our platform, that glyph information, glyph IDs to work in core graphics data type, CGGlyph. Now that we covered the basics of text layout information, I'd like to walk you though some of the things you could do with the layout informations to work in NSLayoutManager. In addition to, get the size of the entire string, now you can get the size of single line to a single glyph at will. As Ian mentioned yesterday in his Introduction to Text Kit session, hit-testing the character or a word under your touch is very trivial.
Also you can get the precise location, fix your perfect location of individual glyphs so that you can add your custom rendering or animation at arbitrary range of characters in your document. And with all text layout information, glyphs plus locations, you can transform and animate text using the power of the core graphic system.
OK. Let's look at the glyph information stored in Layout Manager. You can access the glyph information using this method, glyphAtIndex, simply enough. But notice that the index passed through this method is a glyph index. It's not the character index you use to access the contents of NSTextStorage. So glyph index character index, where are they? They are usually the same but they don't map one to one all the time. It's because ligatures, translation or hyphenation. There are many common situations that make the glyphs to map to their original character directory.
So for that reason, NSLayoutManager keeps track of the character index, the original character index for each glyph for you. You can access the character index using characterIndexForGlyphAtIndex method. And also you can access that index other way around. These are-- You could use these two methods that maps glyph and characters in bulk. And remember, it's important to remember that, in any case, you want to use one of these methods to convert glyphs and character index when you're working with NSLayoutManager.
We look at the glyph information, let's look at the allocations. Just like the glyph info itself, NSLayoutManager keeps track of the text layout information, the allocations for glyph. There are three elements-- generally three elements in text layout, Text Container, line and the glyph location itself. As Peter described earlier in this session, NSLayoutManager connects to an array of Text Containers, glyphs are filled from the beginning of the Text Container at index zero and from so and so forth.
You can access your Text Container associated with a glyph using the textContainerForGlyphAtIndex effectiveRange method. We now know that NSRange pointed by the second argument will be filled with the glyph range corresponding to the Text Container returned from this method. So in a way, you can use this method to animate through all the Text Containers and the corresponding glyph range.
Lines. Just as glyph belong to Text Container, they are inside the line. And Text Containers are filled with lines of text. But notice that a bigger line could be divided into multiple pieces like this due to NSTextContainer geometrical shape defined by exclusion path. So for that reason, we call that data element a line fragment.
You can access the line fragment for a glyph using the lineFragmentRectForGlyphAtIndex effectiveRange method. It returns the CGRect that represent the rectangular area for the line fragment. And finally, the glyph location itself, it's relative to the line fragmented rect that the glyph is inside. You can access the location using the locationForGlyphAtIndex.
Now we covered APIs, I'd like to explain the actual relationship among the three geometrical elements inside the layout information. We have Text Container. It has its own system and it starts at the upper left corner. The origin of the Text Container could be actually anywhere inside a view coordinate system of its parent view. So, it could be offsetted like this.
Now, look into the line fragment itself. The line fragment is represented by CGRect. And its frame origin is relative to the line fragment. It's relative to the Text Container coordinate system. Inside the line fragment, it has its own coordinate system too that start with the upper left corner. And the glyphs are located at its baseline origin that starts from the upper left corner of the line fragment rect itself.
OK. We covered so many concepts and APIs. Now make them used in practice. In this example, I'd like to locate or find the location of the glyph associated with the last character in your document. It's simple. First, we'll get the index of the character in your doc-- the last character in a document just at the lengths of the document and subtract one. Here we are assuming that it's not an empty document.
Then as we discussed earlier, we want to convert the character index to glyph index. Here we're using the glyphIndexForCharacterIndex method. Once you have the glyph index, it's easy to get the other layout elements. Here we're getting the line fragment rect for the glyph using the lineFragmentRectForGlyphAtIndex. Then get the location of the glyph at glyph itself.
Now, we translate the location by adding the line fragment origin so that the location is now contained in Text Container coordinate system. If you want, you can further translate that location into the view coordinate system if the Text Container origin is not at the view origin. Next example.
In addition to the primitive methods that acts as the layout information we discussed so far, NSLayoutManager provides many, many convenience methods to make your life easier. In this example we are doing hit testing. We get the location of the touch inside your text view using the locationInView method. Here we are assuming the location. The view coordinate system is equal to Text Container coordinate system in this example. Now you have the location.
Using the characterIndexForPoint inTextContainer fractionOfDistance BetweenInsertionPoints, you can get the character index correspondent to the glyph closest to the location you specify. Once you have the character index, it's relatively simple to find out what range using some of the NSString amenities such as enumerateSubstringsInRange: options:usingBlock: method. So we are not going deep into the actual implementation finding the word range, but you can do so pretty trivially.
Next. Usually, we recommend sticking to UIKit objects such as UITextView and UILevel for all your text rendering needs. Because with the deeper integration with the attribute string, these objects now provide sufficient functionalities and extensibilities for your needs while you can enjoy Text Kit and amenities such as old layout and accessibility. In some few cases, though, you might want to consider rendering NSLayoutManager into your custom view.
For example, you have multiple overlapping text image frames in your view, commonly found in magazines or newspaper applications, or you want to have custom pagination when you're printing your documents. In this case, you could actually directly access the NSLayoutManager and manually render the contents yourself. Here, it's assumed this Layout Manager variable contains preconfigured Layout Manager.
And we have the rendering area. It's the area inside your view you want to fill the glyph with. It's sort of, you know, you'll get that rect from the direct method. And finally, we and-- we have the container origin that contains the origin of the Text Container inside your view coordinate system. First thing, you want to convert the rendering area into the Text Container coordinate system, just subtract the container origin from the bounding rect frame.
Then use that glyphRangeForBoundingRect inTextContainer, another useful method. You can get the glyph range that are filling that specify that bounding rect. Notice that that glyph range might contain some extra glyphs outside of bounding rect actually. It's because we're handling the bidirectional language such as Arabic and Hebrew, the glyph location could be out of order. So in order to contain all the glyph range, some of the glyph might be lying outside of the bounding rect. So in those cases, you might consider clipping when you are rendering.
Once you have the glyph range, you can render. Here we are rendering the background using that drawBackgroundForGlyphRange atPoint. This method renders attributes such as NSBackgroundColorAttributeName. But we recommend always using this method whenever you are rendering glyphs. It's because in the future, we might enhance this method to support some other attributes. In that case, your application automatically get the new functionalities out of it.
Once you render the background, now render the glyphs using the drawGlyphsForGlyohRange atPoint method. It renders a glyph in the glyph range as well as other auxiliary graphical items such as underlines, strikethrough, shadows and attachments. You might have noticed that we are passing container origin to these methods. So we are rendering an arbitrary glyph range, but you are not passing some location corresponding to the glyph range to drawing method. Maybe that's not what you are used to.
Actually, it's quite simple and straightforward. It's because Layout Manager is designed to render Text Containers. So even though you are passing the glyph range a part of the Text Container, you are always rendering Text Container itself and rendering contents. So when you're passing the location, it's always at the Text Container origin. Now that doesn't necessary mean you will start with your original Text Container shape.
You are free to translate the location by yourself so that the location paths to the rendering method can be arbitrary and your glyph range can be moved to some other places in the view. Here we have the glyph range we want to render. That's some range inside your Text Container.
And we have the location. We want that glyph range to appear at the location inside of view. First, using lineFragmentRectForGlyphAtIndex method we saw earlier, we get the line fragment origin for the glyph you want to render. So in this case, you get the line rect for the first glyph in the glyph range. Once you get that, you subtract the glyph origin from the location you want to render. By doing that, the location is now translated so that the Text Container origin is moved far and the location coincide to the glyph range you want to render.
Now, you have the location, just render it. Another common questions we get at labs and mailing lists, it's like number of lines in your document. It's really simple. But it was actually difficult before Text Kit. With the Text Kits, since NSLayoutManager keeps tracks of all the line fragment rect in your documents, it's easy now. Remember, a visual line could be divided into multiple line fragments because of the exclusion path. You want to store the Y location of the line fragment and using that cache value, you want to compare to the current line fragment rect before answering incremental number of lines.
Here using the glyph range for Text Container, you can get the glyph range inside the Text Container and, you know, you should get used to this method by now, lineFragmentRectForGlyphAtIndex effectiveRange. And as I described it earlier, you can pass a point out to-- a point out to NSRange and get back the glyph range corresponding to the line fragment rect.
And here we are enumerating all the line fragment rect inside the fixed container by comparing the stored last line fragment origin to the current line fragment origin. If the new origin is larger, that means you are moved to the new visual line so that you can increment the number of lines like this.
And at data stored information was the new line fragment origin. It's that simple. We've seen glyphs and layout informations stored in NSLayoutManager there are endless possibilities that you can do with the broader and finer controls you have in your text using the information stored in NSLayoutManager and you can freely access now. But the control over your text doesn't end there.
One of the main Text Kit architecture strength is its vast support for customization. Let's look at how to customize layout using Text Kit. NSLayoutManager provides a rich set of delegation interface. You can-- With some of the interface, you can get notified for step [phonetic] change such as when your layout cache is invited. Or, layout for the container was finished, so on and so forth. With some other delegation interface, you can override many aspects of layout process while it's being laid out.
For example, you can override the line spacing. Your delegate object gets consulted at the end of that every single line fragment rect and you can provide your own line spacing at that point overriding their values stored in paragraph style associated with that text. So for example, with that paragraph style line spacing value, a single line spacing value is used for the whole paragraph. But using this method, you can have custom line spacing everywhere. And this is useful when you want to make space for some other extra rendering like this.
[Inaudible] a lot annotation when you want to have this kind of extra rendering right below the text you want to make space and you don't know if you want to make the space when you are creating the text layout itself, so you have to determine this kind of layout conditions dynamical while you are laying out. Similar to the line spacing, you can override with soft wrapping line-- soft wrapping lines at the end of every single soft wrapping-- soft line breaking. So there it gets consulted.
And by default, we are using the line breaking logic provided by the Unicode standard, so it should be sufficient for most cases, and it provides a localized way of line breaking for every language available on iOS. But in some few cases, you want to enhance the ways of the line wrapping happens for your typographic needs. In that case, you are [inaudible] to override that line wrapping phase like this.
Another powerful feature techniques you can use with delegation, by default, NSLayoutManager uses the mapping between character and glyphs stored inside the font itself. That's the default glyph mapping. You can override this glyph mapping all by yourself while it's laying out of text. This is powerful. It's used by, for example, bullet substitution in security mode or when you want to hide some portion of text when you are folding the line. And there are many, many other ways to customize your text layout like a dynamic query while it's being laid out.
Today, I would like to look at the custom glyph mapping a little farther. When you had text and the text doesn't fit into the available space, it's a common technique used to tail truncation like this. But with the simple tail truncation logic, you might encounter, the actual important information might be truncated out from the user's view. You don't like that.
So using the custom glyph generation logic, you can override and add additional truncation range to your string and make sure your important range of text is visible to the user. Let's see how we can accomplish that. First, with NSLayout method-- NSLayoutManager method, truncatedGlyphRangeInLine FragmentForGlyphAtIndex method, you can get the range of the glyphs that's being truncated out from the user's view.
And you can compare this range with your focus range, for example, when you are searching some words, you want to keep the-- match the words inside the user's view. When it matches this range, you want to truncate additional location. In that case, we estimate the additional truncation range probably using the [inaudible] being truncated out. Now we layout. Inside the layout process, your delegate method, layoutManager: shouldGenerateGlyphs: properties:characterIndexes: font:forGlyphRange method get being called. And inside this method, you can override the default glyph mapping. So, you can do whatever you want.
In this case, we substitute the default glyph mapping with [inaudible] glyphs and truncate. It's that simple. And repeat itself until you find the ideal range that fits everything. Well then let's take a look at the delegate method itself. The delegate method is called for all the text ranges inside your Text Storage when it gets mapped to glyphs. It receives glyph's properties and character indexes for the chunk of text.
And this is a default information and you can override any way you want until you have your delegate object and your implementation of the delegate method. You will see a chunk of glyphs. Look through it and you find a particular range of glyph matches your focus range and you can check against using the original character index. When that happens, you override pass in glyphs information with ellipsis glyph.
And you might wonder what are these things after ellipsis. In order to keep the character and glyph index simple and be efficient, NSLayoutManager often try to pad the glyph range that's being hidden from the users. To do so, the glyph property is working here. What is the glyph property? So it's like other glyph information.
Glyph NSLayoutManager keeps track of glyph property for glyph and it stores semantic behavior for each glyph. For example, you can identify a glyph as a control character like a tab or a new line, so on and so forth. Or, a glyph could be white space that can be treated as elastic at the end of the line break.
In our example, we are using this property, NSGlyphPropertyNull, by displaying this property, the glyph will be treated as the glyph will be ignored from both layout and rendering. So, you can hide a part of the glyph range from the user's view. Now, I'd like to point our Text Kit demo maester, Jordan Breeding to show that multiple truncations demo. Jordan.
So, what I'm going to show you right now is a view controller inside of our demo shell that we used in the Intro session as well. In this case, instead of using a UITextView, we're actually using a new class, a text rendering view. This is a UIView subclass in which we are going to render the glyphs ourselves to achieve multiple truncation. First, I'll show you at running live and then we'll explain what some of the code does. Now we're building and running.
And when we run the demo, you'll notice that we have highlighted the range so that we are concerned about not truncating. And as we get closer and closer to that, you'll notice that it automatically starts to truncate to the left so that we keep it intact. So, how do we do that? Well, in the text rendering view, we actually made a new class called a focus truncation renderer. We setup an instance variable for that. And then awakeFromNib, we actually setup some of our data including the focus range that we are concerned with keeping intact.
Then in our draw rect, we setup some basic information and then we also tell our renderer to use each drawing rect to draw in the same place. So if we go over to our focus truncation renderer, you'll notice that when we setup the contents for the Text Storage, we also set ourselves as the Layout Manager's delegate right here.
The reason that we do that is then in our draw and rect, when we actually draw all of our characters and we know whether we need to force tail truncation and truncate ahead of time or not, all of this calls to Layout Manager will actually consult us for glyph generation.
So, all these calls end up calling down into our delegate method. In this case, layoutManager: shouldGenerateGlyphs: properties:characterIndexes: font:forGlyphRange. This is the method Aki pointed out for laying out custom glyphs. So, what are we actually doing here? Well, first, we're finding out if we have an intersection range. And then in our actual code, we're checking the character indexes that we've been passed by the Layout Manager. And if they're inside the target range, we know that we need to use the ellipsis glyph. So, we get the character for the ellipsis glyph and then we get the glyph for the characters and then we actually do the replacement.
Then, just like Aki said, we actually change the other glyph character properties to be the control character and the null character so that everything just lays out automatically for us in our draw rect. It's actually just as simple as that. It was a lot harder before, right? Next, I'd like to have Aki come back up and close out our session for us.
We saw you can use the gorgeous text layouts in your applications. And you learned how to achieve multiple page, multiple document configuration easy-- easily with the application. And we covered the rich in text layout information provided through the NSLayoutManager API. And finally, we saw some aspect of the Text Kit customizability that was previously not possible. So, Text Kit is not just another text API you need to learn. With the deep integration with UIKit, comprehensive functionalities and broad customizability, we believe it will be the last and only text API even be working for years to come.
If you want to know more, you can contact our evangelist, Jake Behrens. And we have two related sessions, one already happened yesterday and another coming up tomorrow morning at 9:00 at Presidio. And that's going to talk about the technology behind the dynamic type. And if you want to know how to utilize all the cool technologies such as UIFont and UIFont Descriptor, you want to be there. So, thank you and enjoy the rest of the conference.
[ Applause ]