Upgrade to Pro — share decks privately, control downloads, hide ads and more …

State Driven Development - The Beauty of Enums in Swift

State Driven Development - The Beauty of Enums in Swift

https://cfp.uikonf.com/proposals/33

A lot of people discover enums pretty quickly when exposed to Swift. They're arguably one of the most powerful swift language features. Many of us have probably used enums in other programming languages too. Sometimes we might be using enums without even realizing it.

But I think that enums are so valuable that they're worth designing software around, similar to object oriented programming or protocol oriented programming.

It's what I think of as State Driven Development, and that's what this talk is really all about.

Enums make invalid states impossible to represent. We're all used to using boolean state variables to represent the states that our objects can be in. But many times, the combinations of different boolean values represent states that we don't expect or shouldn't allow. Using enums to define specific states makes these invalid combinations of boolean values impossible to represent in our code, which in turn makes several types of bugs impossible to write. That's a big part of why we like building apps in a strongly typed language like Swift in the first place.

Enums help us be specific about the state of an object. That's really what state driven development is all about. They're useful not just for model objects and types, but for views and interface flows as well. Any piece of state in a Swift application can be represented by an enum case, which becomes a wonderful way to build applications.

Enums can also be extended with variables and functions. Whenever you start to check the state of an enum, ask yourself if that check could be a variable or method that gets defined on your enum itself. That way, we ask our enum to describe behaviors for us, and then use its cases to implement those behaviors for the possible states we want to handle.

The state driven development pattern is incredibly helpful when it comes time to add new behavior to an existing class. All you need to do to add new behavior is add a new case to your enum, and add implementations to all of your methods specifically to handle that case. The methods that are part of your enum are effectively the public API to extending the behavior of your object!

Instead of:

var hasLoggedIn : Bool = false

var workoutInProgress : Bool = false

var savingWorkout : Bool = false

func startWorkout() {

guard hasLoggedIn && workoutInProgress == false && !savingWorkout else {
return
}

workoutInProgress = true
}

We are aiming for:

enum WorkoutApplicationLifecycle {

case notLoggedIn
case workoutInProgress(current : Workout)
case savingLastWorkout
case readyToStart

var canStartWorkout : Bool {
get {
switch self {
case .notLoggedIn: fallthrough
case .workoutInProgress: fallthrough
case .savingLastWorkout:
return false
case .readyToStart:
return true
}
}
}
Associated values also greatly expand the types of questions that your enum can answer inside your application's components. If you have an object that always needs to be associated with a specific case, like the specific workout that is in progress, you can associate it with that case on your enum.

In the above example, it makes a lot of sense to have the current workout live within the workoutInProgress case. You can access the associated value and pass that workout to your save workout method so that you know which workout you're saving.

Conversely, if I had a current workout property on my view controller, that property would really only make sense in one particular state that my app is in. In every other state, that value should be empty and thus it has no meaning.

Enums also help manage complex state for view controllers. For something like a document based view controller, there are usually several "modes" that the view controller can be in, like viewing or editing a document. The view controller has a lot of decisions to make based on the state it is in. Encapsulating that logic in an enum gives you a single place to reason about the logic for how to handle each state the document can be in.

There are lots of other great things about enums that make them a joy to use in Swift. I hope to enumerate all of the great things about enums and how they can improve our development experience in this talk.

Conrad Stoll

May 19, 2020
Tweet

More Decks by Conrad Stoll

Other Decks in Programming

Transcript

  1. How often exactly? // Regex for boolean state // Match

    Bool typed vars that aren't computed properties var.*:.*Bool(\n|.*=.*) var.*=\s?(true|false)
  2. var active : Bool var contentLoaded : Bool var dataLoaded

    : Bool var editing : Bool var enabled : Bool var expanded : Bool var handlingTouches : Bool var hasTakenUserAction : Bool var isCompleted : Bool var isRunning : Bool var lastTouchedButtonSide : Bool var lastTouchedRightSide : Bool var notified : Bool var previewEnded : Bool var saved : Bool var started : Bool
  3. One variable var workoutInProgress : Bool = false func startWorkout()

