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.
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.
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.
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
-
UITextViewneeds a delegate because the coordinator (below) includes aUITextViewDelegatemethod. -
UITextViewneeds 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
}
}
}