Instruction

Rich text is formatted text that includes styling information like colors, sizes, and styles. Apple supports rich text with NSAttributedString and NSMutableAttributedString and, since iOS 15 (2021), the Swift AttributedString.

Enabling Rich Text Input

In SwiftUI, to get rich text input, you need a TextEditor, and you store the input in an AttributedString.

In UIKit, UITextView and UITextField need these two properties set to true:

  • allowsEditingTextAttributes
  • supportsAdaptiveImageGlyph

When the property allowsEditingTextAttributes is true, formatting options — Bold, Italic, Underline, and more — appear when the user selects something in the text view or field. And supportsAdaptiveImageGlyph does just that.

And you store the input in an NSAttributedString or NSMutableAttributedString.

Storing Rich Text

There’s no easy way to serialize an AttributedString directly — you can convert it to an NSAttributedString, then use the following procedure.

To store rich text (NSAttributedString or NSMutableAttributedString) for later retrieval, you serialize the textStorage contents of the UITextView into an RTFD data object and store it in your database.

// Extract contents of text view as an NSAttributedString
let textContents = textView.textStorage

// Serialize as data for storage or transport
let rtfData = try textContents.data(
  from: NSRange(location: 0, length: textContents.length),
  documentAttributes: [.documentType: NSAttributedString.DocumentType.rtfd]
)

To display the content again, reverse the process and create an NSAttributedString from the data you stored.

// Create attributed string from serialized data
let textFromData = try NSAttributedString(data: rtfData, documentAttributes: nil)

// Set on text view
textView.textStorage.setAttributedString(textFromData)

To display the retrieved NSAttributedString in a SwiftUI view, convert it to an AttributedString.

Cross-Platform Genmoji Display: HTML

Again, when working with AttributedString, convert to NSAttributedString to perform the following function.

To display an image glyph in HTML, specify HTML instead of RTFD in data(from:length:documentAttributes:):

// Converting NSAttributedString to HTML
let htmlData = try textContent.data(
  from: NSRange(location: 0, length: textContent.length),
  documentAttributes: [.documentType: NSAttributedString.DocumentType.html]
)

Advanced engines like WebKit that support the “apple-adaptive-glyph type” will display the image inline with text as if it were a standard emoji. You should provide a fallback image to display on engines that don’t support image glyphs. The NSAdaptiveImageGlyph’s content description provides the image’s alt-text no matter which image is displayed in the browser.

Plain Text: Inline Image

If you need to transfer your image glyphs to plain text or other non-RTF data stores, you could store the Unicode attachment character NSAttachmentCharacter 0xFFFC at the appropriate text location, with a reference to the image glyph’s identifier in the plain text data field, and add the image to the image store. Because an image glyph’s contentIdentifier is unique and stable, you only need to store it once.

Here’s how you extract adaptive image glyph data from an NSAttributedString:

// Decompose an NSAttributedString
func decomposeAttributedString(_ attrStr: NSAttributedString)
  -> (String, [(NSRange, String)], [String: Data]) {
  // 1
  let string = attrStr.string
  var imageRanges: [(NSRange, String)] = []
  var imageData: [String: Data] = [:]
  // 2
  attrStr.enumerateAttribute(
    .adaptiveImageGlyph, 
    in: NSMakeRange(0, attrStr.length)) { (value, range, stop) in
    
    if let glyph = value as? NSAdaptiveImageGlyph {
      let id = glyph.contentIdentifier
      imageRanges.append((range, id))
      if imageData[id] == nil {
        imageData[id] = glyph.imageContent
      }
    }
  }
  // 3
  return (string, imageRanges, imageData)
}
  1. The plain text of attrStr is in attrStr.string. The rest of the attributed string is made up of NSAdaptiveImageGlyph objects. This function stores the range and identifier of each image glyph in imageRanges and the actual image data in the imageData dictionary, using the unique identifiers as the keys.
  2. Then it enumerates through the attributes of attrStr, extracting the image glyph ranges, identifiers and image data.
  3. And it returns a tuple of the plain text, image ranges and identifiers, and identifier-image-data dictionary.

You can recompose information like this into an NSMutableAttributedString:

// Recompose an attributed string
func recomposeAttributedString(string: String,
                               imageRanges: [(NSRange, String)],
                               imageData: [String: Data]) -> NSAttributedString {
  let attrStr: NSMutableAttributedString = .init(string: string)
  var images: [String: NSAdaptiveImageGlyph] = [:]
  for (id, data) in imageData {
    images[id] = NSAdaptiveImageGlyph(imageContent: data)
  }
  for (range, id) in imageRanges {
    attrStr.addAttribute(.adaptiveImageGlyph, value: images[id]!, range: range)
  }
  return attrStr
}
See forum comments
Download course materials from Github
Previous: Introduction Next: Demo