Property Wrappers and User Defaults

I love magical new features that help us abstract away boiler-plate code and let us focus on the unique features of our apps. Property wrappers is such a feature, and seriously – it just feels like magic!

mervyn-chan-RFXxBTHze_M-unsplash

Let’s take a look at an example to see how property wrappers work.

User defaults: Level 1

A reminder, the simplest way user defaults would work is to first set up some sort of a key:

enum Keys { //enum to prevent instantiation
  static let beenHereBefore = "Been Here Before"
}

You could then use this to store or retrieve the user default:

//Retrieve user default:
if !UserDefaults.standard.bool(forKey: Keys.beenHereBefore) {
  //Onboarding could go here for example
}
//Store user default:
UserDefaults.standard.set(true, forKey: Keys.beenHereBefore)

Great! But wouldn’t it be nicer if we could just set and retrieve the property without worrying about the underlying user defaults implementation?

User defaults: Level 2

Instead, let’s abstract away the User defaults code into a GlobalSettings type:

//Retrieve user default:
if !GlobalSettings.beenHereBefore {
  //Onboarding could go here for example
}
//Store user default:
GlobalSettings.beenHereBefore = true

Much more readable, right?

But of course, we now need to set up the GlobalSettings type:

enum GlobalSettings { //enum to prevent instantiation
    enum Keys {
        static let beenHereBefore = "Been Here Before"
    }
    static var beenHereBefore:Bool {
        get {
            return UserDefaults.standard.bool(forKey: Keys.beenHereBefore)
        }
        set {
            UserDefaults.standard.set(newValue, forKey: Keys.beenHereBefore)
        }
    }
}

Now that’s fine for one user default, but what if we have several? We would have to duplicate this code defining a key and setting and retrieving the UserDefaults for each property.
Could we abstract this code even further? Well, here’s where we can see the magic of property wrappers!

Screenshot 2019-09-02 21.01.33

User defaults: Boss Level! (using Property Wrappers)

A property wrapper is a special attribute you can create and apply to a property that automatically runs a bunch of code behind the scenes for the property.

Let’s move the User Defaults get and set code to a property wrapper.
*Disclaimer: the code for Persist is based on sample code in the Swift Evolution proposal for Property Wrappers, proposal 258.

Let’s give the property wrapper the name Persist:

@propertyWrapper
struct Persist<T> {
  let key: String
  let defaultValue: T

  var wrappedValue: T {
    get {
      return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
    }
    set {
      UserDefaults.standard.set(newValue, forKey: key)
    }
  }
}

To define the property wrapper we follow three essential steps:

1. Create a type (in this case, a struct). The name of our type becomes the name of the property wrapper.
2. Prefix the type with the @propertyWrapper attribute.
3. Include a ​wrappedValue property. In this case this a generic property wrapper, so the wrappedValue property is defined as a generic. To perform an action when this property is retrieved or stored, it is defined as a computed property with both a getter and a setter.

In this case, as UserDefaults needs a key to store and retrieve a property, a key property has been defined in the Persist struct.

As the property wrapper is defined as generic, it uses UserDefault‘s generic object method to retrieve a user default. As this method returns an optional, the Persist struct also defines a defaultValue property that will be returned if the value returned by the object method is nil.

Of course, as Persist is a struct, it doesn’t need an initializer specifically defined to intialize these two properties, as a memberwise initializer will be automatically generated.

Now, we can adjust our GlobalSettings type to use the property wrapper:

enum GlobalSettings {
  @Persist(key: "BeenHereBefore", defaultValue: false)
  static var beenHereBefore: Bool
}

Holy moly. You can see how much shorter the declaration of the beenHereBefore property is, now that all of the UserDefaults code has been abstracted to our custom property wrapper Persist.

All we did to add our custom property wrapper was to prefix a variable with an at symbol (@) followed by the name of the wrapper (Persist), followed by any initialization required.
Like magic, the property wrapper code we wrote will now execute for this variable!

We could easily add a bunch of user defaults to our GlobalSettings type, with minimal additional lines of code.

enum GlobalSettings {
  @Persist(key: "BeenHereBefore", defaultValue: false)
  static var beenHereBefore: Bool
  @Persist(key: "TopScore", defaultValue: 0)
  static var topScore: Int
  @Persist(key: "UserName", defaultValue: "Anon")
  static var userName: String
}

As all the code dealing with persisting data is now in our Persist property wrapper, if we wanted to make adjustments to how this works, we would only have to make this change in one place. Let’s say we change our mind and decide to use iCloud’s NSUbiquitousKeyStore container to persist our data, instead of UserDefaults. Making this change would be straight-forward:

@propertyWrapper
struct Persist {
  let key: String
  let defaultValue: T

  var wrappedValue: T {
    get {
        return NSUbiquitousKeyValueStore.default.object(forKey: key) as? T ?? defaultValue
    }
    set {
        NSUbiquitousKeyValueStore.default.set(newValue, forKey: key)
    }
  }
}

As I mentioned, you can read more about property wrappers in Swift Evolution proposal 258. You can also see more about it in WWDC video Modern Swift API Design. Just be cautious, back then wrappedValue was simply known as value. (it was renamed 8 days after the talk – ooh that would be frustrating!)

Enjoy playing with property wrappers, let me know what magical property wrappers you create!

iOS development with Swift - book: https://manning.com/books/ios-development-with-swift video course: https://www.manning.com/livevideo/ios-development-with-swift-lv

Tagged with: ,
Posted in Swift

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: