Storing Codable types in SwiftUI AppStorage

Aug 23, 2023 · Follow on Twitter and Mastodon

SwiftUI keeps evolving, but there are still some missing things. 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 in this post comes from this article, where Natalia uses the same approach to extend all codable Arrays and Dictionaries.

The basic problem

Let’s say that you have a Codable User struct:

struct User: Codable {

    let name: String
    let age: Int
}

Although 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)
    }
}

One way that we can make this work, is to take a look at the RawRepresentable protocol.

The inspiration to this solution

In her article, Natalia makes arrays and dictionaries that contain Codable types implement the RawRepresentable protocol with these array and dictionary extensions:

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 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, since there’s no AppStorage support for a single Codable.

How to make Codable support AppStorage & SceneStorage

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

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, since not all Codable types may prefer JSON, I created a separate protocol that extends Codable and implements RawRepresentable with the JSON code from above:

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, is that JSON coding may affect values. For instance, JSON encoding a dynamic Color to raw data will remove light & dark mode support.

Conclusion

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

The StorageCodable protocol in this post makes it possible to persist codable types with AppStorage and SceneStorage, using JSON for encoding and decoding.

StorageCodable is available in my SwiftUIKit open-source project. I hope that you like it.