Swift: Alternative to struct
By: Milan Brankovic
Tags: iOS, Swift, iOS App Development

Imagine that you are creating an app which should, among other things, track analytics events. For this you have chosen one of the event tracking systems (e.g. Firebase).

The first idea would be to create a struct that will model AnalyticsEvent .

struct AnalyticsEvent {

let userId: String

let hasAdditionalData: Bool

let additionalData: [String: String]

let hasFeedback: Bool

let feedbackContent: String

// rest of the properties

}

Although this is a hypothetical situation, it highlights a problem. Because struct can contain multiple values, bugs may arise if multiple events are contained in one command (e.g. hasFeedback and hasAdditionalData ). This reveals a problem of not having mutually exclusive properties, as event can be in two different states at the same time.

Using an enum to group the properties into cases makes data much cleaner. Swift offers a construct where you can have values associated with types in order to build more complex data structures.

enum AnalyticsEvent {

case sendFeedback(userId: String, content: String)

case sendAdditionalData(userId: String, data: [String: String]

case connectAppleHealth(userId: String)

// rest of the cases

}

By adding tuples to each of the cases it makes it more clear which properties belong together. Now a proper event can be constructed without worrying about mixing and matching wrong values.

let feedbackEvent =

AnalyticsEvent.sendFeedback(userId: "id", content: "content")

With this type of construct a switch case can be used for unwrapping inner values:

func logEvent(event: AnalyticsEvent ) {

switch event {

case .sendFeedback( _: String, content: String ):

print("\(content)")

// rest of the cases

} if case let AnalyticsEvent .sendFeedback( _, content ) = event {

print("\(content)")

}

By using switch you need to exhaust all of the cases for AnalyticsEvent . If you are interested in specific case you can utilize if case let construct. Also a match on specific property can be done with a wild card operator, with which you are telling that you don't care about the value of the specific property.

Polymorphism with enums

There are times when you need some flexibility in the shape of polymorphism. This means that you are willing to mix any type, but this is not often ideal. Since you don't know the type at the compile time, a check against type Any need to be performed at the runtime in order to see what does it present.

let data: [Any] = [Date(), "string", 123] for elem: Any in data {

switch elem {

case let stringVal as String: // handle string

case let intVal as Int: // handle integer

case let dateVal as Date: // handle date

default: // handle others

}

}

If you don't know what you are expecting, you can still use switch to figure out the type, but you must also cover the default case for the types you are not interested in. Working with Any is desirable when you are not sure about the data you are receiving from server. But if you know the type beforehand you can use enum as a compile time safety guard.

Let's say that you are receiving either a Date or Range<Date> from the server. In this case you can model DateType which will represent one of this options.

enum DateType {

case concrete(Date)

case range(Range<Date>)

}

Thanks to this type of modeling you can now use it to receive single DateType or even an array of DateType s! Each value of the array can contain one of the multiple types, while maintaining compile-time safety. To act upon values you will simply use switch statement like in the previous example.

With this you are now on the safe side, as if you wish to modify the DateType , compiler will now warn you about the new/missing case.

Subclassing with enums

Subclassing is a concept which allows you to build a hierarchy of data. For example you could have Pizza which would represent all types of pizzas offered in pizza house. Now you can have bunch of different Pizza s: Neapolitan, California style, Chicago deep dish…

In the ideal world modeling the data hierarchically makes sense, but in practice that is not often the case. Sometimes you will need to handle edge cases or exceptions that do no stick with the rules of hierarchy.

As an example let's model a game. We can start with single-player game:

struct SinglePlayerGame {

let id: String

let numOfPlayers: Int

let weaponsCollected: [String]

}

If we now want to add multi-player game, you can do that easily as many of the properties are similar.

Game hierarchy

Introduced Game superclass contains the properties that both SinglePlayerGame and MultiPlayerGame share. When modeling like this, you are forced to use classes. But this is not the biggest problem. If you wish to add new game type AIGame which does not share the same properties, then you will need to refactor an entire data model.

By using enums you are bypassing hierarchical model, yet you can still keep the option of passing single Game type. Additionally adding a new Game type will not violate existing games.

enum Game {

case singlePlayer(SinglePlayerGame)

case multiPlayer(MultiPlayerGame)

case ai(AIGame)

}

let aiGame = ...

let game = Game.ai(aiGame)

This type of subclassing is the best when similar types start to diverge, or you wish to use structs (instead of classes only). The downside of this approach is having to match all cases in the entire application. Although it requires extra work it is also a safe guard as you will get compiler warning if something is forgotten. Another downside is locking the model, which means that if you are offering/using enum as/from third party library, the users cannot expand on it.

A safer use of strings

Enums are often used with together strings. When an enum is defined as raw value type, all cases defined carry some value inside them. Enums with raw values are defined by having type added to an enum's declaration.

enum Stage: String {

case production

case staging

case dev

case local

}

You can (if you wish) add raw value for each case, but declaring enum like this gives value for each case the name of the case.

Sometimes you wish to utilize the value of an enum as a parameter which will be sent to the server. You set up the parameter by reading the enum's raw value by accessing the raw value property.

let stage = Stage.production

let params = ["stage" : stage.rawValue] // ["stage": "production"]

Bug can be introduced by assigning the raw value to the case. The compiler will continue to work, but unfortunately API will not.

enum Stage: String {

case production = "prod"

case staging

case dev

case local

} let stage = Stage.production

let params = ["stage" : stage.rawValue] // ["stage": "prod"]

You can fix the problem in two ways:

play safe and ignore enum's raw value and match each case on the enum case

cover the code with unit test to ensure correct behaviour

But what if you are receiving the value from server and the value has been changed? This can be handled with custom initializer

enum Stage: String {

case production

case staging

case dev

case local init?(rawValue: String) {

switch rawValue.lowercased() {

case "prod", "production": self = .production

case "qa", "staging": self = .staging

case "dev": self = .dev

case "local": self = .local

default: return nul

}

}

}

Summary