Almost always when we iOS developers want to display some rich text we are using UIWebView. That’s not by choice, since traditionally Apple did not provide any classes to us being able to show formatted text.
That changed slightly with the iPad, because in 3.2 we got CoreText as well as CATextLayer. CoreText gives us NSAttributedString which is basically a string that can have different attributes for ranges of characters. Those attributes can either be standard ones, like to describe the font, color, size and paragraph format. Or they can be your own arbitrary attributes.
I’ve shown how to programmatically construct these in my previous article on CoreText. The one thing though that is still missing from making CoreText really useful are ways to create attributed strings. Clearly doing it all in code is not feasable.
In this article I am introducing an Open Source project that aims to provide the missing functionality to iOS developers.
The Dilemma
UIWebView is a wrapper class around Webkit and unfortunately Apple does not want to give us more control over it. Possibly so that nobody goes and breaks it. These are only some of the drawbacks:
- You cannot properly control generation of thumbnails or bitmaps
- Webkit is not threadsafe (according to Apple documentation), so you risk blocking your main thread and UI if you render graphics with it
- No (official) control over the shade shown around the content, like removing it if it does not work well in your app style.
- It’s slow compared to regular drawing, even a local HTML document first shows a blank screen until it appears
- if you ditch the rich and just keep the text, then UITextView seems like an option, but you cannot affect the styling of (detected) hyperlinks and each such links exits your app
While there are certain unofficial workarounds the general situation is unbearable if you want to use rich text. Use WebView or render your about screens as PNGs, adding all the additional overhead of having to redo everything if you’re just changing like a version number of the app.
Looking at NSAttributedString.h on the Mac, you find some very interesting methods, commented by Apple as “The following methods should now be considered as conveniences for various common document types.”
- – (id)initWithRTF:(NSData *)data documentAttributes:(NSDictionary **)dict;
- – (id)initWithRTFD:(NSData *)data documentAttributes:(NSDictionary **)dict;
- – (id)initWithHTML:(NSData *)data documentAttributes:(NSDictionary **)dict;
- – (id)initWithHTML:(NSData *)data baseURL:(NSURL *)base documentAttributes:(NSDictionary **)dict;
- – (id)initWithDocFormat:(NSData *)data documentAttributes:(NSDictionary **)dict;
- – (id)initWithHTML:(NSData *)data options:(NSDictionary *)options documentAttributes:(NSDictionary **)dict;
And convenient they ARE, just not to us. Let this sink in a bit: those Mac types have methods to read RTF, Word Doc and HTML files into NSAttributedStrings. It’s SO UNFAIR. 🙂
The Solution
We can never hope to duplicate all the richness and features of Webkit, BUT my tests have show that if you have control over the quality of the HTML code – like if it’s your own for a credits screen – then it is absolutely feasable to provide a category on NSAttributedString that generates these from your HTML.
For this exact purpose I started an Open Source project on GitHub: NSAttributedString-Additions-for-HTML.
Now I have an inkling that Apple will eventually port the above mentioned “convenient methods” to iOS as well, maybe as soon as in SDK 5. But when do you think will the majority of your customers have iOS 5 on their devices? If the adoption rate of iOS 4 is any indication then this will take until spring 2012. Using our code literally gives you a one year head start.
Also you have an option of falling back to UIWebView on devices running an iOS before 3.2 and falling forward to these official methods – if they ever come – if you find them to be available on the users iOS 5 device.
Our initWithHTML methods aim to perfectly match the output from the Mac version. This is possible to achieve for characters and I have unit tests in place that make certain this keeps being perfect. Regarding the attributes there are many things that have to be done slightly different on iOS to achieve the same look. I won’t bother you with the details there.
But this is only the first part of the story. You might have thought that all you need is to get these pesky NSAttributedStrings and then CATextLayer will do the rest. ‘fraid not. CATextLayer does not obey paragraph formatting and it does not deal with images and link interactivity. This might change in future versions, but so far CATextLayer is only good if you want to display static text on a button or label.
This is why we are also developing DTAttributedTextView and DTAttributedTextContentView. The first being a scrollview subclass for longer text and the latter being responsible for the drawing itself. This is this the perfect replacement for UITextView or UIWebView.
Another level of improving over what web views give you is the way of customization you can do. DTAttributedTextContentView asks a delegate for a custom UIView for each glyph run on screen, providing the coordinates for it. This way you can add interactivity or custom UIViews to your HTML as you could never do before. An example shows adding a movie player for a HTML5 video tag. Many more options are possible, like adding custom rendering for SVG images.
This project wants to give you an option to NOT use UIWebView where you don’t actually need the whole browsing experience, but just rich text and hyperlinks.
You have two ways how you can get the source: you can either download a ZIP/TAR or you can clone the git repo. The latter option allows you to get the updates that get added now on a daily basis.
The Future
Development on this component continues are more an more people implement it into their iOS apps. If you find some HTML that does not come out right, then please send it to us for inspection so that we can add or tweak the HTML parser to deal with it properly. Let’s call that “purpose-driven development”.
My long term goal for this is actually something even more ambitious: a Rich-Text editor. I am envisioning creating a component that allows you to edit NSAttributedStrings, copy/paste and generate HTML from it. Maybe Apple will finally give us something like this in iOS 5, keeping our fingers crossed. I have the theory that the rich text editing in the iOS apps are a taste of the things to come to iOS 5. But if this fails to materialize then I will endeavor to fill this void.
A US-based company has expressed interest in sponsoring development of additional needed features for displaying digital documents. Add this monetary incentive to the fact that I am now spending mornings adding new features and fixing stuff you will find that this project is growing in leaps and bounds.
As any FOSS initiative it also depends on people making modifications, improvements and fixing bugs to then let me pull these changes into my master repository. So please have a look at the project and begin to replace web views where possible. The snags you might hit there are invaluable feedback to us to know what we still need to work on.
Categories: Tools
Oliver,
“No (official) control over the shade shown around the content, like removing it if it does not work well in your app style.”
You are correct. There isn’t an official way to get around this. Have you seen this technique?
http://idevrecipes.com/2010/12/03/transparent-uiwebviews/
https://github.com/boctor/idev-recipes/tree/master/TransparentUIWebViews
I pretty much agree with you about UIWebView, but the main problem with CoreText is that you can’t select text to copy / past.
That’s a really great project. I’m going to check it and see if it can be helpful in my current project 🙂
Hi Oliver,
Just gave your project a try.
Is there any way or limitation regarding font anti-aliasing?
Text rendered through NSAttributedString doesn’t look as polished as UILabel ones.
Thanks,
Jérémy
have a look at the settings at the beginning of drawRect of DTAttributedTextContentView.
Here’s a little treat for ya’
If look at the (public) header for UITextView you’ll see it has a WebFrame instance, and reference to the body DOMHTMLElement. Peculiar innit? 😉
If you where to inspect the methods on UITextView you’ll also find two methods of interest: -contentAsHTMLString and -setContentToHTMLString:. Call the later one with some html and it’ll render it.
Sadly those methods aren’t part of the public API.
But it would indicate that UITextView is in fact implemented with a WebFrame. I’m guessing it’s because UITextView dates back before CoreText was available, even for Apple themselves, on iOS.
Thanks for sharing this amazing info. I guess I will implement these two methods too on DTAttributedTextView.
+1. The big thing for me is interoperability with major dtp file formats, reliably. Simply getting UIWebView to display word, pages files, etc as advertised is not working, and not everyone publishes docs in HTML. Granted, its a huge issue, with lots of features (text boxes, multi-columns). I’m working on a project now to display a Pages doc on the iPad, and all I get is a “Unable to Read Document. An error occurred while reading the document.” great. Being able to create yet-another-standard with these attributed strings is a day late and a dollar short, imho. Will be checking up on you though, good luck!
hey thank you!
you save my live!!
bests from switzerland
philippe
Awesome work. But I am not able to get select/copy/paste. Any ideas?
select/copy/paste need to be implemented separately. select/copy probably will be in the open source. paste and other rich-text editing will be a component I’ll be selling.
Thanks for the reply. Any pointers about the implementation in terms of approach.
Hi,
Thanks a lot for this framework !
Does it support saving a nsattributedstring to a RTF file ?
Thanks,
Vincent
No, still looking for somebody with knowledge on RTF who could reverse engineer that for iOS from the existing functions on Mac.
I have some clue for doing that :
Omnigroup has released a sample project with a full TextEditor : https://github.com/omnigroup/OmniGroup/tree/master/Frameworks/OmniUI/iPad/Examples/TextEditor
But this is quite complex to integrate to an existing project, it has a lot of dependencies and require to use their own classes for the whole project (you even need to use their UIApplicationDelegate)
But if you look at the source you’ll se that they use 2 classes that handle reading and writing to RTF :
OmniUI/iPad/RTF/OUIRTFReader
OmniUI/iPad/RTF/OUIRTFWriter
I didn’t manage to include them in my project, and i posted a question on the omnigroup developper forum : http://forums.omnigroup.com/showthread.php?t=20537
if you want to give it a try you’re most welcome 😉
Anyway if i manage to find out how to integrate theses classes to my project i’ll let you know.
Thanks for the great project. I have one concern though. When the HTML string being rendered contains Arabic characters it will crash. I guess the problem is from NSScanner but I’m not quite sure. Any help would be appreciated as I’m mainly going to use it to display Arabic text.
You have to be more specific than this. In the least send a short HTML sample.
Something very short and simple like مرحباً will make it crash.
I’m sorry, the Arabic word above is supposed to be enclosed in a “p” tag.
I fixed the problem, it was in stringByNormalizingWhitespace: https://github.com/Cocoanetics/NSAttributedString-Additions-for-HTML/commit/9d4897eb1361109b739138b9c2e26aef00a9e04e
After lots of debugging I figured out the problem was in stringByNormalizingWhitespace but I couldn’t fix it. Thanks a lot, will download the modified code now 🙂
How about PDFs? I have used that technique and it works great. WYSIWYG, custom fonts, perfect rendering… when the iPhone 4 appeared, I didn’t even need to update my PDFView class, it automagically rendered at 2x resolution and was crisp and clean. 🙂
Your opinion. I think that PDF is impractible if you want performance, interactivity and full layouting Control.
For static layouts it works very well and is plenty fast. Obviously for dynamic layouts it would be cumbersome at best. The case I needed was perfect placement of styled text without using burned-in graphics. Just throwing it out there, might be useful for someone! 🙂
Could you provide some info on how to get started with this? Thanks!
Look at the demo project on GitHub, Link is under ‘The Solution’ above.
Could you be more specific on how to implement this into an existing project? What I did was drag and drop the CoreTextExtensions.xcodeproj and DTWebArchive.xcodeproj file into my project navigator, added the CoreTextExtensions and DTWebArchive target to the target dependencies, and update the search paths, but whenever I build, I get the following errors:
Undefined symbols for architecture i386:
“_OBJC_CLASS_$_DTAttributedTextContentView”, referenced from:
objc-class-ref in ShowThreadItemCell.o
“_OBJC_CLASS_$_DTAttributedTextView”, referenced from:
objc-class-ref in ShowThreadItemCell.o
“_DTDefaultFontFamily”, referenced from:
-[ShowThreadItemCell initWithStyle:reuseIdentifier:] in ShowThreadItemCell.o
“_DTDefaultLinkColor”, referenced from:
-[ShowThreadItemCell initWithStyle:reuseIdentifier:] in ShowThreadItemCell.o
“_DTMaxImageSize”, referenced from:
-[ShowThreadItemCell initWithStyle:reuseIdentifier:] in ShowThreadItemCell.o
“_NSBaseURLDocumentOption”, referenced from:
-[ShowThreadItemCell initWithStyle:reuseIdentifier:] in ShowThreadItemCell.o
“_NSTextSizeMultiplierDocumentOption”, referenced from:
-[ShowThreadItemCell initWithStyle:reuseIdentifier:] in ShowThreadItemCell.o
ld: symbol(s) not found for architecture i386
collect2: ld returned 1 exit status
I’ve made sure to include all the necessary frameworks, but it still gives me errors. Any suggestions, is this the correct way to install? Many thanks for the work you’ve done!
since the product of these xcodeproj are not libraries you cannot add them as sub-projects. You have to add the class files instead.
Doh, thank you. I got it working now.
Hey, gr8 project but bad on ios6 and native views with your html additions.
Unfortunately the ios6 NSAttributedString don’t use CoreText formatting and when you try to assign attributedText property to controls with coretext attributes, the application crash.
Do you plan to rewrite NSString+HTML additions for that ? I’ve seen there is well done work for css, html tags and
parsing but, all done with coretext attributes.
Some people has already posted this issue (incompatible coretext attributes on io6 NSAttributedString) as ios sdk bug to apple.Personally, hope apple will remedy to that with internal automatic conversion between coretext attributes and appkit attributes for the new NSAttributedString.
The only solution, for now, is the enumeration of attributed string coming from html and substitution of every coretext attribute with new ios6 attribute. Your DTViews still continue to work off course, the issue is only related to attributedText property of io6 views.
Keep the good work.
Luca