Whilst adding some settings to The Stack1, I was reminded of something that has bugged me for a while about SwiftUI’s @AppStorage.

In a classic act of yak shaving, I decided to fix the annoyance instead of doing the task I actually set out to do…

Is That The DRY-Violation Klaxon I Hear?

SwiftUI’s @AppStorage macro is a neat way to access a UserDefaults value from within a SwiftUI view2

You use it like this:

struct ContentView: View {
    @AppStorage("username") var username: String = "Anonymous"

    var body: some View {
        VStack {
            Text("Welcome, \(username)!")

            Button("Log in") {
                username = "@samdeane"
            }
        }
    }
}

The ability to declare a default value is useful for the first time the user runs the app.

Unfortunately though, for most apps of a non-trivial size, you are likely to have some user-interface code in one place to allow the user to change the setting, and some other UI or application logic elsewhere that reads the value.

Which leaves you with something more like this:


struct SettingsView: View {
    @AppStorage("doTheThing") var doTheThing: String = true

    var body: some View {
        Toggle(isOn: $doTheThing) {
          Text("Show the button to do the thing?")
        }
    }
}

struct AppLogicView: View {
    @AppStorage("doTheThing") var doTheThing: String = false

    var body: some View {
      if doTheThing {
        Button("Do The Thing!) {
          performTheThing()
        }
      }
    }
}

Shome Mishtake Shurely?

Did you spot my deliberate mistake?

I set the default to true in one view, and false in the other.

My assumption is that what will actually happen here will depend on which view the user visits first, in the situation where they don’t already have a value set for the setting.

Which is… non-optimal.

It also bothers me that I have to type the "doTheThing" key in both places.

I might change it or get it wrong in one place and not the other.

I can even potentially use a different type for the same setting in different places.

A Better Way™

After thinking about this for a bit, I added the following extension3:

import SwiftUI

public struct AppStorageKey<Value> {
  let key: String
  let defaultValue: Value
  
  public init(_ key: StringLiteralType, defaultValue: Value) {
    self.key = key
    self.defaultValue = defaultValue
  }
  
}

public extension AppStorage {
  init(_ key: AppStorageKey<Value>, store: UserDefaults? = nil) where Value == Bool {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey<Value>, store: UserDefaults? = nil) where Value == Int {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey<Value>, store: UserDefaults? = nil) where Value == Double {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey<Value>, store: UserDefaults? = nil) where Value == String {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey<Value>, store: UserDefaults? = nil) where Value == URL {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey<Value>, store: UserDefaults? = nil) where Value == Date {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
  
  init(_ key: AppStorageKey<Value>, store: UserDefaults? = nil) where Value == Data {
    self.init(wrappedValue: key.defaultValue, key.key, store: store)
  }
}

I can use this in my application:


public extension AppStorageKey where Value == Bool {
  static let doTheThing = AppStorageKey("doTheThing", defaultValue: false)
}

struct SettingsView: View {
    @AppStorage(.doTheThing) var doTheThing

    var body: some View {
        Toggle(isOn: $doTheThing) {
          Text("Show the button to do the thing?")
        }
    }
}

struct AppLogicView: View {
    @AppStorage(.doTheThing) var doTheThing

    var body: some View {
      if doTheThing {
        Button("Do The Thing!) {
          performTheThing()
        }
      }
    }
}

These declarations can all live in different swift files (or even different packages).

I can define the settings key, type and default value once.

I get type inference everywhere that I use @AppStorage in this way, so I don’t have to declare a type.

As a bonus, when I type @AppStorage(. Xcode will auto-suggest any static AppStorageKey values that it knows about.

Which is nice…

  1. I’m still looking for Test Flight testers for this. If you’d be up for giving it a go - even just to help me out by confirming that it doesn’t immediately explode, then please sign up

  2. Or modifier, command, observable object, or various other SwiftUI structures. 

  3. The repetition here with different types is ugly, but it mirrors the way that AppStorage defines its own API. It would be nice if there was a better way, but I’ve not found it yet.