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

Stupid Enum Tricks in Swift and Kotlin - We Are Developers, Vienna, Austria, May 2018

Stupid Enum Tricks in Swift and Kotlin - We Are Developers, Vienna, Austria, May 2018

Enums in Swift and enum classes in Kotlin are extremely powerful ways to deal with a known set of possible values. Learn how to use these tools to help you cleaner, safer code, and how to push the boundaries of what's possible with enums.

Ellen Shapiro
PRO

May 16, 2018
Tweet

More Decks by Ellen Shapiro

Other Decks in Technology

Transcript

  1. STUPID ENUM TRICKS
    IN SWIFT AND KOTLIN
    WE ARE DEVELOPERS | VIENNA, AUSTRIA | MAY 2018
    BAKKENBAECK.COM | JUSTHUM.COM | DESIGNATEDNERD.COM
    BY ELLEN SHAPIRO | @DESIGNATEDNERD

    View Slide

  2. PREFACE:
    A NOTE ON PRONUNCIATION

    View Slide

  3. "EE-NUMB" VS. "EE-NOOM"

    View Slide

  4. "EE-NUMB" VS. "EE-NOOM"

    (I PRONOUNCE IT THIS WAY)

    View Slide

  5. "EE-NUMB" VS. "EE-NOOM"
    (THIS WAY IS ALSO FINE)

    View Slide

  6. GIF

    View Slide

  7. GIF

    View Slide

  8. WHAT IS AN ENUM?

    View Slide

  9. A LIST OF VALUES THAT ARE
    POSSIBLE

    View Slide

  10. OPTION SET

    View Slide

  11. OPTION SET

    View Slide

  12. POSSIBLE
    IT COULD BE ANY ONE OF THESE THINGS IN THE LIST

    View Slide

  13. SWIFT
    enum ColorName: String {
    case red
    case orange
    case yellow
    case green
    case blue
    case violet
    }

    View Slide

  14. IT COULD ALSO BE SOMETHING
    TOTALLY DIFFERENT

    View Slide

  15. SWIFT
    func colorForName(_ name: String) -> UIColor? {
    switch name {
    case ColorName.red.rawValue:
    return UIColor.red
    case ColorName.orange.rawValue:
    return UIColor.orange
    // same for other ColorName cases
    default:
    return nil
    }

    View Slide

  16. EXHAUSTIVE ENUMS

    View Slide

  17. EXAUSTIVE
    BUT THESE ARE THE ONLY THINGS WHICH ARE POSSIBLE

    View Slide

  18. SWIFT
    enum TrafficLightColor {
    case red
    case yellow
    case green
    }

    View Slide

  19. LIMIT YOUR
    POSSIBILITIES

    View Slide

  20. LIMIT YOUR
    RUNTIME ERRORS

    View Slide

  21. SWIFT STANDARD LIBRARY
    public enum ComparisonResult : Int {
    case orderedAscending
    case orderedSame
    case orderedDescending
    }

    View Slide

  22. KOTLIN STANDARD LIBRARY
    public enum ClassKind {
    CLASS,
    INTERFACE,
    ENUM_CLASS,
    ENUM_ENTRY,
    ANNOTATION_CLASS,
    OBJECT;
    }

    View Slide

  23. HOW DO I CREATE
    MY OWN ENUM?

    View Slide

  24. SWIFT
    enum TrafficLightColor {
    case red
    case yellow
    case green
    }

    View Slide

  25. KOTLIN
    enum class

    View Slide

  26. KOTLIN
    enum class TrafficLightColor {
    Red,
    Yellow,
    Green;
    }

    View Slide

  27. HOW DO I USE ENUMS?

    View Slide

  28. switch / when

    View Slide

  29. SWIFT
    var currentColor: TrafficLightColor = .red
    switch currentColor {
    case .red:
    print("STOP RIGHT THERE")
    case .yellow:
    print("Whoa, slow down there, buddy.")
    case .green:
    print("Go, go, go!")
    }

    View Slide

  30. SWIFT
    var currentColor: TrafficLightColor = .red
    switch currentColor {
    case .red:
    print("STOP RIGHT THERE")
    case .green:
    print("Go, go, go!")
    // Error: Unhandled case!
    }

    View Slide

  31. SWIFT
    var currentColor: TrafficLightColor = .red
    switch currentColor {
    case .red:
    print("STOP RIGHT THERE")
    case .yellow,
    .green:
    print("Go, go, go!")
    }

    View Slide

  32. KOTLIN
    var currentColor = TrafficLightColor.Red
    when (currentColor) {
    TrafficLightColor.Red -> println("STOP RIGHT THERE")
    TrafficLightColor.Yellow -> println("Whoa, slow down there, buddy.")
    TrafficLightColor.Green -> println("Go, go, go!")
    }

    View Slide

  33. WHAT DO I GET
    FOR FREE WITH ENUMS?

    View Slide

  34. KOTLIN: A LOT!

    View Slide

  35. KOTLIN
    Instance Gets You Example
    instance.name String value of
    case's written name
    Days.THURSDAY.name
    is "THURSDAY",
    Days.Thursday.name
    is "Thursday"
    instance.ordinal Index of case in
    list of cases

    View Slide

  36. KOTLIN
    Type Gets You
    Type.valueOf(string:
    String)
    Enum value of string, or
    null
    Type.values() Generated list of all
    values in the enum class

    View Slide

  37. SWIFT: IT DEPENDS

    View Slide

  38. BY DEFAULT: NOT MUCH

    View Slide

  39. RawRepresentable

    View Slide

  40. SWIFT
    enum SettingsSection {
    case profile
    case contact
    case legalese
    case logout
    }

    View Slide

  41. SWIFT
    enum SettingsSection: Int {
    case profile
    case contact
    case legalese
    case logout
    }

    View Slide

  42. SWIFT
    enum SettingsSection: Int {
    case profile // 0
    case contact // 1
    case legalese // 2
    case logout // 3
    }

    View Slide

  43. SWIFT
    enum SettingsSection: Int {
    case profile // 0
    case legalese // 1
    case contact // 2
    case logout // 3
    }

    View Slide

  44. SWIFT
    guard let section = SettingsSection(rawValue: 10) else {
    assertionFailure("Section 10 does not exist!")
    }

    View Slide

  45. SWIFT
    enum JSONKey: String {
    case user_name
    case email_address
    case latitude
    case longitude
    }

    View Slide

  46. SWIFT
    enum JSONKey: String {
    case user_name
    case email_address
    case latitude = "lat"
    case longitude = "long"
    }

    View Slide

  47. SWIFT Codable
    class User: Codable {
    let userName: String
    let email: String
    let latitude: Double
    let longitude: Double
    enum CodingKeys: String, CodingKey {
    case userName = "user_name"
    case email = "email_address"
    case latitude = "lat"
    case longitude = "long"
    }
    }

    View Slide

  48. SWIFT 4.2: CaseIterable

    View Slide

  49. SWIFT

    View Slide

  50. SWIFT
    enum SettingsSection: Int, CaseIterable {
    case profile
    case legalese
    case contact
    case logout
    }

    View Slide

  51. SWIFT
    enum SettingsSection: Int, CaseIterable {
    case profile
    case legalese
    case contact
    case logout
    }
    // Automatically generated:
    static var allCases: [SettingsSection]

    View Slide

  52. SWIFT
    func numberOfSectionsInTableView(_ tableView: UITableView) -> Int {
    return SettingsSection.allCases.count
    }
    func tableView(_ tableView: UITableView,
    titleForHeaderInSection section: Int) -> String {
    let section = SettingsSection.allCases[section]
    return section.localizedTitle
    }

    View Slide

  53. WHAT CAN I
    ADD TO ENUMS?

    View Slide

  54. COMPUTED PROPERTIES

    View Slide

  55. SWIFT
    enum LandingScreenButton {
    case signIn
    case signUp
    case viewTerms
    }

    View Slide

  56. SWIFT
    enum LandingScreenButton {
    case signIn
    case signUp
    case viewTerms
    var localizedTitle: String {
    switch self {
    case .signIn:
    return NSLocalizedString("Sign In",
    "Sign in button title")
    case .signUp:
    return NSLocalizedString("Sign Up", "Sign up button title")
    case .viewTerms:
    return NSLocalizedString("View Terms & Conditions", "Title for button to view legalese")
    }
    }

    View Slide

  57. SWIFT
    public enum LandingScreenButton {
    case signIn
    case signUp
    case viewTerms
    public var localizedTitle: String {
    switch self {
    case .signIn:
    return NSLocalizedString("Sign In", "Sign in button title")
    case .signUp:
    return NSLocalizedString("Sign Up", "Sign up button title")
    case .viewTerms:
    return NSLocalizedString("View Terms & Conditions", "Title for button to view legalese")
    }
    }

    View Slide

  58. UI TESTING

    View Slide

  59. SWIFT / XCUI

    View Slide

  60. KOTLIN /
    ESPRESSO

    View Slide

  61. ASSET HANDLING [IOS]

    View Slide

  62. ASSET CATALOGS

    View Slide

  63. SWIFT / IOS: BEFORE
    guard let image =
    UIImage(named: "cinnamon_rolls") else {
    return
    }

    View Slide

  64. SWIFT / IOS: BEFORE
    guard let image =
    UIImage(named: "cinnamon_rolls") else {
    return
    }

    View Slide

  65. SWIFT / IOS: BEFORE
    guard let image =
    UIImage(named: "cinamon_rolls") else {
    return
    }

    View Slide

  66. SWIFT / IOS: BEFORE
    guard let image =
    UIImage(named: "cinamon_rolls") else {
    return
    }

    View Slide

  67. SWIFT / IOS: AFTER
    public enum Asset: String {
    case cinnamon_rolls
    case innocent
    case no
    case snack
    case window
    var image: UIImage {
    return UIImage(named: self.rawValue)!
    }
    }

    View Slide

  68. SWIFT / IOS: TESTS!
    func testAllAssetsAreThere() {
    XCTAssertNotNil(UIImage(named: Asset.cinnamon_rolls.rawValue))
    XCTAssertNotNil(UIImage(named: Asset.cinnamon_rolls.rawValue))
    XCTAssertNotNil(UIImage(named: Asset.cinnamon_rolls.rawValue))
    XCTAssertNotNil(UIImage(named: Asset.cinnamon_rolls.rawValue))
    XCTAssertNotNil(UIImage(named: Asset.cinnamon_rolls.rawValue))
    }

    View Slide

  69. SWIFT / IOS: TESTS!
    func testAllAssetsAreThere() {
    XCTAssertNotNil(UIImage(named: Asset.cinnamon_rolls.rawValue))
    XCTAssertNotNil(UIImage(named: Asset.cinnamon_rolls.rawValue))
    XCTAssertNotNil(UIImage(named: Asset.cinnamon_rolls.rawValue))
    XCTAssertNotNil(UIImage(named: Asset.cinnamon_rolls.rawValue))
    XCTAssertNotNil(UIImage(named: Asset.cinnamon_rolls.rawValue))
    } !

    View Slide

  70. SWIFT / IOS: TESTS!
    func testAllAssetsAreThere() {
    for asset in Asset.allCases {
    XCTassertNotNil(UIImage(named: asset.rawValue),
    "Image for \(asset.rawValue) was nil!")
    }
    }

    View Slide

  71. SWIFT / IOS: TESTS!
    func testAllAssetsAreThere() {
    for asset in Asset.allCases {
    XCTassertNotNil(UIImage(named: asset.rawValue),
    "Image for \(asset.rawValue) was nil!")
    }
    } !!!!!!!

    View Slide

  72. USE CODE GENERATION

    View Slide

  73. USE CODE GENERATION
    (OR THIS CAN GET SUPER-TEDIOUS ON BIG PROJECTS)

    View Slide

  74. SWIFT CODE GENERATION
    > https://github.com/SwiftGen/SwiftGen
    > https://github.com/krzysztofzablocki/Sourcery
    > https://github.com/stencilproject/Stencil

    View Slide

  75. BENDING ENUMS
    TO YOUR WILL

    View Slide

  76. COMBINING ENUMS
    +
    PROTOCOLS / INTERFACES

    View Slide

  77. SWIFT: BEFORE
    @IBAction private func signIn() {
    guard self.validates() else { return }
    performSegue(withIdentifier: "toCatsLoggedIn", sender: nil)
    }

    View Slide

  78. SWIFT: THE ENUM
    enum MainStoryboardSegue: String {
    case toSignIn
    case toSignUp
    case toTerms
    case toCats
    case toCatsLoggedIn
    }

    View Slide

  79. SWIFT: THE PROTOCOL
    protocol SeguePerforming {
    var rawValue: String { get }
    }

    View Slide

  80. SIDE NOTE:
    LEARN TO CHEAT

    View Slide

  81. SWIFT: THE PROTOCOL WITH A GENERIC EXTENSION FUNCTION
    protocol SeguePerforming {
    var rawValue: String { get }
    }
    extension UIViewController {
    func perform(segue: T, sender: Any? = nil) {
    performSegue(withIdentifier: segue.rawValue, sender: sender)
    }
    }

    View Slide

  82. SWIFT: THE ENUM, REVISED
    enum MainStoryboardSegue: String, SeguePerforming {
    case toSignIn
    case toSignUp
    case toTerms
    case toCats
    case toCatsLoggedIn
    }

    View Slide

  83. SWIFT: AFTER
    @IBAction private func signIn() {
    guard self.validates() else { return }
    self.perform(MainStoryboardSegue.toCats)
    }

    View Slide

  84. ASSOCIATED OBJECTS

    View Slide

  85. SWIFT
    enum DownloadState {
    case notStarted,
    case downloading(let progress: Float)
    case success(let data: Data)
    case error(let error: Error?)
    }

    View Slide

  86. KOTLIN
    enum class CreditCard(val prefix: String) {
    Visa("4"),
    Mastercard("5"),
    Amex("3");
    }

    View Slide

  87. KOTLIN
    sealed class

    View Slide

  88. KOTLIN
    sealed class DownloadStateWithInfo {
    }

    View Slide

  89. KOTLIN
    sealed class DownloadStateWithInfo {
    class NotStarted: DownloadStateWithInfo()
    }

    View Slide

  90. KOTLIN
    sealed class DownloadStateWithInfo {
    class NotStarted: DownloadStateWithInfo()
    class Downloading(val progress: Float): DownloadStateWithInfo()
    }

    View Slide

  91. KOTLIN
    sealed class DownloadStateWithInfo {
    class NotStarted: DownloadStateWithInfo()
    class Downloading(val progress: Float): DownloadStateWithInfo()
    class Succeeded(val data: ByteArray): DownloadStateWithInfo()
    }

    View Slide

  92. KOTLIN
    sealed class DownloadStateWithInfo {
    class NotStarted: DownloadStateWithInfo()
    class Downloading(val progress: Float): DownloadStateWithInfo()
    class Succeeded(val data: ByteArray): DownloadStateWithInfo()
    class Failed(val error: Error): DownloadStateWithInfo()
    }

    View Slide

  93. BLACK MAGIC WITH

    GENERICS

    View Slide

  94. KOTLIN
    enum class KeyboardType {
    QWERTY,
    ABCDEF,
    QWERTZ,
    AZERTY;
    }

    View Slide

  95. KOTLIN
    inline fun > T.next(): T {
    val currentIndex = this.ordinal
    val nextIndex = currentIndex + 1
    val allValues = enumValues()
    return if (nextIndex >= allValues.size) {
    allValues[0]
    } else {
    allValues[nextIndex]
    }
    }

    View Slide

  96. KOTLIN
    inline fun > T.next(): T {
    // val currentIndex = this.ordinal
    // val nextIndex = currentIndex + 1
    // val allValues = enumValues()
    // return if (nextIndex >= allValues.size) {
    // allValues[0]
    // } else {
    // allValues[nextIndex]
    // }
    }

    View Slide

  97. KOTLIN
    inline fun > T.next(): T {
    // val currentIndex = this.ordinal
    // val nextIndex = currentIndex + 1
    val allValues = enumValues()
    // return if (nextIndex >= allValues.size) {
    // allValues[0]
    // } else {
    // allValues[nextIndex]
    // }
    }

    View Slide

  98. KOTLIN
    inline fun > T.next(): T {
    // val currentIndex = this.ordinal
    // val nextIndex = currentIndex + 1
    // val allValues = enumValues()
    return if (nextIndex >= allValues.size) {
    allValues[0]
    } else {
    allValues[nextIndex]
    }
    }

    View Slide

  99. KOTLIN
    KeyboardType.QWERTY.next() // returns KeyboardType.ABCDEF
    KeyboardType.ABCDEF.next() // returns KeyboardType.QWERTZ
    KeyboardType.QWERTZ.next() // returns KeyboardType.AZERTY
    KeyboardType.AZERTY.next() // returns KeyboardType.QWERTY

    View Slide

  100. KOTLIN
    KeyboardType.QWERTY.next()
    KeyboardType.ABCDEF.next()
    KeyboardType.QWERTZ.next()
    KeyboardType.AZERTY.next()

    View Slide

  101. THE LIMITS
    OF ENUM AWESOMESAUCE

    View Slide

  102. ASSOCIATED OBJECTS = AWESOME
    LISTABILITY = AWESOME
    LISTABILITY + ASSOCIATED OBJECTS =

    View Slide

  103. KOTLIN
    sealed class DownloadStateWithInfo {
    class NotStarted: DownloadStateWithInfo()
    class Downloading(val progress: Float): DownloadStateWithInfo()
    class Succeeded(val data: ByteArray): DownloadStateWithInfo()
    class Failed(val error: Error): DownloadStateWithInfo()
    }

    View Slide

  104. KOTLIN
    DownloadStateWithInfo.NotStarted().next()

    View Slide

  105. KOTLIN
    DownloadStateWithInfo.NotStarted().next() !"!

    View Slide

  106. A LITTLE EASY TO GET
    CARRIED AWAY

    View Slide

  107. SWIFT: GOOD
    enum LandingScreenButton {
    // Stuff we saw before
    var localizedTitle: String {
    switch self {
    case .signIn:
    return NSLocalizedString("Sign In",
    "Sign in button title")
    case .signUp:
    return NSLocalizedString("Sign Up",
    "Sign up button title")
    case .viewTerms:
    return NSLocalizedString("View Terms & Conditions",
    "Title for button to view legalese")
    }
    }

    View Slide

  108. SWIFT: OVER THE TOP
    enum LandingScreenButton {
    // Stuff we saw before
    func handleClick(in viewController: UIViewController) {
    switch self {
    case .signIn:
    viewController.navigationController?
    .pushViewController(SignInViewController(), animated: true)
    case .signUp:
    viewController.navigationController?
    .pushViewController(RegistrationViewController(), animated: true)
    case .viewTerms:
    viewController.present(LegaleseViewController(), animated: true)
    }
    }
    }

    View Slide

  109. IS THIS FUNCTIONALITY
    INHERENTLY RELATED
    TO THE THING I'M REPRESENTING?

    View Slide

  110. SEPERATION OF CONCERNS
    STILL APPLIES!

    View Slide

  111. OBLIGATORY SUMMARY SLIDE

    View Slide

  112. OBLIGATORY SUMMARY SLIDE
    > Enums are a great way to represent distinct state

    View Slide

  113. OBLIGATORY SUMMARY SLIDE
    > Enums are a great way to represent distinct state
    > Limit your cases, limit your bugs

    View Slide

  114. OBLIGATORY SUMMARY SLIDE
    > Enums are a great way to represent distinct state
    > Limit your cases, limit your bugs
    > Value determined by the current case -> computed var

    View Slide

  115. OBLIGATORY SUMMARY SLIDE
    > Enums are a great way to represent distinct state
    > Limit your cases, limit your bugs
    > Value determined by the current case -> computed var
    > Generated enums help reduce stringly-typed code

    View Slide

  116. OBLIGATORY SUMMARY SLIDE
    > Enums are a great way to represent distinct state
    > Limit your cases, limit your bugs
    > Value determined by the current case -> computed var
    > Generated enums help reduce stringly-typed code
    > Don't forget about separation of concerns

    View Slide

  117. CODE EXAMPLES IN SWIFT AND KOTLIN!
    https://github.com/designatednerd/
    StupidEnumTricks

    View Slide

  118. LINKS!
    > The Swift book's section on Enums:
    https://developer.apple.com/library/
    content/documentation/Swift/Conceptual/
    Swift_Programming_Language/
    Enumerations.html
    > Kotlin Enum Class documentation:
    https://kotlinlang.org/docs/reference/
    enum-classes.html

    View Slide