Save custom Codable types in AppStorage or SceneStorage

Since iOS 14 we have two property wrappers for persisting state in SwiftUI: AppStorage and SceneStorage. AppStorage is usually used to persist some user specific settings and SceneStorage is designed for state restoration.

It's very easy to use these properties when we want to persist value types that they support out of the box, those are Bool, Int, Double, String, URL and Data. For example, we can save the user's chosen city in a weather app using AppStorage.

struct ContentView: View {
    @AppStorage("city") var city = ""

    var body: some View {
        TextField("Enter your city", text: $city)
    }
}

But we need to do a bit more work if we want to save a type that is not supported by AppStorage or SceneStorage by default. They both have initializers that accept a RawRepresentable where RawValue is of type Int or String. So we would need to add conformance to RawRepresentable to our custom type.

It's important to note though that we shouldn't be using AppStorage and SceneStorage as a replacement for a database, they are designed to save small pieces of data. But sometimes an app setting or a view state are represented by a custom type and this would be the use case described in this article.

As an example, we will look at a recipe app where the users can pin up to 3 recipes to the top of the list. You can get the full code for the example project from GitHub.

Recipe app screenshot with no pinned recipes Recipe app screenshot with three pinned recipes

The pinned recipes in our case are a user setting that should be persisted for the whole app, not per scene, so we will be using AppStorage. But the approach with RawRepresentable conformance can be used for SceneStorage as well.

Our custom type to be saved in AppStorage is an array of UUIDs which is Codable by default. You might need to add Codable conformance to your type manually.

typealias PinnedRecipes = [UUID]
Integrating SwiftUI into UIKit Apps by Natalia Panferova book coverIntegrating SwiftUI into UIKit Apps by Natalia Panferova book cover

Check out our book!

Integrating SwiftUI into UIKit Apps

Integrating SwiftUI intoUIKit Apps

UPDATED FOR iOS 17!

A detailed guide on gradually adopting SwiftUI in UIKit projects.

  • Discover various ways to add SwiftUI views to existing UIKit projects
  • Use Xcode previews when designing and building UI
  • Update your UIKit apps with new features such as Swift Charts and Lock Screen widgets
  • Migrate larger parts of your apps to SwiftUI while reusing views and controllers built in UIKit

Now we need to add RawRepresentable conformance to our custom Codable type. Remember, that AppStorage only supports RawRepresentable where the RawValue's associatedtype is of type Int or String. So the rawValue property has to return an Int or a String.

extension PinnedRecipes: RawRepresentable {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
            let result = try? JSONDecoder().decode(PinnedRecipes.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
    }
}

Once our type conforms to RawRepresentable we can use it with AppStorage or SceneStorage.

struct ContentView: View {
    @AppStorage("pinnedRecipes") var pinnedRecipes = PinnedRecipes()
    
    var body: some View {
        List {
            Section(header: Text("Pinned Recipes")) {
                ForEach(pinnedRecipes, id: \.self) { id in
                    ...
                }
            }
        }
    }
}

You can use this approach to save a custom struct, a dictionary or an enum with associated values.

You an get the full code for this article on GitHub.