With the upcoming release of the third major version of Swift, massive improvements are coming to the language and we are beginning to see the chains being broken on some of the shackles of Objective-C’s legacy. However a lot of these improvements still rely on “Stringly typed” APIs which have the potential to trip us up when developing apps.
This talk from try! Swift will look into how we can avoid using these APIs by replacing them with alternatives that make our code more readable, safer, intentional, and Swifty.
What is a Stringly Typed API? (0:18)
It’s a riff on the term “strongly typed” that is used to describe an implementation which needlessly relies on strings when programmer- and refactor-friendly options are available.
// Stringly typed
let image = UIImage(named: "PalletTown")
// Strongly typed
let tableView = UITableView(style: .Plain)
Two examples above, where one is a stringly typed. If you’ve worked with UIImage
initializer, you know that it takes a String, and that String corresponds to something in your resources. Whereas the UITableView
initializer uses an enum; you can only instantiate it with a Plain or Group style.
Downsides (0:51)
Typos
When we are left to our own devices and we don’t have the precompiler, we tend to write a lot of typos. For example, I always misspell the word “notification”.
let notification = "userAlertNotificaton"
Side Effects
Here we have our UIImage
initializer, but I’ve misspelled “Charmander”. That’s a side effect, because it can return nil.
let image = UIImage(named: "Charmandr")
Collisions
If we have user default, so part A of our app is setting isUserLoggedIn
to false, and part B is setting isUserLoggedIn
to true. Those two different parts of the app don’t really know about each other, but at the same time they’re saving different values to the same key. You could get unintended side effects from that.
let keyA = "isUserLoggedIn"
let keyB = "isUserLoggedIn"
keyA == keyB
Runtime Crashes
With TableViews before iOS 5, we didn’t have to register our class. But when iOS 5 came out, when we wanted to display UITableViewCell
, we’d have to register to the cell and give it a reuse identifier. Then we go down into our UITableView
function, and we have .dequeueReusableCellWithIdenifier
.
Here, I’ve misspelled “CellIdentifier”, so that’s going to go as a runtime crash, and nobody wants that.
tableView.registerClass(UITableViewCell.self,
forCellReuseIdentifier: "CellIdentifer")
...
let cell: UITableViewCell = tableView
.dequeueReusableCellWithIdentifier("CelIdentifer")
Unicode Artifacts
If you copy strings from unsanitized sources, such as a rich-text editor or iMessage, you can bring stuff into your source code that at first glance looks like nothing. An emoji in Xcode, for example, may appear like there’s nothing there.
The good thing about this is that it only affects Objective-C. Swift precompiler tells us, “Hey, you had this illegal character, you should fix it.”
Disorderly
Some people thing stringly typed is disorderly. Here we have title casing versus camel casing, and if you’re OCD like me, this keeps you up at night.
- Title Cased
"IsUserLoggedIn"
- Camel Cased
"isUserLoggedIn"
- Prefixes
Sometimes we use prefixes, which are good because it allows us to name-space our keys. With some prefixing, we don’t get collisions. In the final example, we might even prefix with our bundle identifier. We would do this when we might collide with an external framework.
"isUserLoggedIn"
"Account.isUserLoggedIn"
"com.andyyhope.Account.isUserLoggedIn"
- Naming
See here we have three different ways to spell a key.
"isUserBlocked"
"userBlocked"
"hasUserBeenBlocked"
We’re mixing tenses, and they’re everywhere. UIImage, UIKit, and Foundation are especially big culprits of these.
UserDefaults (4:17)
Quick recap, what is UserDefaults? It’s used to store small, persistent pieces of information.
I like to use it as settings. I sometimes like to put in small pieces of objects in there because I don’t want to bring in Core Data. Everyone knows knows of someone who’s used UserDefaults as a replacement of Core Data. That’s why we call it “Diet Core Data”. We get all the flavor of Core Data without any of the performance, and unfortunately, it’s stringly typed.
First, let’s take a quick recap on Swift 3 which is around the corner.
Instance
- Old
NSUserDefaults.standardUserDefaults()
- New
UserDefaults.standard
UserDefaults
is no longer known NSUserDefaults
. Plus, with the Swift API guidelines, the instance is now just standard
. You notice that’s not a function anymore, it’s just the computed variable.
Set API
- Old
.setBool(true, forKey: "isUserLoggedIn")
.setInt(1, forKey: "pokeballCount")
.setObject(pikachu, forKey: "pikachu")
- New
.set(true, forKey: "isUserLoggedIn")
.set(1, forKey: "pokeballCount")
.set(pikachu, forKey: "pikachu")
No longer do we do setBool
, setInt
, setObject
, now we just say set
. We do this with Swift using this little thing called function overloading. You can have the same function name, same parameters, but as long as the parameter types are different, the compiler will be able to infer which API you intend to call based on the parameters you pass in. Here I’m passing a boolean, so it’s calling setBool
.
Get API
- Old
.boolForKey("isUserLoggedIn")
.integerForKey("pokeballCount")
.objectForKey("pikachu")
- New
.bool(forKey: "isUserLoggedIn")
.integer(forKey: "pokeballCount")
.object(forKey: "pikachu")
The get APIs are pretty much the same, except you’re following the Swift API guidelines. You’re removing the first parameter name out of the function name and into its own parameter name.
Synchronize
Synchronize has been deprecated.
- Old
NSUserDefaults.standardUserDefaults().synchronize()
- New
// deprecated
Fixing UserDefaults (6:49)
We said UserDefaults are Stringly typed APIs. Here’s what it looks like in Swift 3. Let’s start with fixing them.
// Setter
UserDefaults.standard.set(true, forKey: "isUserLoggedIn")
// Getter
UserDefaults.standard.bool(forKey: "isUserLoggedIn")
First, if you write a String more than once, you should turn it into a constant.
UserDefaults.standard.set(true, forKey: "isUserLoggedIn")
UserDefaults.standard.bool(forKey: "isUserLoggedIn")
...
let isUserLoggedInKey = "isUserLoggedIn"
UserDefaults.standard.set(true, forKey: isUserLoggedInKey)
UserDefaults.standard.bool(forKey: isUserLoggedInKey)
This helps us avoid typos, and it makes our code a lot easier and neater to read.
One pattern you brought from Objective-C was that sometimes we like to unify things.
struct Constants {
struct Keys {
// Account
static let isUserLoggedIn = "isUserLoggedIn"
// Onboarding
...
}
}
Here we have this struct Constants
, and we have a substruct called Keys
. We do this so we can have a bird’s eye view of all our constants and help us maintain uniformity.
When we interact with UserDefaults APIs, it looks like this:
// Set
UserDefaults.standard
.set(true, forKey: Constants.Keys.isUserLoggedIn)
// Get
UserDefaults.standard
.bool(forKey: Constants.Keys.isUserLoggedIn)
Why don’t we use a sub substruct, called Account
?
struct Constants {
struct Keys {
struct Account {
static let isUserLoggedIn = "isUserLoggedIn"
}
}
}
It’s a static let because we don’t want to initialize a substruct every time we want to use the key. Now it looks like this:
// Set
UserDefaults.standard
.set(true, forKey: Constants.Keys.Account.isUserLoggedIn)
// Get
UserDefaults.standard
.bool(forKey: Constants.Keys.Account.isUserLoggedIn)
We have this problem where we have this sub substruct Account
.
Constants.swift
struct Account {
static let isUserLoggedIn = "isUserLoggedIn"
}
Once again, this is prone to errors. If I misspell isUserLoggedIn
with an extra N and that makes its way into production code, I’ll just hate myself. What I suggest is switching to enums.
Constants.swift
enum Account : String {
case isUserLoggedIn
}
Why enum? Here you can see we have an enum Account
, which conforms to a string raw representable. Instead of a static let, we use a case. The cool thing with enums that conform to string raw representable is if we don’t provide a value, its value will be the same as its case. When we use it, it looks like this:
struct Constants {
struct Keys {
enum Account : String {
case isUserLoggedIn
}
}
}
It’s a little bit nicer.
// Set
UserDefaults.standard
.set(true, forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
// Get
UserDefaults.standard
.bool(forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
Let’s do a recap.
// Strings
let key = "isUserLoggedIn"
// Constant & Grouped
let key = Constants.isUserLoggedIn
// Context
let key = Constants.Keys.Account.isUserLoggedIn
// Safety
let key = Constants.Keys.Account.isUserLoggedIn.rawValue
First we start off with strings, then we turned them into constants and we grouped them together. We gave our string path or our constant path a little bit of context, so we’re saying constants key to isUserLoggedIn, and we added that little bit extra safety by turning it into an enum.
// Set
UserDefaults.standard
.set(true, forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
// Get
UserDefaults.standard
.bool(forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
Extend UserDefaults (10:09)
We’re going to turn our code from this:
// Set
UserDefaults.standard
.set(true, forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
// Get
UserDefaults.standard
.bool(forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
To this:
// Set
UserDefaults.standard
.set(true, forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
UserDefaults.standard.set(true, forKey: .isUserLoggedIn)
// Get
UserDefaults.standard
.bool(forKey: Constants.Keys.Account.isUserLoggedIn.rawValue)
UserDefaults.standard.bool(forKey: .isUserLoggedIn)
If you know someone who’s been writing Swift and you ask them to solve a problem, you know their one answer for everything is protocols.
Protocols (11:03)
What does our protocol look like? It’s going to be called BoolDefaultSettable
.
protocol BoolDefaultSettable {
associatedtype BoolKey : RawRepresentable
}
As you can see, it takes an associatedtype
called BoolKey
, which conforms to RawRepresentable
. Before we get into the gist of it, let’s go down memory lane to WWDC 2015, where we were introduced to Crusty.
You may know Crusty, but you may not know Crusty’s Third Law: “For every protocol there is an equal and corresponding protocol extension.” Following Crusty’s Third Law, we have our protocol extension.
protocol BoolDefaultSettable {
associatedtype BoolKey : RawRepresentable
}
extension BoolDefaultSettable where BoolKey.RawValue == String {
}
As you see, we have a where
clause in there. Now that we have that set up…
extension BoolDefaultSettable where BoolKey.RawValue == String {
// Setter
func set(_ value: Bool, forKey key: BoolKey) {
let key = key.rawValue
UserDefaults.standard.set(value, forKey: key)
}
}
It’s pretty much the exact same API as UserDefault.
extension BoolDefaultSettable where BoolKey.RawValue == String {
// Getter
func bool(forKey key: BoolKey) -> Bool {
let key = key.rawValue
return UserDefaults.standard.bool(forKey: key)
}
}
As an overview, it looks like this:
protocol BoolDefaultSettable {
associatedtype BoolKey : RawRepresentable
}
extension BoolDefaultSettable where BoolKey.RawValue == String {
func set(_ value: Bool, forKey key: BoolKey) {
let key = key.rawValue
UserDefaults.standard.set(value, forKey: key)
}
func bool(forKey key: BoolKey) -> Bool {
let key = key.rawValue
return UserDefaults.standard.bool(forKey: key)
}
}
Here we have BoolKey
, but you could do the rest of the default settable family, so integer, double, float object, URL.
protocol IntegerDefaultSettable { ... }
protocol DoubleDefaultSettable { ... }
protocol FloatDefaultSettable { ... }
protocol ObjectDefaultSettable { ... }
protocol URLDefaultSettable { ... }
Now we’re going to extend UserDefaults.
extension UserDefaults : BoolDefaultSettable {
enum BoolKey : String {
case isUserLoggedIn
}
}
UserDefaults now extend to conform to BoolDefaultSettable
. To do that, we have to include our associated types. Now our case is BoolKey
, it’s an enum
, which conforms to string raw representable, and our first case is, isUserLoggedIn
. And this is what it looks like:
extension UserDefaults : BoolDefaultSettable {
enum BoolKey : String {
case isUserLoggedIn
}
}
...
UserDefaults.standard.set(true, forKey: .isUserLoggedIn)
UserDefaults.standard.bool(forKey: .isUserLoggedIn)
Much nicer, right? Just because we created a protocol doesn’t mean we’re confined to UserDefaults. If you want to add a little bit more context we extend other objects.
extension Account : BoolDefaultSettable {
enum BoolKey : String {
case isUserLoggedIn
}
}
...
Account.set(true, forKey: .isUserLoggedIn)
Account.bool(forKey: .isUserLoggedIn)
In this case is Account
. However, now we have this collision problem, right?
Account.BoolKey.isUserLoggedIn.rawValue
// key: "isUserLoggedIn"
UserDefaults.BoolKey.isUserLoggedIn.rawValue
// key: "isUserLoggedIn"
Account.BoolKey.isUserLoggedIn
is the exact same thing as UserDefaults.BoolKey.isUserLoggedIn
, so we’re going to make another protocol.
protocol KeyNamespaceable {
func namespaced<T: RawRepresentable>(_ key: T) -> String
}
It’s one simple function. It takes in a generic which conforms to raw representable, and returns a string. Once again, we follow Crusty’s Third Law and extend it.
protocol KeyNamespaceable {
func namespaced<T: RawRepresentable>(_ key: T) -> String
}
extension KeyNamespaceable {
func namespaced<T: RawRepresentable>(_ key: T) -> String {
return "\(Self.self).\(key.rawValue)"
}
}
It’s pretty much a simple return string using string interpolation. You’re combining the string version of self and the raw value separated by a full stop. In this case, it would be UserDefaults.isUserLoggedIn
.
protocol KeyNamespaceable {
func namespaced<T: RawRepresentable>(_ key: T) -> String
}
extension KeyNamespaceable {
func namespaced<T: RawRepresentable>(_ key: T) -> String {
return "\(Self.self).\(key.rawValue)"
}
}
// key: "UserDefaults.isUserLoggedIn"
And we go back to our protocol BoolDefaultSettable
and we make it conform to key namespaced
bool.
protocol BoolDefaultSettable : KeyNamespaceable {
associatedtype BoolKey : RawRepresentable
}
Back to our setters and getters. Because BoolDefaultSettable
conforms to key namespaced
bool, we can just say let key = namespaced(key)
.
extension BoolDefaultSettable where BoolKey.RawValue == String {
func set(_ value: Bool, forKey key: BoolKey) {
let key = namespaced(key)
UserDefaults.standard.set(value, forKey: key)
}
func bool(forKey key: BoolKey) -> Bool {
let key = namespaced(key)
return UserDefaults.standard.bool(forKey: key)
}
}
And now we’re collision-free.
UserDefaults.set(true, forKey: .isUserLoggedIn)
// key: "UserDefaults.isUserLoggedIn"
Account.set(true, forKey: .isUserLoggedIn)
// key: "Account.isUserLoggedIn"
Uniformity and Context (14:50)
The cool thing with Swift is that when we extend the class or extend an object, that extension doesn’t have to live within the same class or Swift file as its declaration. We go back to our Constants.swift pattern and we can pull our extensions in the Constants.swift file, outside of the Constants
struct.
extension Account : BoolDefaultSettable { ... }
extension Onboarding : BoolDefaultSettable { ... }
struct Constants { ... }
...
Account.set(true, forKey: .isUserLoggedIn)
Account.bool(forKey: .isUserLoggedIn)
We do this so we can have a bird’s eye view of everything. But what about context? Before, instead of using UserDefaults set true for isUserLoggedIn
, we moved to account set true for isUserLoggedIn
, and that brought us more context. Instead of sticking with the constants pattern, I would suggest actually making a default.
struct Defaults {
struct Account : BoolDefaultSettable { ... }
struct Onboarding : BoolDefaultSettable { ... }
}
...
Defaults.Account.set(true, forKey: .isUserLoggedIn)
Defaults.Onboarding.set(true, forKey: .isUserOnboarded)
Defaults.Account.bool(forKey: .isUserLoggedIn)
Defaults.Onboarding.bool(forKey: .isUserOnboarded)
It’s a lot easier to read than what we started with.
Conclusion (16:23)
Stringly typed APIs are bad. You shouldn’t really use them.
There’s plenty of room for improvement with our code. We should always be challenging the ways of the norm, and trying to explore what we can and can’t do.
Grouping constants isn’t a necessity, but at the same time I like to do it because I like to maintain uniformity in what I’m doing.
Namespacing our APIs gives a lot more context, as well as with our strings or with our keys. It also helps us avoid collisions.
Protocols are so hot right now.
Finally, we should rethink how we work with APIs. Just because we have these APIs given to us by Apple, it doesn’t mean we have to use them the way that they were delivered to us. They give us stringly typed APIs because it’s the way they can help us cover all bases of what we want to do. They can’t give us an enum for defaults because, unlike with a TableView, they cannot predetermine what we want to do with our code.
I like to think of it like having a house; you start moving in, but at the same time, you want to get rid of all the old furniture and start putting in your own.
Receive news and updates from Realm straight to your inbox