Swift: Typecasing

Enums …as data models?

Swift Programming
Published in
6 min readAug 17, 2016

--

Every once in a while you come across one of those unicorn edge cases that force you to challenge everything you know about everything you’ve ever learnt up until that current point in time and space. Just recently I had become a victim of such an event.

In the Chinese language, the word “crisis” is composed of two characters,
one representing danger(危) and the other, opportunity(机).
— John F. Kennedy

A great quote from one of the most famous Americans of all time back in the late 50s, and 35 years later it was modernised by another famous American:

Crisi-tunity!
— Homer J. Simpson

The crisis

I had a chance to revisit, remodel and refactor an API response for one of the apps I work on, where it would receive a JSON structure that had mixed models inside of an array. The reason why they’re mixed, is because they need to be represented in a UITableView from within the same section, in chronological order. So this made receiving the data in two separate arrays and combining them afterwards a non-option.

To simplify the problem, I’ve mocked up a fake, but more interesting API response that demonstrates the conundrum:

"characters" : [
{
type: "hero",
name: "Jake",
power: "Shapeshift"
},
{
type: "hero",
name: "Finn",
power: "Grass sword"
},
{
type: "princess",
name: "Lumpy Space Princess",
kingdom: "Lumpy Space"
},
{
type: "civilian",
name: "BMO"
},
{
type: "princess",
name: "Princess Bubblegum",
kingdom: "Candy"
}
]

As you can see, these objects are in some way alike but also contain properties that others do not. Let’s go through some of the possible solutions.

Classes and inheritance

class Character {
type: String
name: String
}
class Hero: Character {
power: String
}
class Princess: Character {
kingdom: String
}
class Civilian: Character {
}
...struct Model {
characters: [Character]
}

This is a totally valid solution, however it becomes frustrating to use because we have to type check and typecast the objects into it’s specific type whenever access to the type-specific properties is needed:

// Type checkingif model.characters[indexPath.row] is Hero {
print(model.characters[indexPath.row].name)
}
// Type checking and Typecastingif let hero = model.characters[indexPath.row] as? Hero {
print(hero.power)
}

Structs and protocols

protocol Character {
var type: String { get set }
var name: String { get set }
}
struct Hero: Character {
power: String
}
struct Princess: Character {
kingdom: String
}
struct Civilian: Character {
}
...struct Model {
characters: [Character]
}

Because we’re using structs, we get a performance boost from the system but it’s pretty much the same as using a superclass. Because we aren’t leveraging any of the advantages of protocols, there’s simply nothing here to leverage, and we still have to conditionally type check and typecast the item to any type specific properties of the model.

// Type checkingif model.characters[indexPath.row] is Hero {
print(model.characters[indexPath.row].name)
}
// Type checking and Typecastingif let hero = model.characters[indexPath.row] as? Hero {
print(hero.power)
}

Typecasting

By now you’re probably thinking I have some massive disdain for typecasting objects, but I don’t. Typecasting in this scenario has the potential to introduce unintentional side effects into our code. What if the API were to suddenly introduce a new type value? Depending on how we parse the response it could do nothing or it could do something. When we write code, we should be aiming to always make it intentional because Swift is a language that was designed to prioritise safety above all else.

{
type: "king"
name: "Ice King"
power: "Frost"
}

The opportunity

We’ve now reach a point where both of our regular attempts ultimately produce the same result, so now we’re forced to think outside the box, think bold or perhaps think …different?

How can we create an array of objects that are strongly typed and access their properties without typecasting?

Enums

enum Character {
case hero, princess, civilian
}

Because a switch statement must be exhaustive, enums are a great way to remove side effects from our code. But enums aren’t enough, we need to go further.

Associated values

enum Character {
case hero(Hero)
case princess(Princess)
case civilian(Civilian)
}
...switch characters[indexPath.row] {
case .hero(let hero):
print(hero.power)
case .princess(let princess):
print(princess.kingdom)
case .civilian(let civilian):
print(civilian.name)
}

Goodbye, typecasting… Hello, Typecasing™®©!

Now we’ve removed the potential problems for when all of our typecasts fail and implemented strict type validation and which ultimately lead to writing intentional code.

Raw Value

enum Character: String { // Error: ❌
case hero(Hero)
case princess(Princess)
case civilian(Civilian)
}

You may have noticed in the Character enum in previous examples doesn’t conform to the RawRepresentable protocol, this is because an enum cannot have associated values as well as conform to RawRepresentable, both are mutually exclusive of each other.

Initialising

init

So if we can’t initialize from a rawValue, we can simply just write our own initializer, because that’s all the RawRepresentable protocol did, was provide an initializer and rawValue for us to access.

enum Type

enum Character {
private enum Type: String {
case hero, princess, civilian
static let key = "type"
}
}

An enum, inside an enum… It’s an Enum-ception.

We need to go deeper…

Lol, nah. Kthxbai

Before we use our initializer we need to predefine our types, and the best way to do that is with an enum because we also need it to fail if the rawValue we try to initialize it with doesn’t match any of our cases.

With our JSON example we had the type key to validate types, however this doesn’t have to always be the case. You simply just need a unique attribute of your JSON object to validate that is the type you intend to model.

Failable initializers

Our initializer is going to be failable because if our Type enum rawValue initializer fails, then our Character initialiser will fail with it. We will also do the same failable initializers for each of our associated values too because the Swift compiler will not allow us to have an enum value without a value unless the value is declared as optional.

// enum Characterinit?(json: [String : AnyObject]) {
guard
let string = json[Type.key] as? String,
let type = Type(rawValue: string)
else { return nil }
switch type {
case .hero:
guard let hero = Hero(json: json)
else { return nil }

self = .hero(hero)

case .princess:
guard let princess = Princess(json: json)
else { return nil }
self = .princess(princess) case .civilian:
guard let civilian = Civilian(json: json)
else { return nil }
self = .civilian(civilian)
}
}

Parsing

// Model initialisationif let characters = json["characters"] as? [[String : AnyObject]] {
self.characters = characters.flatMap { Character(json: $0) }
}

Because our Character enum has failable initializers, we need to remove the nil values from our array when we parse it, so here we would use the flatMap function when parsing the JSON so that our array will only contain non-nil values.

Typecasing

switch model.characters[indexPath.row] {
case .hero(let hero):
print(hero.power)

case .princess(let princess):
print(princess.kingdom)

case .civilian(let civilian):
print(civilian.name)
}

And there you have it, we’ve repurposed our unintentional code to be intentional as well as learn something new in the process. Gone are the days where we would have to use arrays consisting Any, AnyObject or some generic inheritance/composite model with type checking and casting before use.

BONUS: Typecasing with Pattern Matching

if case

Let’s say we have a function that passes in an argument value which is of Character type and we only really care about one handling particular case. A switch statement in this scenario could be considered overkill because of the amount of syntax involved with using them and handling all other cases:

func printPower(character: Character) {
switch character {
case .hero(let hero):
print(hero.power)
default:
break
}

Instead we can use pattern matching and shorten our code to be far more concise by using an if or guard statement:

func printPower(character: Character) {
if case .hero(let hero) = character {
print(hero.power)
}
}

As always I’ve provided a playground on GitHub for you to check out as well as a Gist incase you aren’t in front of Xcode.

If you like what you’ve read today you can check our my other articles or want to get in touch, please send me a tweet or follow me on Twitter, it really makes my day. I also organise Playgrounds Conference in Melbourne, Australia and would to see you at the next event.

--

--

Swift Programming

iOS Engineer at Twitter (prev. Meta) from Perth, Western Australia. Technical blogger and speaker. Organiser of Playgrounds Conference.