Post

Replies

Boosts

Views

Activity

Reply to Custom AttributedString and SwiftData
Hi Ziqiao, Thank you for the extra help of providing me working code that implements the wrapping of an AttributedString with custom attributes and persists it in SwiftData. Since I was also interested in further streamlining this conversion, per your instructions, I converted the attributed string wrapper into a class and (after puzzling for a while about how exactly to do this) added a ValueTransformer to the process. For the benefit of others, the relevant code is below. If you (or anyone else who reads this) see anything wrong or something that could be improved, please let me (and the world) know. Code: import SwiftUI @main struct ApplesSolutionApp: App { init() { ValueTransformer.setValueTransformer(AttributedStringWrapperTransformer(), forName: NSValueTransformerName("AttributedStringWrapperTransformer")) } var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: MyModel.self) } } import Foundation import SwiftData @Model class MyModel { var _attributedStringWrapperData: Data var attributedString: AttributedString { get { guard let transformer = ValueTransformer(forName: NSValueTransformerName("AttributedStringWrapperTransformer")), let wrapper = transformer.reverseTransformedValue(_attributedStringWrapperData) as? AttributedStringWrapper else { print("Failed to decode using transformer") return AttributedString("Failed to decode") } return wrapper.attributedString } set { guard let transformer = ValueTransformer(forName: NSValueTransformerName("AttributedStringWrapperTransformer")), let data = transformer.transformedValue(AttributedStringWrapper(newValue)) as? Data else { print("Failed to encode using transformer") return } _attributedStringWrapperData = data } } init(attributedString: AttributedString) { let wrapper = AttributedStringWrapper(attributedString) if let transformer = ValueTransformer(forName: NSValueTransformerName("AttributedStringWrapperTransformer")), let data = transformer.transformedValue(wrapper) as? Data { _attributedStringWrapperData = data } else { fatalError("Failed to encode initial value using transformer") } } } import Foundation class AttributedStringWrapper: Codable { let attributedString: AttributedString enum MyCodingKey: CodingKey { case attributedStringKey } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: MyCodingKey.self) try container.encode(attributedString, forKey: .attributedStringKey, configuration: AttributeScopes.MyAppAttributes.self) } required init(from decoder: any Decoder) throws { let rootContainer = try decoder.container(keyedBy: MyCodingKey.self) attributedString = try rootContainer.decode(AttributedString.self, forKey: .attributedStringKey, configuration: AttributeScopes.MyAppAttributes.self) } init(_ attributedString: AttributedString) { self.attributedString = attributedString } } import Foundation class AttributedStringWrapperTransformer: ValueTransformer { override class func allowsReverseTransformation() -> Bool { return true } override class func transformedValueClass() -> AnyClass { return NSData.self } override func transformedValue(_ value: Any?) -> Any? { guard let wrapper = value as? AttributedStringWrapper else { return nil } do { let jsonData = try JSONEncoder().encode(wrapper) return jsonData as NSData } catch { print("Encoding failed: \(error.localizedDescription)") return nil } } override func reverseTransformedValue(_ value: Any?) -> Any? { guard let data = value as? NSData else { return nil } do { let wrapper = try JSONDecoder().decode(AttributedStringWrapper.self, from: data as Data) return wrapper } catch { print("Decoding failed: \(error.localizedDescription)") return nil } } }
Aug ’24
Reply to Custom AttributedString and SwiftData
Thank you for the updated reply. I tried implementing your code, as written, but it crashed. And unfortunately, the error messages were as enigmatic to me as before I contacted Apple Support. In any case, I'm also uncertain about what you meant in your concluding paragraph (beginning with "Alternatively"). My app needs to be able to access, retrieve, and modify the attributed string and its custom attributes frequently, and store the updated string on the fly. Wouldn't it then be imperative that I use a value transformer for this? I thought that was the reason the WWDC24 Apple engineer steered me in the direction of using a ValueTransformer. If you could provide code that integrates each of the steps needed for this, I would greatly appreciate it. I normally would try troubleshooting myself (I love problem solving), but the problems I've encountered are beyond my abilities. It's also a lot easier to learn code from a simple, working example.... Thanks for your help.
Aug ’24
Reply to Custom AttributedString and SwiftData
Hi Ziqiao, In my inquiry, I had emphasized that this problem is about persisting AttributedStrings with custom attributes. Merely persisting strings with standard attributes (at least of the NSAttributedString type) is not a problem. In any case, I tried implementing your first solution, and as you can see from the results in my test app (code pieces, below), even standard attributes are not persisted with this approach! Frankly, I don't need to store standard attributes, anyway. Accordingly, if you could focus your efforts on the problem of persisting specifically custom attributes, I would greatly appreciate it. Thanks! P.S. Why isn't there a way of uploading the app itself, instead of having to chop it up. I imagine it makes testing things on your end a bit less efficient. StringPersistenceApp.swift import SwiftUI @main struct StringPersistenceApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: PersistedString.self) } } Model.swift import SwiftData @Model class PersistedString { var attributedStringData: Data var attributedString: AttributedString { get { do { return try JSONDecoder().decode(AttributedString.self, from: attributedStringData) } catch { print("Failed to decode AttributedString: \(error)") return AttributedString("Failed to decode AttributedString: \(error)") } } set { do { attributedStringData = try JSONEncoder().encode(newValue) } catch { print("Failed to encode AttributedString: \(error)") } } } init(attributedString: AttributedString) { self.attributedStringData = try! JSONEncoder().encode(attributedString) } } ContentView.swift import SwiftData struct ContentView: View { @Environment(\.modelContext) private var modelContext: ModelContext @Query private var persistedString: [PersistedString] @State private var inputText: String = "" @State private var attributedString: AttributedString = AttributedString("") var body: some View { VStack(spacing: 20) { Text("Type something with the word 'red' in it.") TextField("Enter text", text: $inputText) .padding() .textFieldStyle(RoundedBorderTextFieldStyle()) Button("Save and Colorize") { attributedString = AttributedString(inputText) if let range = attributedString.range(of: "red") { attributedString[range].foregroundColor = .red } let newPersistedString = PersistedString(attributedString: attributedString) modelContext.insert(newPersistedString) try? modelContext.save() } .padding() .buttonStyle(.borderedProminent) if let lastPersistedString = persistedString.last { Text("Attributed string:") .font(.headline) Text(attributedString) .padding() .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(8) Text("Attributed string's description:") .font(.headline) Text(attributedString.description) .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(8) Text("Last persisted attributed string:") .font(.headline) Text(lastPersistedString.attributedString) .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(8) Text("Last persisted attributed string's description:") .font(.headline) Text(lastPersistedString.attributedString.description) .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(8) } else { Text("No string persisted yet.") .font(.subheadline) } } .padding() } } #Preview { ContentView() }
Aug ’24