onmyway133 / blog

🍁 What you don't know is what you haven't learned
https://onmyway133.com/
MIT License
679 stars 33 forks source link

How to store Codable in AppStorage #949

Open onmyway133 opened 1 year ago

onmyway133 commented 1 year ago

AppStorage and SceneStorage accepts RawRepresentable where value is Int or String.

Creates a property that can read and write to a string user default, transforming that to RawRepresentable data type.

init(wrappedValue:_:store:)

init(
    wrappedValue: Value,
    _ key: String,
    store: UserDefaults? = nil
) where Value : RawRepresentable, Value.RawValue == String

One clever thing (that does not work) is to use a custom Codable type that conforms to RawRepresentable, like below

public protocol RawCodable: Codable, RawRepresentable {}

public extension RawCodable {
    init?(rawValue: String) {
        guard
            let data = rawValue.data(using: .utf8),
            let result = try? JSONDecoder().decode(Self.self, from: data)
        else {
            return nil
        }

        self = result
    }

    var rawValue: String {
        guard
            let data = try? JSONEncoder().encode(self),
            let result = String(data: data, encoding: .utf8)
        else { 
            return ""
        }

        return result
    }
}

struct Book: RawCodable {}

struct BooksView: View {
    @AppStorage("favorites") var favorite: Book?

    var body: some View {
        List {

        }
    }
}

The above will cause infinite loop as JSONEncoder will use rawValue to encode, see Codable.swift

extension RawRepresentable<String> where Self: Encodable {
    /// Encodes this value into the given encoder, when the type's `RawValue`
    /// is `String`.
    ///
    /// This function throws an error if any values are invalid for the given
    /// encoder's format.
    ///
    /// - Parameter encoder: The encoder to write data to.
    public func encode(to encoder: any Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(self.rawValue)
    }
}

To avoid this, we can implement our own encode(to:) to avoid infinite loop.

Another way is to introduce a container struct that conforms to RawRepresentable

public struct RawCodable<Value: Codable>: RawRepresentable {
    public var wrappedValue: Value

    public init(
        wrappedValue: Value
    ) {
        self.wrappedValue = wrappedValue
    }

    public init?(rawValue: String) {
        guard 
            let data = rawValue.data(using: .utf8),
            let value = try? JSONDecoder().decode(Value.self, from: data)
        else {
            return nil
        }

        self.wrappedValue = value
    }

    public var rawValue: String {
        guard 
            let data = try? JSONEncoder().encode(wrappedValue),
            let string = String(data: data, encoding: .utf8)
        else {
            return ""
        }

        return string
    }
}