@AppStorage and @SceneStorage are two SwiftUI property wrappers that have been introduced this year.

Since both are backed by plists behind the scenes, out of the box we can use them with the following types: Bool, Int, Double, String, URL and Data.

What about other types?

RawRepresentable Types

Both @AppStorage and @SceneStorage offer two initializers accepting values conforming to the RawRepresentable protocol with an associated type RawValue of either Int or String:

extension AppStorage {
  public init(
    wrappedValue: Value, 
    _ key: String, 
    store: UserDefaults? = nil
  ) where Value: RawRepresentable, Value.RawValue == Int

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

extension SceneStorage {
  public init(
    wrappedValue: Value, 
    _ key: String, 
  ) where Value: RawRepresentable, Value.RawValue == Int

  public init(
    wrappedValue: Value, 
    _ key: String, 
  ) where Value: RawRepresentable, Value.RawValue == String
}

If you would like to dig deeper into RawRepresentable, I recommend this NSHipster article by Mattt.

These initializers make it easy to store types such as enums:

// RawValue == Int
enum Fruit: Int, Identifiable, CaseIterable {
  case banana
  case orange
  case mango

  var id: Int { rawValue }
}

struct ContentView: View {
  @AppStorage("fruit") private var fruit: Fruit = .mango

  var body: some View {
    Picker("My Favorite Fruit", selection: $fruit) {
      ForEach(Fruit.allCases, id: \.self) {
        Text("\($0)" as String)
      }
    }.pickerStyle(SegmentedPickerStyle())
  }
}

// RawValue == String
enum Fruit: String, Identifiable, CaseIterable {
  case banana
  case orange
  case mango

  var id: String { rawValue }
}

struct ContentView: View {
  @AppStorage("fruit") private var fruit: Fruit = .mango

  var body: some View {
    Picker("My Favorite Fruit", selection: $fruit) {
      ForEach(Fruit.allCases, id: \.self) {
        Text($0.id)
      }
    }.pickerStyle(SegmentedPickerStyle())
  }
}

While this is great, our apps most likely need to store a set of settings/preferences:
we could store each one of them separately, but we’d probably rather have an easy way to fetch and store a Codable instance instead, how can we do so?

Codable Types

Imagine to have a Preference struct, with all our app settings:

enum Appearance: String, Codable, CaseIterable, Identifiable {
  case dark
  case light
  case system

  var id: String { rawValue }
}

struct Preferences: Codable {
  var appearance: Appearance
  // TODO: add more settings here
}

How can we use @AppStorage and @SceneStorage with our Codable type?
Unfortunately so far this doesn’t seem to be possible, if you have found a way, please let me know.

There are some smart people working on it.

In the meantime, we can take up the challenge and solve it ourselves.

There are many ways to approach this, one of the simplest is probably extending @Published:

private var cancellableSet: Set<AnyCancellable> = []

extension Published where Value: Codable {
  init(wrappedValue defaultValue: Value, _ key: String, store: UserDefaults? = nil) {
    let _store: UserDefaults = store ?? .standard

    if
      let data = _store.data(forKey: key),
      let value = try? JSONDecoder().decode(Value.self, from: data) {
      self.init(initialValue: value)
    } else {
      self.init(initialValue: defaultValue)
    }

    projectedValue
      .sink { newValue in
        let data = try? JSONEncoder().encode(newValue)
        _store.set(data, forKey: key)
      }
      .store(in: &cancellableSet)
  }
}

Credits to Victor Kushnerov for the original implementation.

Since @Published doesn’t have the same kind of limitations of @AppStorage and @SceneStorage, we can extend it with this new initializer where:

  • the first part sets the initial value (either the one currently stored or the passed default value)
  • the second part creates an observer that will update UserDefaults every time our @Published value changes.

Thanks to this new initializer @Published behaves similarly to @AppStorage, but for Codable types (the same approach can be used to bring @AppStorage support to iOS 13).

We can now go back to our ContentView and use our Codable Preference type:

class ContentViewModel: ObservableObject {
  @Published("userPreferences") var preferences = Preferences(appearance: .system)
}

struct ContentView: View {
  @StateObject var model = ContentViewModel()

  var body: some View {
    Picker("Appearance", selection: $model.preferences.appearance) {
      ForEach(Appearance.allCases, id: \.self) {
        Text(verbatim: $0.rawValue)
      }
    }.pickerStyle(SegmentedPickerStyle())
  }
}

It’s not as succinct as an @AppStorage variable definition, but definitely not too bad.

In a similar fashion we can take care of @SceneStorage where, instead of UserDefaults, we pass an UISceneSession instance:

private var cancellableSet: Set<AnyCancellable> = []

extension Published where Value: Codable {
  init(wrappedValue defaultValue: Value, _ key: String, session: UISceneSession) {
    if
      let data = session.userInfo?[key] as? Data,
      let value = try? JSONDecoder().decode(Value.self, from: data) {
      self.init(initialValue: value)
    } else {
      self.init(initialValue: defaultValue)
    }

    projectedValue
      .sink { newValue in
        let data = try? JSONEncoder().encode(newValue)
        session.userInfo?[key] = data
      }
      .store(in: &cancellableSet)
  }
}

The final gist can be found here.

Conclusions

@AppStorage and @SceneStorage are two very welcome SwiftUI additions, unfortunately they support the same types supported by plists, however in this article we’ve seen how can can extend SwiftUI to take care of other types as well.

Do you use a different approach? Have you found a way to directly extend @AppStorage and @SceneStorage? Please let me know!

Thank you for reading!

⭑⭑⭑⭑⭑