skip to Main Content

Swift 5.5 introduced AttributedString conforming to Codable however this is not working for me as I’d expect.

Take the following example:

Here I define a custom attribute TextCase

public enum TextCase: Codable {
    case lowercase
    case uppercase
}

public struct TextCaseAttribute: CodableAttributedStringKey {
    public typealias Value = TextCase
    public static var name = "TextCaseAttribute"
}

public extension AttributeScopes {
    struct ExtendedTextAttributes: AttributeScope {
        public let textCase: TextCaseAttribute
        public let foundation: FoundationAttributes
    }
    var customAttributes: ExtendedTextAttributes.Type { ExtendedTextAttributes.self }
}

public extension AttributeDynamicLookup {
    subscript<T: AttributedStringKey>(
        dynamicMember keyPath: KeyPath<AttributeScopes.ExtendedTextAttributes, T>
    ) -> T {
        get { self[T.self] }
    }
}

and use it like this:

    var hello: AttributedString {
        var result = AttributedString("Hello")
        result.textCase = .uppercase
        result.font = .largeTitle
        return result
    }

when I use JSONEncoder to encode the AttributedString it gives me the following:

po String(decoding: JSONEncoder().encode(hello), as: UTF8.self)
->
"["Hello",{"SwiftUI.Font":{}}]"

Notice that my textCase is missing plus I expected SwiftUI.Font to have a value.

2

Answers


  1. Chosen as BEST ANSWER

    Implementing custom encoding and decoding seems to work:

    struct NonCodableType: Hashable {
        var inner: String
    }
    
    extension AttributeScopes {
        enum CustomCodableAttribute: CodableAttributedStringKey {
            typealias Value = NonCodableType
            static let name = "NonCodableConvertible"
    
            static func encode(_ value: NonCodableType, to encoder: Encoder) throws {
                var c = encoder.singleValueContainer()
                try c.encode(value.inner)
            }
    
            static func decode(from decoder: Decoder) throws -> NonCodableType {
                let c = try decoder.singleValueContainer()
                let inner = try c.decode(String.self)
                return NonCodableType(inner: inner)
            }
        }
    }
    
    extension AttributeDynamicLookup {
        subscript<T: AttributedStringKey>(
            dynamicMember keyPath: KeyPath<AttributeScopes.CustomCodableAttribute, T>
        ) -> T {
            self[T.self]
        }
    }
    
    struct UnifiedAttributes: AttributeScope {
        var customCodable: AttributeScopes.CustomCodableAttribute
    }
    
    struct CodableType: Codable {
        @CodableConfiguration(from: UnifiedAttributes.self)
        var attributedString = AttributedString()
    }
    

    print it like this

    po String(decoding: JSONEncoder().encode(CodableType(attributedString: hello)), as: UTF8.self)
    

    Source: Apple swift-corelibs-foundation test code here https://github.com/apple/swift-corelibs-foundation/blob/main/Tests/Foundation/Tests/TestAttributedString.swift

    still curious why encoding a Codable type won't work.


  2. This solution is for NSAttributedString but you can convert it to AttributedString with init function:
    https://developer.apple.com/forums/thread/682431

    You can packing the NSAttributedString in a Codable container, and the container is Data: you can convert the NSAttributedString to Data. Because Data is codable so your custom Struct will able to be codable:

    Code to convert NSAttributedString to Data:

    extension NSAttributedString {
       
        func data() throws -> Data { try 
        NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) }
    
    }
    

    Code to convert Data back to NSAttributedString:

    extension Data {
        func topLevelObject() throws -> Any? { try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(self) }
        func unarchive<T>() throws -> T? { try topLevelObject() as? T }
        func attributedString() throws -> NSAttributedString? { try unarchive() }
    }
    

    Add it to your custom Struct and make the whole struct Codable:

    struct CustomStruct : Codable {
        
        /// NSAttributedString in Data
        var attStringData : Data
    }
    

    Pack it in a computedVariable for better syntax:

    extension CustomStruct {
        var string : NSAttributedString? {
            get {
                return attStringData.attributedString()
            }
            set {
                let attData =  newValue.data()
                attStringData = attData
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search