Getting to Know Enum, Struct and Class Types in Swift

Learn all about enums, structs, and classes in Swift, including value vs reference semantics, dynamic member lookup, and protocol conformance. By Adam Rush.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Associated Values

ColorName is good for named colors, but you might recall that CSS colors have several representations: named, RGB, HSL and more.

Enums in Swift are great for modeling things that have one of a number of representations, such as CSS color, and each enum case can be paired with its own data. These data are called associated values.

Define CSSColor using an enum by adding the following to the end of your playground:

enum CSSColor {
  case named(name: ColorName)
  case rgb(red: UInt8, green: UInt8, blue: UInt8)
}

With this definition, you give the CSSColor model one of two states:

  1. It can be named, in which case the associated data is a ColorName value.
  2. It can be rgb, in which case the associated data is three UInt8 (0-255) numbers for red, green and blue.

Note that this example leaves out rgba, hsl and hsla cases for brevity.

Protocols and Methods With an Enum

Any model type in Swift can adopt a protocol

Because CSSColor has associated values, it’s harder (though not impossible) to make it conform to RawRepresentable. The easiest way to get a string representation out of the new enum is by making it conform to CustomStringConvertible.

The key to inter-operating with the Swift Standard Library is to adopt standard library protocols.

Add the following extension for CSSColor to the end of your playground:

extension CSSColor: CustomStringConvertible {
  var description: String {
    switch self {
    case .named(let colorName):
      return colorName.rawValue
    case .rgb(let red, let green, let blue):
      return String(format: "#%02X%02X%02X", red, green, blue)
    }
  }
}

In this implementation, description switches upon self to determine if the underlying model is a named or RGB type. In each case, you convert the color to the required string format. The named case just returns the string name, whereas the RGB case returns the red, green and blue values in the required format.

To see how this works, add the following to your playground:

let color1 = CSSColor.named(name: .red)
let color2 = CSSColor.rgb(red: 0xAA, green: 0xAA, blue: 0xAA)

// prints color1 = red, color2 = #AAAAAA
print("color1 = \(color1), color2 = \(color2)") 

Everything is type checked and proven correct at compile time, unlike when you use only String values to represent colors.

The extension style is nice because it makes what you define fully explicit, in order to conform to a given protocol. In the case of CustomStringConvertible, you’re required to implement a getter for description.

Note: While you could go back to your previous definition of CSSColor and modify it, you don’t have to. You’ve used an extension to re-open the color type and adopt a new protocol.

Initializers With an Enum

Just like classes and structs in Swift, you can also add custom initializers to enum. For example, you can make a custom initializer for grayscale values.

Add this extension to your playground:

extension CSSColor {
  init(gray: UInt8) {
    self = .rgb(red: gray, green: gray, blue: gray)
  }
}

Add the following to your playground:

let color3 = CSSColor(gray: 0xaa)
print(color3) // prints #AAAAAA

You can now conveniently create grayscale colors!

Namespaces With Enum

Named types can act as a namespace to keep things organized and to minimize complexity. You created ColorName and CSSColor, and, yet, ColorName is only ever used in the context of a CSSColor.

Wouldn’t it be nice if you could nest ColorName within the CSSColor model?

Well, you can! Remove ColorName from your playground and replace it with the following code:

extension CSSColor {
  enum ColorName: String, CaseIterable {
    case black, silver, gray, white, maroon, red, purple, fuchsia, green, 
      lime, olive, yellow, navy, blue, teal, aqua
  }
}

This moves ColorName into an extension on CSSColor. Now, ColorName is tucked away, and the inner type is defined on CSSColor.

Since it’s now nested, the for loop you created earlier needs to be updated as well. Change it to the following:

for color in CSSColor.ColorName.allCases {
    print("I love the color \(color).")
}

However, if you receive an error in your playground about ColorName being an undeclared type, move the above extension to just below your enum definition of CSSColor to clear the playground error.

Sometimes, playgrounds are sensitive to the ordering of definitions, even when it doesn’t really matter.

Note: One of the great features of Swift is that the order in which you declare things usually doesn’t matter. The compiler scans the file multiple times and figures it out without needing to forward declare things as it does when working with C/C++/Objective-C.

Taking Stock of Enums

Enums are much more powerful in Swift than they are in other languages, such as C or Objective-C. As you’ve seen, you can extend them, create custom initializer methods, provide namespaces and encapsulate related operations.

So far, you’ve used enum to model CSS colors. This works well because CSS colors are a well understood, fixed W3C specification.

Enumerations are great for picking items from a list of well-known things, such as days of the week, faces of a coin or states in a state machine. It’s no surprise that Swift optionals are implemented in terms of an enum with a state of .none or .some with an associated value.

On the other hand, if you wanted CSSColor to be user extensible to other color space models that are not defined in the W3C specification, an enumeration is not the most useful way of modeling colors.

That brings you to the next Swift named model type: structures or structs.

Using Structs

Because you want your users to be able to define their own custom shapes within the SVG, using an enum is not a good choice for defining shape types.

You cannot add new enum cases later in an extension. To enable that behavior, you have to use either a class or a struct.

The Swift Standard Library team suggests that, when you create a new model, you should first design the interface using a protocol. You want your shapes to be drawable, so add this to your playground:

protocol Drawable {
  func draw(with context: DrawingContext)
}

The protocol defines what it means to be Drawable. It has a draw method that draws to something called a DrawingContext.

Speaking of DrawingContext, it’s just another protocol. Add it to your playground as follows:

protocol DrawingContext {
  func draw(_ circle: Circle)
}

A DrawingContext knows how to draw pure geometric types: Circle, Rectangle and other primitives. Take note of something here: the actual drawing technology is not specified, but you could implement it in terms of anything — SVG, HTML5 Canvas, Core Graphics, OpenGL, Metal, etc.

You’re ready to define a circle that adopts the Drawable protocol. Add this to your playground:

struct Circle: Drawable {
  var strokeWidth = 5
  var strokeColor = CSSColor.named(name: .red)
  var fillColor = CSSColor.named(name: .yellow)
  var center = (x: 80.0, y: 160.0)
  var radius = 60.0

  // Adopting the Drawable protocol.

  func draw(with context: DrawingContext) {
    context.draw(self)
  }
}

Any type that conforms to DrawingContext now knows how to draw a Circle.