    { guard workoutInProgress == false else { return } workoutInProgress = true // ... }
  4. Two variables var hasLoggedIn : Bool = false var workoutInProgress

    : Bool = false func startWorkout() { guard hasLoggedIn && workoutInProgress == false else { return } workoutInProgress = true // ... }
  5. The tangled web of state var hasLoggedIn : Bool =

    false var workoutInProgress : Bool = false var savingWorkout : Bool = false func stopWorkout() { workoutInProgress = false savingWorkout = true saveWorkout { (success) in self.savingWorkout = false } }
  6. The tangled web of state var hasLoggedIn : Bool =

    false var workoutInProgress : Bool = false var savingWorkout : Bool = false func startWorkout() { guard hasLoggedIn && workoutInProgress == false && !savingWorkout else { return } workoutInProgress = true // ... }
  7. Valid boolean states User Not Logged In, No Workout, Not

    Saving User Logged In, No Workout, Not Saving User Logged In, Workout In Progress, Workout Not Saving User Logged In, No Workout, Workout Is Saving var hasLoggedIn : Bool var workoutInProgress : Bool var savingWorkout : Bool
  8. Invalid boolean states User Not Logged In, Workout In Progress,

    Not Saving User Not Logged In, Workout In Progress, Workout Is Saving User Not Logged In, No Workout, Workout Is Saving User Logged In, Workout In Progress, Workout Is Saving var hasLoggedIn : Bool var workoutInProgress : Bool var savingWorkout : Bool
  9. Four valid states - Hasn't logged into the app yet

    - In the middle of a workout - Saving the last workout - Idle
  10. The vague question with BOOLs var hasLoggedIn : Bool =

    false var workoutInProgress : Bool = false var savingWorkout : Bool = false func startWorkout() { guard hasLoggedIn && workoutInProgress == false && !savingWorkout else { return } workoutInProgress = true // ... }
  11. The vague question with enums var lifecycle : WorkoutApplicationLifecycle =

    .notLoggedIn func startWorkout() { guard lifecycle == .idle else { return } workoutInProgress = .workoutInProgress // ... }
  12. Is this a running activity? if activityType == .running {

    gpsProvider.start() } if activityType == .running { gpsProvider.stop() }
  13. Is this a running or walking activity? if activityType ==

    .running || activityType == .walking { gpsProvider.start() } if activityType == .running { gpsProvider.stop() }
  14. Does this activity support GPS? enum ActivityType { case running

    case walking case yoga var supportsGPS : Bool { switch self { case .running, .walking: return true case .yoga: return false } } } if activityType.supportsGPS { gpsProvider.start() }
  15. Answers to specific questions enum WorkoutApplicationLifecycle { case notLoggedIn case

    workoutInProgress case savingLastWorkout case idle var canStartWorkout : Bool { get { switch self { case .notLoggedIn, .workoutInProgress, .savingLastWorkout: return false case .idle: return true } } } }
  16. The temptation of BOOL var workoutInProgress : Bool = false

    func startWorkout() { guard workoutInProgress == false else { return } // ... }
  17. Asking the specific question var lifecycle : WorkoutApplicationLifecycle = .notLoggedIn

    func startWorkout() { guard lifecycle.canStartWorkout else { return } lifecycle = .workoutInProgress // ... }
  18. Describing program state var lifecycle : WorkoutApplicationLifecycle = .notLoggedIn func

    stopWorkout() { lifecycle = .savingLastWorkout saveWorkout { (success) in self.lifecycle = .idle } }
  19. Enums describing custom behavior enum WorkoutApplicationLifecycle { case notLoggedIn case

    workoutInProgress case savingLastWorkout case idle var canStartWorkout : Bool var actionTitle : String var backgroundColor : UIColor var barButtonItems : [UIBarButtonItem] }
  20. Action title for buttons enum WorkoutApplicationLifecycle { var actionTitle :

    String { get { switch self { case .notLoggedIn: return "Sign In" case .workoutInProgress: return "End Workout" case .idle: return "Start Workout" default: return "" } } } }
  21. Background color for buttons enum WorkoutApplicationLifecycle { var backgroundColor :

    UIColor { get { switch self { case .notLoggedIn: return UIColor.blue case .workoutInProgress: fallthrough case .savingLastWorkout: return UIColor.red case .idle: return UIColor.green } } } }
  22. Actions for buttons enum WorkoutApplicationLifecycle { // ... func actionTaken(from

    viewController : WorkoutViewController) { switch self { case .notLoggedIn: viewController.login() case .workoutInProgress: viewController.stopWorkout() viewController.saveWorkout() case .savingLastWorkout: break case .idle: viewController.startWorkout() } } }
  23. Adding new states enum WorkoutApplicationLifecycle { case restoringWorkout } -

    Can start workout? - false - Action title? - default - Background color? - UIColor.red - Action taken? - break
  24. Pattern matching if case .workoutInProgress = lifecycle { } if

    case let .workoutInProgress(_) = lifecycle { } if case let .workoutInProgress(current: workout) = lifecycle { } if case let .workoutInProgress(workout) = lifecycle { } if case .workoutInProgress(let workout) = lifecycle { }
  25. Switch statement switch self { case .workoutInProgress: // ... case

    let .workoutInProgress(_): // ... case let .workoutInProgress(current: workout): // ... case let .workoutInProgress(workout): // ... case .workoutInProgress(let workout): }
  26. Recommended syntax if case let .workoutInProgress(current: workout) = lifecycle {

    // ... } switch self { case let .workoutInProgress(current: workout): // ... }
  27. Setting associated values var lifecycle : WorkoutApplicationLifecycle = .notLoggedIn func

    startWorkout() { guard lifecycle.canStartWorkout else { return } var newWorkout : Workout = Workout() // ... lifecycle = .workoutInProgress(current: newWorkout) }
  28. Answering more questions enum WorkoutApplicationLifecycle { // ... case workoutInProgress(current:

    Workout) var navigationTitle : String { switch self { case let .workoutInProgress(current: workout): return workout.type.activityName } } }
  29. Highly relevant values enum WorkoutApplicationLifecycle { // ... case workoutInProgress(current:

    Workout) func actionTaken(from viewController : WorkoutViewController) { switch self { case let .workoutInProgress(current: workout): viewController.stopWorkout() viewController.saveWorkout(current: workout) } } }
  30. Result1 enum Result<Value> { case success(Value) case failure(Error) } @frozen

    enum Result<Success, Failure> where Failure : Error { case success(Success) case failure(Failure) } 1 https://www.swiftbysundell.com/posts/the-power-of-result-types-in-swift
  31. Enum cases can provide context protocol TableViewCellContent { var titleText

    : String? var descriptionText : String? var image : UIImage? var accessoryType : UITableViewCellAccessoryType } // Supported Configuration Options * Simple cell with one line of text * Detailed cell with two lines of text * Image Cell * Accessory Image Cell
  32. Enums conform to protocols! protocol TableViewCellContent { var titleText :

    String? var descriptionText : String? var image : UIImage? var accessoryType : UITableViewCellAccessoryType } enum CellConfiguration : TableViewCellContent { case simpleCell(title : String) case detailedCell(title : String, description : String) case imageCell(image : UIImage) case accessoryCell(title : String, image : UIImage, accessory : UITableViewCellAccessoryType) }
  33. extension Workout { var cellConfiguration : CellConfiguration { return .detailedCell(title:

    workoutTitle, description: activityType.description) } } extension User { var cellConfiguration: CellConfiguration { return .imageCell(image: profileImage) } }
  34. func tableView(_ tableView: UITableView, cellForItemAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell (withReuseIdentifier: reuseIdentifier, for: indexPath) as! CustomTableViewCell let workout = workouts[indexPath.row] cell.setup(with: workout.cellConfiguration) return cell }
  35. class CustomTableViewCell : UITableViewCell { @IBOutlet weak var titleLabel :

    UILabel? @IBOutlet weak var descriptionLabel : UILabel? @IBOutlet weak var imageView : UIImageView? func setup(with content : TableViewCellContent) { titleLabel?.text = content.titleText descriptionLabel?.text = content.descriptionText imageView?.image = content.image accessoryType = content.accessoryType } }
  36. View controller customizations protocol ViewControllerAppearance { var title : String

    var leftBarButtonItems : [UIBarButtonItem] var rightBarButtonItems : [UIBarButtonItem] var overlayAlpha : CGFloat var activityIndicatorActive : Bool } enum ViewControllerState : ViewControllerAppearance { case .editing(Workout) case .saving(Workout) case .viewing(Workout) }
  37. Appearance as a function of state func setupView(for state: ViewControllerState)

    { navigationItem.title = state.title navigationItem.leftBarButtonItems = state.leftBarButtonItems navigationItem.rightBarButtonItems = state.rightBarButtonItems UIView.animate(withDuration: 0.35) { self.overlayView.alpha = state.overlayAlpha } if state.activityIndicatorActive { activityIndicator.startAnimating() } else { activityIndicator.stopAnimating() } }
  38. Behavior as a function of state protocol ViewControllerBehavior { func

    didSelectCell(at indexPath: IndexPath) func highlightCell(at indexPath: IndexPath) func handleDragOnCell(at indexPath: IndexPath) func numberOfRows(in section: Int) -> Int var numberOfSections: Int var supportsDragAndDropInteraction: Bool } extension ViewControllerState : ViewControllerBehavior {}
  39. CaseIterable2 enum WorkoutApplicationLifecycle : CaseIterable { case notLoggedIn case workoutInProgress

    case savingLastWorkout case idle } WorkoutApplicationLifecycle.allCases [notLoggedIn, workoutInProgress, savingLastWorkout, idle] 2 https://oleb.net/blog/2018/06/enumerating-enum-cases/
  40. Custom CaseIterable enum WorkoutApplicationLifecycle { case notLoggedIn case workoutInProgress(current :

    Workout) case savingLastWorkout case idle } extension WorkoutApplicationLifecycle : CaseIterable { static var allCases: [WorkoutApplicationLifecycle] { return [.notLoggedIn, .savingLastWorkout, .idle] + WorkoutActivityType.allCases.map(.workoutInProgress) } }
  41. Enum raw values enum DefaultKeys : String { case loggedInUserId

    case selectedActivityType } print(DefaultKeys.selectedActivityType.rawValue) selectedActivityType
  42. Specific string raw values enum DefaultKeys : String { case

    loggedInUserId case selectedActivityType = "SelectedActivityTypeDefaultKey" } print(DefaultKeys.selectedActivityType.rawValue) SelectedActivityTypeDefaultKey
  43. Customize with RawRepresentable enum ApplicationTheme { case defaultLight case darkMode

    case blueberry case eggplant case mustard case broccoli } struct Theme : Equatable { let name : String let background : UIColor let cell : UIColor let tint : UIColor let foreground : UIColor let statusBar : UIStatusBarStyle }
  44. RawRepresentable enum ApplicationTheme : RawRepresentable { typealias RawValue = Theme

    init?(rawValue: Theme) { // ... } var rawValue: Theme { // ... } } print(ApplicationTheme.broccoli.rawValue.name) Brocolli
  45. Associated theme values enum ApplicationTheme { case defaultLight(theme : Theme)

    case darkMode(theme : Theme) case blueberry(theme : Theme) case eggplant(theme : Theme) case mustard(theme : Theme) case broccoli(theme : Theme) }
  46. All the Themes enum ApplicationTheme : CaseIterable { case defaultLight

    case darkMode case blueberry case eggplant case mustard case broccoli } ApplicationTheme.allCases [.defaultLight, .darkMode, .blueberry, .eggplant, .mustard, .broccoli]
  47. ConfettiMoment public enum ConfettiMoment { // Color Confetti case confettiMMF

    case confettiMFP case confetti ! // Emoji Confetti case " // Custom Confetti case customColors(colors : [UIColor], name : String) case customImages(images : [UIImage], name : String) func effectScene() -> SKScene }
  48. Additional resources - Enum Driven Table Views34 - Pattern Matching

    in Swift5 - Maintaining State6 6 https://developer.apple.com/documentation/swift/maintainingstateinyourapps 5 http://alisoftware.github.io/swift/pattern-matching/2016/03/27/pattern-matching-1/ 4 https://www.raywenderlich.com/5542-enum-driven-tableview-development 3 https://www.natashatherobot.com/swift-enums-tableviews/
  49. Additional resources - Enums and Optionals7 - Writing Self Documenting

    Swift Code8 - Code Encapsulation in Swift9 - Enumerations10 10 https://ericasadun.com/2015/05/26/swift-the-hall-of-the-dwarven-enumeration-king/#more-1529 9 https://www.swiftbysundell.com/posts/code-encapsulation-in-swift 8 https://www.swiftbysundell.com/posts/writing-self-documenting-swift-code 7 http://khanlou.com/2018/04/enums-and-optionals/