It’s common when referring to strings from multiple places to create a centralized location where they are defined. For keys used for accessing UserDefaults values, that may be a static property in an enum, which is used to act as a namespace for user default keys.
This allows you to use this property rather than a string literal, so that the compiler can help ensure the wrong value is not used, by telling you you have mis-spelled the name or some such.
Another reason for defining the key as a static property like this is that because our custom UserDefaults methods reference the Key type, Swift can infer the Key type and we can use a shorter dot syntax for more readable code.
While the code now helps with defining keys, it does nothing to help the compiler understand what value type is expected for a given key. The following will be accepted by the compiler despite being a bug.
To prevent unknown misuse, we can use a generic parameter to specify a type for the value that the key expects to be and use that to retrieve and store values to UserDefaults.
It’s perhaps interesting to note that the generic parameter of Value isn’t actually used anywhere within the body of the Key type. This is what’s known as a Phantom type, which surely makes this approach even better!
Using our new Key type, we can provide a stronger-typed API for UserDefaults. We specify Value as a generic parameter on the function so that we can use that type information in multiple places in the function definition.
The first function retrieves values from UserDefaults and says we take a key which has a Value type which is the same as the type of the returned value.
The second will only take a value which is of the same type as the key’s Value type.
The final one is a convenience where the generic parameter needs to be defined to be valid, but which isn’t really utilised.
As we did before, we can define keys as static properties of the Key type, but now we must define the type of Value in the where clause of the extension otherwise Swift won’t know what Value should be. This becomes the definition for the type of Value represented by this key.
Due to the generic Value of the key, the call to value(for:) knows that it will be returned a value of Int type.
The call to set the value for the key is type-checked by the compiler, so that you cannot pass a non-integer value for this key, so the following line will fail to compile.
error: member ‘launchCount’ in ‘UserDefaults.Key<_>’ produces result of type ‘UserDefaults.Key<Int>’, but context expects ‘UserDefaults.Key<_>’
Our error message here is unfortunately a little backwards. Because we have passed a String in for the new value, the compiler complains that the launchKey has the wrong type. However because we trust the type of launchKey, we are alerted that we are actually trying to set a value of an incorrect type.
Since its release, Swift has provided ways to make nicer, stronger-typed and safer APIs than were previously afforded to us in Objective-C. These make our codebases easier to read and allow the compiler to help us make the correct choices, reducing the potential for incorrect outcomes before the code is even run.
For me, the biggest benefit of Swift is its type-safety showing us incorrect states at compile time, rather than having to discover bugs at more costly stages of development such as unit tests, quality assurance or the worst outcome, a user discovering it.