Blog

Store Codable types in AppStorage

Aug 23, 2023 swiftui

SwiftUI keeps evolving, but there are still some things that we have to write custom code for. Today, let’s see how we can extend Codable to make it possible to persist it in AppStorage and SceneStorage.

The inspiration to the StorageCodable protocol presented in this post came from this article, where Natalia Panferova uses the same approach to extend all codable Arrays and Dictionaries.

Let’s say that you have the following codable User struct:

struct User: Codable {

    let name: String
    let age: Int
}

Although you this type can automatically be encoded and decoded in various ways, it can’t be persisted in AppStorage or SceneStorage. This means that you can’t do this:

struct MyView: View {

    @AppStorage("com.myapp.user")
    var user: User?

    var body: some View {
        Text(user?.name)
    }
}

Let’s see how we can use RawRepresentable to make this possible.

The inspiration to this solution

In her article, Natalia uses the RawRepresentable support that was added in iOS 15, by making arrays and dictionaries that contain Codable types implement the protocol:

extension Array: RawRepresentable where Element: Codable {

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

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

extension Dictionary: RawRepresentable where Key: Codable, Value: Codable {

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

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

This makes it possible to use codable arrays and dictionaries with AppStorage and SceneStorage:

struct MyView: View {

    @AppStorage("com.myapp.users")
    var users: [User] = []

    var body: some View {
        Text(users.first?.name)
    }
}

However, since the extensions only apply to arrays and dictionaries, we still can’t do this:

struct MyView: View {

    @AppStorage("com.myapp.user")
    var user: User?

    var body: some View {
        Text(user?.name)
    }
}

This will make SwiftUI complain that it doesn’t understand what you’re trying to do, since there is no AppStorage implementation that takes a Codable type, while there is one for RawRepresentable.

How to make Codable support AppStorage and SceneStorage

Since Natalia’s approach is based on Codable, we could fix this by extending Codable like this:

extension Codable: RawRepresentable {
    
    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
    }
}

However, not all Codable types may prefer to use JSON, I’ve added a separate protocol for this:

public protocol StorageCodable: Codable, RawRepresentable {}

public extension StorageCodable {
    
    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
    }
}

All you have to do now, is to make User implement StorageCodable instead of Codable:

struct User: StorageCodable {

    let name: String
    let age: Int
}

With this small change, this will now finally work:

struct MyView: View {

    @AppStorage("com.myapp.user")
    var user: User?

    var body: some View {
        Text(user?.name)
    }
}

One important thing to keep in mind with this approach, is that JSON encoding may affect the encoded values. For instance, JSON encoding a dynamic Color value to a raw data representation, could remove any light and dark mode support from the color when it’s decoded.

Conclusion

SwiftUI is becoming more and more capable, but we still have to write custom code for some things, such as making Codable work with SwiftUI’s persistency stores.

The StorageCodable protocol that was presented in this post makes it possible to persist any type in AppStorage and SceneStorage, using JSON for encoding and decoding.

StorageCodable is available in the brand new SwiftUIKit 3.6 release. I hope that you find it useful.