{{announcement.body}}
{{announcement.title}}

Replacing Optionals With Enums to Manage State: A Swift Tutorial

DZone 's Guide to

Replacing Optionals With Enums to Manage State: A Swift Tutorial

Learn how and why using enumeration, or enums, to manage state is a useful option to make code less fragile and more robust.

· Web Dev Zone ·
Free Resource

Using enumeration, or enums, to manage state is a useful option to make code less fragile and more robust by replacing state scenarios that shouldn’t be possible with solutions that make them impossible.

Wait  – how do you end up in a state that shouldn’t be possible?

The Problem With Optionals

Using optionals without adding the additional logic to handle every future state change can lead to states that shouldn’t be possible. Optionals are used in situations where a value may be absent. An optional represents two possible states: either there is a value or there isn’t a value at all. Where optionals only represent two possible states, enums are able to specify and combine multiple “stateful” properties into a single state representation.

Let’s clarify using Apple’s example:

class UserAuthenticationController {
	var isLoggedIn: Bool = false 
	var user: User?
}

In the case above, there are opportunities for this controller to enter an “illegal” state. For example, it’s possible for the controller to be in a “logged in” state while the user is nil or non-existent, or vice versa, where the controller is in a “logged out” state when the user exists. Using an optional in this instance forces you to remember to nullify the user manually when entering a “logged out” state. To avoid this, we can capture the properties in a State enum, and make it impossible to enter those “illegal” states:

class UserAuthenticationController {
    enum State {
        case idle 
        case loggedIn(user: User)
    }

    var state: State = .idle
}

Adding a State enum immediately clarifies the controller states with a concrete state list the controller can take on.

Alright, but how exactly do you define an enum?

Using Enums to Manage State

An enum defines a common type for a group of related values. Unlike C enums, which are represented by unique integer values for each case, Swift enums don’t require any primitive raw value. If a raw value is provided for each enumeration case, the value can be a string, a character, or a value of any integer or floating type. 

Let’s explore how to use enums to manage state using a hockey game example. We’ll examine an example of modifying state in a hockey game app, the problems that could possibly arise if State isn’t handled properly, and how to improve the validity of transitions by implementing a State enum. First, let’s set the foundation of our hockey game idea.

We’ll want our app to keep track of game scores, so we’ll build a struct to represent the score of a game consisting of an integer value for each home and away team, and a function to return a new Score by adding the supplied home and away values to the scoreboard.

struct Score: Equatable {
    static let zero = Score(home: 0, away: 0)

    var home: Int
    var away: Int

    /// Returns a new Score adding the supplied home and away values to this score.
    func adding(home: Int, away: Int) -> Score {
        return Score(home: self.home + home, away: self.away + away)
    }
}

Next, we'll set a few properties we'll want in a game. Game0 below represents the skeleton of a game. It’s pretty straightforward: a game has two teams, and if a game is in play, there is a score. You’ll notice we have to declare the score as an optional Score. The score is defined as optional to allow us to distinguish between a game that hasn’t started yet and a game with a 0-0 score.

We can give our game more detail by adding different states. We'll give a game a date once it’s been scheduled, a list of Player stars once it’s over, and four different game states: scheduledstartedfinished, or cancelled, which you can see below. 

class Game1 {
    let home: Team
    let away: Team
    var score: Score?

    private(set) var date:  Date?        // nil if not scheduled yet
    private(set) var stars: [Player]?    // nil if game hasn't finished

    private(set) var isScheduled: Bool = false
    private(set) var isStarted:   Bool = false
    private(set) var isFinished:  Bool = false
    private(set) var isCancelled: Bool = false

    init(home: Team, away: Team) {
        self.home = home
        self.away = away
    }
}

Now, we’re going to add functions to change game states. In the next example, there are several problems that might arise by calling these functions in unexpected ways.

class Game2 {
    let home: Team
    let away: Team
    var score: Score?

    private(set) var date:  Date?        // nil if not scheduled yet
    private(set) var stars: [Player]?    // nil if game hasn't finished

    private(set) var isScheduled: Bool = false
    private(set) var isStarted:   Bool = false
    private(set) var isFinished:  Bool = false
    private(set) var isCancelled: Bool = false

    init(home: Team, away: Team) {
        self.home = home
        self.away = away
    }

    /// Schedules the game for the specified date.
    func schedule(date: Date) {
        isScheduled = true
        self.date = date
    }

    /// Starts the game
    func start() {
        isStarted = true
        score = .zero
    }

    /// Ends the game
    func end(stars: [Player]) {
        isFinished = true
        self.stars = stars
    }

    /// Cancels the game
    func cancel() {
        isCancelled = true
    }

    /// Adds points to the home score
    func homeScored(_ points: Int) {
        score?.home = (score?.home ?? 0) + points
    }

    /// Adds points to the away score
    func awayScored(_ points: Int) {
        score?.away = (score?.away ?? 0) + points
    }
}

What happens if a canceled game is started? What happens if a started game is scheduled? Can isFinished and isCancelled be true simultaneously? Should they be able to? In Game3, let’s build a State enum to address the apparent issues we’ve identified in Game2.

