I was playing around a bit with the new AttributedString and a few questions came up. I saw this other forum question "JSON encoding of AttributedString with custom attributes", but I did not completely understand the answer and how I would need to use it.
I created my custom attribute where I just want to store additional text like this:
enum AdditionalTextAttribute: CodableAttributedStringKey, MarkdownDecodableAttributedStringKey {
typealias Value = AttributedString
static let name = "additionalText"
}
I then extended the AttributeScopes
like this:
extension AttributeScopes {
struct MyAppAttributes: AttributeScope {
let additionalText: AdditionalTextAttribute
let swiftUI: SwiftUIAttributes
}
var myApp: MyAppAttributes.Type { MyAppAttributes.self }
}
and I also implemented the AttributeDynamicLookup
like this:
extension AttributeDynamicLookup {
subscript<T: AttributedStringKey>(dynamicMember keyPath: KeyPath<AttributeScopes.MyAppAttributes, T>) -> T { self[T.self] }
}
So next I created my AttributedString and added some attributes to it:
var attStr = AttributedString("Hello, here is some text.")
let range1 = attStr.range(of: "Hello")!
let range2 = attStr.range(of: "text")!
attStr[range1].additionalText = AttributedString("Hi")
attStr[range2].foregroundColor = .blue
attStr[range2].font = .caption2
Next I tried to create some JSON from my string and took a look at it like this:
let jsonData = try JSONEncoder().encode(attStr)
print(String(data: jsonData, encoding: .utf8) ?? "no data")
//print result: ["Hello",{},", here is some ",{},"text",{"SwiftUI.ForegroundColor":{},"SwiftUI.Font":{}},".",{}]
I guess it makes sense, that both SwiftUI.ForegroundColor
and SwiftUI.Font
are empty, because they both do not conform to Codable
protocol.
My first question would be: Why does my additionalText
attribute not show up here?
I next tried to extend Color
to make it codable like this:
extension Color: Codable {
enum CodingKeys: CodingKey {
case red, green, blue, alpha
case desc
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
guard let cgColor = self.cgColor,
let components = cgColor.components else {
if description.isEmpty { throw CodingErrors.encoding }
try container.encode(description, forKey: .desc)
return
}
try container.encode(components[0], forKey: .red)
try container.encode(components[1], forKey: .green)
try container.encode(components[2], forKey: .blue)
try container.encode(components[3], forKey: .alpha)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let description = try container.decodeIfPresent(String.self, forKey: .desc) {
if description == "blue" {
self = Color.blue
return
}
throw CodingErrors.decoding
}
let red = try container.decode(CGFloat.self, forKey: .red)
let green = try container.decode(CGFloat.self, forKey: .green)
let blue = try container.decode(CGFloat.self, forKey: .blue)
let alpha = try container.decode(CGFloat.self, forKey: .alpha)
self.init(CGColor(red: red, green: green, blue: blue, alpha: alpha))
}
}
But it looks like even though Color
is now codable, the encoding function does not get called when I try to put my attributed string into the JSONEncoder
.
So my next question is: Does it just not work? Or do I also miss something here?
Coming to my last question: If JSONEncoder does not work, how would I store an AttributedString to disk?
I see now what I did wrong. I need to JSON encode the wrapper struct which holds the attributed string, so this can then use my custom attributes.
Here is my complete example. Maybe it also helps someone else:
import SwiftUI
extension AttributeScopes {
var myApp: MyAppAttributes.Type { MyAppAttributes.self }
struct MyAppAttributes: AttributeScope {
let alternativeGreeting: AlternativeGreetingTextAttribute
let swiftUI: SwiftUIAttributes
}
}
extension AttributeDynamicLookup {
subscript<T: AttributedStringKey>(dynamicMember keyPath: KeyPath<AttributeScopes.MyAppAttributes, T>) -> T { self[T.self] }
}
extension AttributeScopes.MyAppAttributes {
enum AlternativeGreetingTextAttribute: CodableAttributedStringKey {
typealias Value = String
static let name = "alternativeGreeting"
}
}
// Wrapper struct to also encode custom attributes
struct CodableType: Codable {
@CodableConfiguration(from: \.myApp)
var attrStr = AttributedString()
}
var attStr = AttributedString("Hello Max.")
attStr[attStr.range(of: "Hello")!].alternativeGreeting = "Hi"
attStr[attStr.range(of: "Max")!].personNameComponent = .givenName
// Decode to JSON, but use wrapper struct to recognize custom attributes
let jsonData = try JSONEncoder().encode(CodableType(attrStr: attStr))
print(String(data: jsonData, encoding: .utf8) ?? "no data")
print("-------")
// From JSON back to AttributedString
print(try JSONDecoder().decode(CodableType.self, from: jsonData).attrStr)