Actually I wanted to release this as version 1.5.4, but frankly I got lazy trying to separate new features from bug fixes. So let’s call this 1.6.0 instead.
- CHANGED: The algorithm to place lines during typesetting is completely revamped. It is now modeled after how Safari does it.
- ADDED: Basic Voice Over support
- ADDED: Support for soft hyphens
- ADDED: Delegate method that gets called before text is drawn so that you can draw beneath it
- ADDED [SPONSORED]: Support for custom HTML attributes on text
- FIXED: new typesetting algorithm fixes several issues with wildly changing line heights
- FIXED: new typesetting algorithm fixes line height on wrapping lines
- FIXED: new typesetting algorithm fixes line positioning using fixed line height in combination with text attachments
- FIXED: DTHTMLWriter would emit multiple A tags if format changed inside the hyperlink
So you can see that this definitely deserves at least a minor version bump.
Basic Voice Over
Thanks to Austen Green who provided the current implementation for Voice Over. I was hesitant to merge that which fixing 1.5 bugs, fearing that it might interfere with DTRichTextEditor. But now I figured I didn’t want to delay this any more.
Austen explains his implementation:
DTCoreTextLayoutFrameAccessibilityElementGenerator is a helper object that takes a DTCoreTextLayoutFrame returns an array objects conforming to the UIAccessibility informal protocol. For attributed text, it wraps up items in a UIAccessibilityElement subclass, and for custom views it simply inserts them into the array, as VoiceOver wants an ordered collection of accessibile elements.
DTAccessibilityElement is a UIAccessibilityElement that takes a parent view and calculates its accessibilityFrame, which must be in screen coordinates, when asked. UIAccessibilityElement uses a static accessibilityFrame, which causes the rects to be off as you scroll.
DTAccessibilityViewProxy is simply a proxy object for a UIView. Since DTAttributedTextContentView lazily loads its attachment views, but VoiceOver needs to know about the element, I used a proxy for the view that will return nil accessibility attributes if the view hasn’t been created yet (which is really just when it’s offscreen and shouldn’t be needed by VoiceOver anyway). Once the view is onscreen and in the content view’s customViewsForAttachmentsIndex, the proxy will start returning the view’s accessibility attributes and the view will become visible to VoiceOver. The DTAttributedTextContentView returns these view proxies for its text attachment custom views when asked for a view to represent a text attachment by the DTCoreTextLayoutFrameAccessibilityElementGenerator.
The DTAttributedTextContentView lazily loads its accessibility elements and invalidates them whenever the text frame changes. So there’s no performance penalty if the user never turns on VoiceOver, and the rects should be updated and correct even when the user rotates the device, causing the content view’s bounds to change.
At the moment Voice Over support really is very basic, for the most part it only reads portions of the text together that have the same attributes. I’m happy to receive pull request for further refinements.
The W3C Visual Formatting Model document describes the method how web browsers are supposed to position lines. The formula seems to be something like this:
The general overview of the “WebKit Algorithm is this”:
- F = maximum font pixel size in a line
- A = line’s Ascent
- D = line’s Descent
- M = line height multiplier
- L (Leading) = F*M
- P = L – (A+D)
Half of P is applied before a line, Half of L is applied following it. In the following example I set -webkit-margin-before and -webkit-margin-after to 0, so you see in yellow/red the A+D and the color outside of a line is P.
What makes this a bit more tricky is how to deal with inline images, but I think I have got it working for most usual scenarios.
The big advantage of this is that the line height is now predictable because it is related to the maximum font size in the line. Before it would always depend on the ascenders and descenders of the glyphs. Because of this lines containing Emoji would have a different position than lines without.
Now a line is scanned for the maximum font point size. This is then multiplied with a reasonable factor (as mentioned in the linked W3C document). If you want the lines to touch each other, then you just set the line height multiplier to 1.0.
Custom HTML Attributes
A client sponsored this addition to DTCoreText which allows adding of custom HTML attributes to ranges in the attributed string. All attributes that are not used internally as well as all CSS classes which are not from the merged stylesheet will be added in a dictionary behind the new DTCustomAttributesAttribute.
DTHTMLWriter tries to add those attributes to the longest possible range of the following:
- Paragraph (P, H1 – H6, LI) tag
- Hyperlink A tag
- Character SPAN tags
If the range of the attribute is equal to the one of the entire paragraph then it will be added to that. If it is equal to a hyperlink then there. Finally if none of these ranges match, then it will be added to the SPAN tags for the individual character ranges.
This mechanism for example allows to add a target attribute to a link and have that also be output as HTML.
About the Future of DTCoreText
I’ve been asked multiple times how the pending release of iOS 7 will impact DTCoreText. This project has always been a side hobby of mine and part of its development was funded by sponsored enhancements (like attributes in this version) and part from sales of my DTRichTextEditor component.
Truth be told, sales tanked in June. As is the tradition everybody is looking what news there are from Apple. And they did not recover since, 2 weeks after the event. This tells me that people generally assume that they target iOS 7 only with anything they are building right now.
I would say that about 70% of what DTCoreText does is now available in iOS 7’s new Text Kit. If your specific use case is covered by it then by all means you should cut off iOS 6 support as soon as iOS 7 is released and make your app iOS 7 only.
There are however those remaining 30% even though there is quite a bit of flux. We have seen indications of new features being added from seed 1 to seed 2 even, so those smart Apple engineers might still be working hard to get in more of the remaining missing features. Keep a close eye on the API diffs!
At this point we simply cannot know the true extend of the Sherlocking of DTCoreText. There might be some parts – specifically to parsing or generating HTML – that might be worth salvaging. But for these too we have to wait what Text Kit will bring. NSHTMLWriter? initWithHTML:? We’ll see.
Now there are many already released apps using DTCoreText and also there might be people not willing to abandon iOS 6 or 5 compatibility. Also some scenarios might be outside of what Text Kit is meant to be able to do. For those it boils down to the question: what is it worth to you?
I am ready to move on. I for one are really happy that rich text is finally the proper support in iOS 7 it deserves. Finally there will be a proper framework for dealing with text, not this internal WebKit-wrapping that was going on in classes like UITextView. Working with Core Text taught me a great deal about text on Apple’s platforms. Enough to have an enormous appreciation of Text Kit.
But … like everybody else I know I need to be able to make a living off the time I spend programming. We are hardly selling any of those Non-Attribution Licenses any more and as mentioned before the DTRichTextEditor is selling very badly at the moment. I can no longer work for free.
So it will fall to You to vote with your wallet about what is going to happen with DTCoreText.
Every sold copy of DTRichTextEditor also funds a bit of work on DTCoreText, so buying those licenses is one way to support it.
If you find a bug, please fix it and provide a pull request. If you have an enhancement that fits with the philosophy of the project please also send a pull request. Or you can contract us to do either, our rate is 75 Euros per hour.