Demo

Serializing & Deserializing an AttributedString

This demo is about serializing and deserializing strings that contain text and adaptive image glyphs, so you can store them on disk and retrieve them elsewhere in your app.

Open the starter project for lesson 2. ContentView has a property to hold the deserialized string:

@State var textToDisplay: AttributedString? = nil

And a Text view to display the deserialized string:

if let textToDisplay {
  Text(AttributedString(textToDisplay))
} else {
  Text("No text to display")
    .foregroundColor(.gray)
}

When the text view’s text changes, the onChange modifier converts it to an NSAttributedString, serializes and deserializes this, then converts it back to AttributedString:

.onChange(of: textTop) { oldText, newText in
  let nsAttrString = NSAttributedString(newText)
  if let rtfData = serializeText(nsAttrString),
     let newNSAttrString = deserializeText(rtfData) {
    textToDisplay = AttributedString(newNSAttrString)
  }
}

ContentViewExt contains an extension to ContentView with two functions — one to serialize an NSAttributedString into RTFD data and the other to deserialize RTFD data into an NSAttributedString:

extension ContentView {
  // 1. Serialize an NSAttributedString into RTFD data
  func serializeText(_ text: NSAttributedString?) -> Data? {
    guard let text = text else { return nil }
    do {
      let rtfData = try text.data(
        from: NSRange(location: 0, length: text.length), 
        documentAttributes: [.documentType: NSAttributedString.DocumentType.rtfd])
      return rtfData
    } catch {
      print("Error serializing text: \(error)")
      return nil
    }
  }

  // 2. Deserialize RTFD data into an NSAttributedString
  func deserializeText(_ data: Data) -> NSAttributedString? {
    do {
      let attributedString = try NSAttributedString(data: data, documentAttributes: nil)
      return attributedString
    } catch {
      print("Error deserializing text: \(error)")
      return nil
    }
  }
}

Build and run the app in a simulator. Copy your text with Genmoji from Notes and paste it into the top text editor.

Serialize/deserialize doesn't work with converted string.
Serialize/deserialize doesn't work with converted string.

Unexpectedly, this doesn’t work — the Genmoji are gone! Converting between AttributedString and NSAttributedString should preserve adaptive image glyphs. Modify the onChange code to test this — comment out the serialize/deserialize code and just convert the NSAttributedString back to an AttributedString:

.onChange(of: textTop) { oldText, newText in
  let nsAttrString = NSAttributedString(newText)
  textToDisplay = AttributedString(nsAttrString)
  
//  if let rtfData = serializeText(nsAttrString),
//     let newNSAttrString = deserializeText(rtfData) {
//    textToDisplay = AttributedString(newNSAttrString)
//  }
}

Build and run, then copy-paste your text with Genmoji.

AttributedString <=> NSAttributedString preserves Genmoji.
AttributedString <=> NSAttributedString preserves Genmoji.

This works! nsAttrString does contain your Genmoji, so these should get serialized and deserialized correctly. SwiftUI support for Genmoji is relatively new, so there’s possibly a bug. Until it’s fixed, you can use a UIViewRepresentable UITextView to work directly with NSAttributedString.

CustomContentView

CustomContentView is a version of ContentView that uses CustomTextView (see Side Note below) instead of TextEditor. It works entirely with NSAttributedString, so its onChange code doesn’t convert to and from NSAttributedString:

.onChange(of: textTop) { oldText, newText in
  if let rtfData = serializeText(newText) {
    textToDisplay = deserializeText(rtfData)
  }
}

In GenmojiApp, comment out ContentView() and call CustomContentView() instead:

struct GenmojiApp: App {
  var body: some Scene {
    WindowGroup {
      CustomContentView()
//      ContentView()
    }
  }
}

Build and run, then copy-paste your text with Genmoji.

Serialize/deserialize works with NSAttributedString.
Serialize/deserialize works with NSAttributedString.

Serializing and deserializing NSAttributedString works.

Side Note: CustomTextView

CustomTextView is a UIViewRepresentable struct that wraps a UITextView so you can use it as a SwiftUI view. Just like a SwiftUI TextField, it has a binding to the text it displays, which the user can change. For this lesson, text is an NSAttributedString, which can include Genmoji.

@Binding var text: NSAttributedString

makeUIView(context:) creates a UITextView and sets some of its properties.

let textView = UITextView()
textView.attributedText = text
textView.delegate = context.coordinator  // 1

textView.isEditable = true
textView.font = UIFont.preferredFont(forTextStyle: textStyle)

textView.supportsAdaptiveImageGlyph = true  // 2
textView.allowsEditingTextAttributes = true
  1. UITextView needs a delegate because the coordinator (below) includes a UITextViewDelegate method.
  2. UITextView needs the last two properties to work with Genmoji and allow formatting.

updateView(_:context:) updates the text view’s text property with the user’s changes.

func updateUIView(_ uiView: UITextView, context: Context) {
  if uiView.text != text {
    uiView.text = text
  }
}

CustomTextView needs a coordinator to feed the text view’s text back to ContentView:

func makeCoordinator() -> Coordinator {
  Coordinator(self)
}

class Coordinator: NSObject, UITextViewDelegate {
  var parent: CustomTextView

  init(_ parent: CustomTextView) {
    self.parent = parent
  }

  // UITextView delegate method
  func textViewDidChange(_ textView: UITextView) {
    if let currentText = textView.text {
      parent.text = currentText
    }
  }
}
See forum comments
Download course materials from Github
Previous: Instruction Next: Conclusion