In iOS 7, Apple introduced the NSTextAttachment class for embedding images into attributed strings. OS X, pardon macOS, did have this feature much earlier, already as early as 2001 in version 10.0 “Cheetah”. I suspect that they needed 7 years to migrate because the first needed to revamp the inner workings of UITextView and UITextField to natively work with attributed text, as well as modernize CoreText into what is nowadays referred to as TextKit.
With iOS 10 being released, we now have 3 major iOS releases supporting text attachment in standard UIKit views. About time, that we start using text attachments for displaying inline images in rich text.
In DTCoreText – which supports iOS 4.3 and up – I needed to work around the non-existence of NSTextAttachment by creating my own attachment class, DTTextAttachment. It uses the CoreText run delegate callback mechanism to reserve sufficient space in the text view – also custom – to show the image inline.
Simple Inline Images
At its simplest you put your UIImage into the appropriate property and set the bounds to something manageable. If you don’t set the bounds then the image will likely be way too large.
let image = UIImage(named: "cocoanetics") // create a text attachment for the image let attachment = NSTextAttachment() attachment.image = image attachment.bounds = CGRect(x: 0, y: 0, width: 100, height: 134) // use convenience initializer that takes attachment as parameter let attributedString = NSAttributedString(attachment: attachment) // set it on UIKit view textView.attributedText = attributedString
The attributedString here becomes a string of length 1, consisting of a standard unicode object character ￼ and the attachment under the NSAttachmentAttributeName key.
How about HTML?
Most people probably would be lazy and avoid constructing attributed strings by hand. Especially if they learn that they could have it parse HTML via the appropriate init function. The following code shows a simple text I performed to see how Apple is dealing with HTML IMG tags referencing remote images by URL.
let body = "<img src=\"https://www.cocoanetics.com/files/Cocoanetics_Square.jpg\" />" let data = body.dataUsingEncoding(NSUTF8StringEncoding) let options : [String: AnyObject] = [ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, NSCharacterEncodingDocumentAttribute: NSUTF8StringEncoding ] // parse HTML data let attribStr = try! NSAttributedString(data: data!, options: options, documentAttributes: nil) // get the attachment at index 0 let attachment = attribStr.attribute(NSAttachmentAttributeName, atIndex: 0, effectiveRange: nil) // show that the image data is already there print(attachment?.fileWrapper??.regularFileContents)
When we inspect the resulting attachment, we find that Apple has already (synchronously) downloaded the referenced image data and placed it into the fileWrapper property of the attachment. NSFileWrapper does not have a facility for asynchronously downloading files over the web and so we find the image data already present in the file wrapper’s regularFileContents property.
Since the attachment is buried in the resulting attributed string, we find it a bit harder to set a custom value there. Also we don’t like that this operation occurred synchronously. The same test – with WiFi disabled – doesn’t download the image, and omits the text attachment altogether, resulting in an attributedString of length 0 for the above example. Ugh!
You can see how this can become a problem if you are dependent on the device having a good internet connection for generating the correct attributed string. If you wanted to be independent of internet connectivity, you could reference an image by relative URL and specify the base URL to be the app’s resource folder URL.
Making it Asynchronous
We want to retain control over the downloading of images. So we need an asynchronous version of NSTextAttachment. One that downloads the image as soon as it is needed, and updates the text view as soon as the download is complete.
We want to be able to do this to lazily load the referenced remote image:
// create an async text attachment let imageURL = NSURL(string: "https://www.cocoanetics.com/files/Cocoanetics_Square.jpg")! let attachment = AsyncTextAttachment(imageURL: imageURL) // use convenience initializer that takes attachment as parameter let attributedString = NSAttributedString(attachment: attachment) // set it on UIKit view textView.attributedText = attributedString
Oh, and we would also like the resulting image to be scaled to fit the width of the text view, regardless of orientation. And the text view should update without us having to call any layout methods.
In the second part of this series I will show how I got it working. We will be looking at the NSLayoutManager methods and build one that allows us to refresh the display and layout of specific characters.
There is very little information on the web on text attachments and none that I could find for dealing with remote images. I was able to figure out a technique to achieve such behavior, only using public APIs and fully compatible with UIKit.
I am looking forward to describing how to do it in part 2.
Also published on Medium.