class Game3 {
    /// The state of a game
    enum State {
        /// Game has not yet been scheduled
        case tbd

        /// Game has been scheduled for `date`
        case scheduled(date: Date)

        /// Game is in progress, has scheduled date, and current score
        case started(date: Date, score: Score)

        /// Game has been cancelled, date represents the previously scheduled
        /// date, if any
        case cancelled(date: Date?)

        /// Game has ended, score represents final score, stars is the list of star players
        case over(date: Date, score: Score, stars: [Player])
    }

    let home: Team
    let away: Team
    var state: State = .tbd // start out unscheduled

    var score: Score?     { return state.score }
    var date:  Date?      { return state.date  }
    var stars: [Player]?  { return state.stars }

    var isScheduled: Bool { return state.isScheduled }
    var isStarted:   Bool { return state.isStarted   }
    var isFinished:  Bool { return state.isFinished  }
    var isCancelled: Bool { return state.isCancelled }

    init(home: Team, away: Team) {
        self.home = home
        self.away = away
    }

    func schedule(date: Date) {
        state = state.schedule(date: date)
    }

    func start() {
        state = state.start()
    }

    func end(stars: [Player]) {
        state = state.end(stars: stars)
    }

    func cancel() {
        state = state.cancel()
    }

    func homeScored(_ points: Int) {
        state = state.scored(home: points, away: 0)
    }

    func awayScored(_ points: Int) {
        state = state.scored(home: 0, away: points)
    }
}

/// Provide functions for transitioning between game states.
private extension Game3.State {
    var score: Score? {
        switch self {
        case .started(_, let score):    return score
        case .over(_, let score, _):    return score
        default:                        return nil
        }
    }

    var date: Date? {
        switch self {
        case .scheduled(let date):      return date
        case .started(let date, _):     return date
        case .over(let date, _, _):     return date
        case .cancelled(let date):      return date
        default:                        return nil
        }
    }
    var stars: [Player]?  {
        switch self {
        case .over(_, _, let stars):    return stars
        default:                        return nil
        }
    }

    var isScheduled: Bool {
        switch self {
        case .tbd, .cancelled:  return false
        default:                return true
        }
    }

    var isStarted:   Bool {
        switch self {
        case .started, .over:   return true
        default:                return false
        }
    }

    var isFinished:  Bool {
        switch self {
        case .over:             return true
        default:                return false
        }
    }

    var isCancelled: Bool {
        switch self {
        case .cancelled:        return true
        default:                return false
        }
    }

    func schedule(date: Date) -> Game3.State {
        switch self {
        case .tbd, .scheduled:              return .scheduled(date: date)
        default:                            return failTransition(for: #function)
        }
    }

    func start() -> Game3.State {
        switch self {
        case .tbd:                          return .started(date: Date(), score: .zero)
        case .scheduled(let date):          return .started(date: date, score: .zero)
        default:                            return failTransition(for: #function)
        }
    }

    func scored(home: Int, away: Int) -> Game3.State {
        switch self {
        case .started(let date, let score): return .started(date: date, score: score.adding(home: home, away: away))
        default:                            return failTransition(for: #function)
        }
    }

    func end(stars: [Player]) -> Game3.State {
        switch self {
        case .started(let date, let score): return .over(date: date, score: score, stars: stars)
        default:                            return failTransition(for: #function)
        }
    }

    func cancel() -> Game3.State {
        switch self {
        case .tbd:                          return .cancelled(date: nil)
        case .scheduled(let date):          return .cancelled(date: date)
        case .started(let date, _):         return .cancelled(date: date)
        default:                            return failTransition(for: #function)
        }
    }

    private func failTransition(for action: String) -> Game3.State {
        assertionFailure("Attempt to \(action) a game that is \(self)")
        return self
    }
}

In Game2 we added methods that change state (schedule, start, cancel, etc.). When we did this, we opened the door to several issues:

  1. None of the state changing methods take into account the current state of the game.
  2. We have a number of Bool flags that can conflict with each other (isScheduled, isCancelled, etc.).
  3. We have properties that might be populated or nil regardless of the state that the game should be in. 

In Game3, we implemented a State enum. The state enum immediately creates safer code by creating a concrete list of states the game might be in. Properties that are only valid in particular states are no longer awkwardly detached from the game, rather they are associated values to the state they are relevant to.

With game properties captured inside our State enum and a well-defined list of states a game can take on, the enum makes our state transitions safe. We have a well-defined list of actions that cause state transitions. These actions are schedule, start, end, cancel, scored and they map onto the state changing methods added in Game2.

The Verdict on Maintaining State

Nothing is stopping you from spreading state across multiple variables. Make your life easier and avoid potential state issues – use enums.

For this article, I partnered with Clearbridge Mobile iOS Developer, Andrew Patterson, who helped me learn about enumeration. Credit goes to Andrew for writing the code to support this article. This article was originally published, here

Topics:
mobile app development ,ios tutorial ,swift tutorial ,web dev ,enums ,state management

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}