Storing Codable types in SwiftUI AppStorage

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 types in this post comes from this article, where Natalia uses a similar approach to extend all codable Arrays and Dictionaries.

Updated

This post was updated June 10, 2025, and now shows how to use the StorageValue instead of the old StorageCodable protocol, which stopped working in an earlier version of SwiftUI.

The 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, it can’t be used with 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

In her article, Natalia makes arrays and dictionaries with Codable types implement RawRepresentable 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 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 these 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.

The Solution

My first approach was to make all codable types automatically implement RawRepresentable, with an extension 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, this stopped working at one point, where the type kept calling itself, which caused infinite recursions. So I now use a generic StorageValue instead:

public struct StorageValue<Value: Codable>: RawRepresentable {

    /// Create a storage value.
    public init(_ value: Value? = nil) {
        self.value = value
    }

    /// Create a storage value with a JSON encoded string.
    public init?(rawValue: String) {
        guard
            let data = rawValue.data(using: .utf8),
            let result = try? JSONDecoder().decode(Value.self, from: data)
        else { return nil }
        self = .init(result)
    }

    /// The stored value.
    public var value: Value?
}

public extension StorageValue {

    /// Whether the storage value contains an actual value.
    var hasValue: Bool {
        value != nil
    }

    /// A JSON string representation of the storage value.
    var jsonString: String {
        guard
            let data = try? JSONEncoder().encode(value),
            let result = String(data: data, encoding: .utf8)
        else { return "" }
        return result
    }

    /// A JSON string representation of the storage value.
    var rawValue: String {
        jsonString
    }
}

By having a separate type implementing RawRepresentable, we can get around the infinite recursion.

We can now persist any codable type in AppStorage or SceneStorage by wrapping it in StorageValue:

struct MyView: View {

    @AppStorage("com.myapp.user")
    var userValue = StorageValue<User>()

    var user: User? { userValue.value }

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

        Button("Toggle user") {
            let hasValue = userValue.hasValue
            let daniel = User(name: "Daniel", age: 46)
            userValue.value = hasValue ? nil : daniel
        }
    }
}

An important thing to keep in mind is that JSON encoding dynamic values like Color to raw data may change the value. In the color case, storing the value will remove light & dark mode support.

Conclusion

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

The StorageValue type in this post makes it possible to use any codable type with AppStorage and SceneStorage. It’s available in my SwiftUIKit open-source project, if you want to give it a try.

Discussions & More

If you found this interesting, please share your thoughts on Bluesky and Mastodon. Make sure to follow to be notified when new content is published.