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.

Version

  • Swift 4.2, iOS 12, Xcode 10
Update note: Adam Rush updated this tutorial for Xcode 10, iOS 12 and Swift 4.2. Ray Fix wrote the original.

Back in the days when there was only Objective-C, encapsulation was limited to working with classes. However, in modern iOS and macOS programming using Swift, there are three choices: enums, structs and classes.

Combined with protocols, these types make it possible to create amazing things. While they share many common abilities, these types also have important differences.

The objective of this tutorial is to:

  • Give you some experience using enums, structs and classes.
  • Grant you some intuition about when to use them.
  • Give you an understanding of how each works.

In terms of prerequisites, this tutorial assumes that you have at least the basics of Swift and some object-oriented programming experience. If you want to learn the basics of Swift checkout our Swift Apprentice Book

It’s All About Those Types

Three big selling points of Swift are its safety, speed and simplicity.

Safety implies that it’s difficult to accidentally write code that runs amok, corrupting memory and producing hard-to-find bugs. Swift makes your work safer because it tries to make it obvious when you have a bug by showing you problems at compile time, rather than hanging you out to dry at runtime.

The key to making this happen is the Swift type system:

Diagram showing the six types in Swift

Swift types are powerful, despite there being only six of them. That’s right – unlike many other languages that have literally dozens of built-in types, Swift only has six.

These consist of four named types: protocol, enum, struct and class. There are two compound types as well: tuple and function.

There are those other things that you might think of as basic types, such as Bool, Int, UInt, Float, etc. However, these are actually built up from the named types and delivered as part of the Swift Standard Library.

This tutorial focuses on the so-called named model types, which consist of enum, struct and class.

Shapes With Scalable Vector Graphics (SVG)

As a working example, you’ll build a safe, speedy and simple SVG shape (scalable vector graphics) rendering framework.

SVG is an XML-based vector image format for 2D graphics. This specification has been an open standard developed by the W3C since 1999.

Getting Started

Create a new playground in Xcode to follow along by choosing File ▸ New ▸ Playground… from the menu. Next, choose the platform as macOS and choose the Blank template. Next, name it Shapes and choose a location to save it, then click Create to save the playground. Clear the file completely, then enter the following:

import Foundation

Your goal will be to render something like this:

<!DOCTYPE html>
<html>
  <body>
    <svg width='250' height='250'>
      <rect x='110.0' y='10.0' width='100.0' height='130.0' stroke='teal' 
        fill='aqua' stroke-width='5' />
      <circle cx='80.0' cy='160.0' r='60.0' stroke='red' fill='yellow' 
        stroke-width='5' />
    </svg>
  </body>
</html>

Using a WebKit view, it looks like this:

shapes

You’ll need a representation for colors. SVG uses the CSS3 color type that can be specified as a name, RGB or HSL. For more details, you can read the full specification.

To use a color in SVG, you specify it as an attribute of part of your drawing — for example, fill = 'gray'. An easy approach to this in Swift is to use a String — as in, let fill = "gray".

While using String is easy and does the job, there are some major downsides:

  • It’s error prone. Any strings that are not part of the color spectrum will compile fine but not show up correctly at runtime. For example, “grey” spelled with an “e” doesn’t work.
  • Autocomplete won’t help you find valid color names.
  • When you pass around a color as a parameter, it might not always be obvious that the string is a color.

Using Enums

Using a custom type solves these problems. If you’re coming from Cocoa Touch, you might think to implement an encapsulated class like UIColor. While using a class design could work, Swift gives you more choices for how to define your model.

Without typing anything in just yet, first have a think how you might implement the colors as an enum.

You might think to implement it, like so:

enum ColorName {
  case black
  case silver
  case gray
  case white
  case maroon
  case red
  // etc.
}

The above works very similarly to a set of C-style enums. However, unlike C-style enums, Swift gives you the option to specify a type to represent each case.

Enumerations that explicitly specify a backing store type are referred to as RawRepresentable, because they automatically conform to RawRepresentable.

You can specify the type of ColorName as String, and you assign a value to each case, like so:

enum ColorName: String {
  case black = "black"
  case silver = "silver"
  case gray = "gray"
  case white = "white"
  case maroon = "maroon"
  case red = "red"
  // etc.
}

However, Swift does something special for enums with a String representation. If you don’t specify what the case is equal to, the compiler automatically makes the string the same as the name of the case. That means that you only need to write the case name:

enum ColorName: String {
  case black
  case silver
  case gray
  case white
  case maroon
  case red
  // etc.
}

You can further reduce your typing by separating the cases with commas using the keyword case just once.

Add the following code to the end of your playground:

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

Now, you have a first-class custom type and all the goodness that comes with that.

let fill = ColorName.grey // ERROR: Misspelled color names won't compile. Good!
let fill = ColorName.gray // Correct names autocomplete and compile. Yay!

CaseIterable

Enums in Swift are great for holding a list of items such as our example list of colors. To make enums even more powerful, Swift 4.2 added a new protocol named CaseIterable that provides a collection of all the values of the conformer.

At compile time, Swift will automatically create an allCases property that is an array of all your enum cases, in the order you defined them.

Using CaseIterable is very simple. All you have to do is declare the conformance in the definition of ColorName as shown below:

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

You can then use the allCases property whose type is [ColorName]. Add the following to the end of your playground:

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

In the console, you’ll see 16 lines printed — one for every color in ColorName.

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.

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.

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.

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).")
}
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.

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.

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.

Dynamic Member Lookup

Swift 4.2 introduces a way to bring Swift a lot closer to scripting languages such as Python. You don’t lose any of Swift’s safety, but you do gain the ability to write the kind of code you’re more likely to see in Python.

Inside this new feature is a new attribute called @dynamicMemberLookup. This will call a subscript method when trying to access the properties.

Replace your current Circle implementation with the following:

@dynamicMemberLookup
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)
  }
}

With the above, you have defined the new @dynamicMemberLookup attribute to the Circle struct. This requires Circle to implement subscript(dynamicMember:) method to handle the implementation of your @dynamicMemberLookup.

Add the following inside the Circle struct:

subscript(dynamicMember member: String) -> String {
  let properties = ["name": "Mr Circle"]
  return properties[member, default: ""]
}

You can now access the name, hard-coded to “Mr Circle”, of your Circle by adding the following code:

let circle = Circle()
let circleName = circle.name

After all, all shapes have names. :]

The Dynamic Member Lookup attribute can be added to a class, struct, enum or protocol declaration.

Structs work a lot like classes with a couple of key differences. Perhaps the biggest difference is that structs are value types and classes are reference types. Now what does that mean?!

Value vs. Reference Types

value_type

Value types work as separate and distinct entities. The quintessential value type is an integer because it works that way in most programming languages.

If you want to know how a value type acts, ask the question, “What would Int do?” For example:

For Int:

var a = 10
var b = a
a = 30 // b still has the value of 10
a == b // false

For Circle (defined using struct):

var a = Circle()
a.radius = 60.0
var b = a
a.radius = 1000.0 // b.radius still has the value 60.0

reference_type

If you had made your circle from a class type, it would have been given reference semantics. That means that it references an underlying shared object.

For Circle (defined using class):

let a = Circle() // a class based circle
a.radius = 60.0
let b = a
a.radius = 1000.0  // b.radius also becomes 1000.0

When creating new objects using value types, copies are made; when using reference types, the new variable refers to the same object. This dissimilarity in behavior is a critical difference between class and struct.

Rectangle Model

Your Circle is a bit lonely currently, so it’s time to add a Rectangle model:

struct Rectangle: Drawable {
  var strokeWidth = 5
  var strokeColor = CSSColor.named(name: .teal)
  var fillColor = CSSColor.named(name: .aqua)
  var origin = (x: 110.0, y: 10.0)
  var size = (width: 100.0, height: 130.0)

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

You also need to update the DrawingContext protocol so that it knows how to draw a rectangle. Replace DrawingContext in your playground with the following:

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

Circle and Rectangle adopt the drawable protocol. They defer the actual work to something that conforms to the DrawingContext protocol.

Now, it’s time to make a concrete model that draws in SVG style. Add this to your playground:

final class SVGContext: DrawingContext {
  private var commands: [String] = []

  var width = 250
  var height = 250

  // 1
  func draw(_ circle: Circle) {
    let command = """
      <circle cx='\(circle.center.x)' cy='\(circle.center.y)\' r='\(circle.radius)' \
      stroke='\(circle.strokeColor)' fill='\(circle.fillColor)' \
      stroke-width='\(circle.strokeWidth)' />
      """
    commands.append(command)
  }

  // 2
  func draw(_ rectangle: Rectangle) {
    let command = """
      <rect x='\(rectangle.origin.x)' y='\(rectangle.origin.y)' \
      width='\(rectangle.size.width)' height='\(rectangle.size.height)' \
      stroke='\(rectangle.strokeColor)' fill='\(rectangle.fillColor)' \
      stroke-width='\(rectangle.strokeWidth)' />
      """
    commands.append(command)
  }

  var svgString: String {
    var output = "<svg width='\(width)' height='\(height)'>"
    for command in commands {
      output += command
    }
    output += "</svg>"
    return output
  }

  var htmlString: String {
    return "<!DOCTYPE html><html><body>" + svgString + "</body></html>"
  }
}

SVGContext is a class that wraps a private array of command strings. In sections 1 and 2, you conform to the DrawingContext protocol, and the draw methods append a string with the correct XML for rendering the shape.

Finally, you need a document type that can contain many Drawable objects, so add this to your playground:

struct SVGDocument {
  var drawables: [Drawable] = []

  var htmlString: String {
    let context = SVGContext()
    for drawable in drawables {
      drawable.draw(with: context)
    }
    return context.htmlString
  }

  mutating func append(_ drawable: Drawable) {
    drawables.append(drawable)
  }
}

Here, htmlString is a computed property on SVGDocument that creates an SVGContext and returns the string with HTML from the context.

Show Me Some SVG

How about you finally draw an SVG? Add this to your playground:

var document = SVGDocument()

let rectangle = Rectangle()
document.append(rectangle)

let circle = Circle()
document.append(circle)

let htmlString = document.htmlString
print(htmlString)

This code creates a default circle and rectangle, and it puts them into a document. It then prints the XML. Add the following to the end of the playground to see the SVG in action:

import WebKit
import PlaygroundSupport
let view = WKWebView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
view.loadHTMLString(htmlString, baseURL: nil)
PlaygroundPage.current.liveView = view

This does some playground trickery and sets up a web view to view the SVG. Press Command-Option-Return to show this web view in the assistant editor. Ta-da!

Using Classes

So far, you used a combination of structs (value types) and protocols to implement drawable models.

Now, it’s time to play with classes, too. Classes let you define base classes and derived classes. The more traditional object-oriented approach to the shapes problem is to make a Shape base class with a draw() method.

Even though you won’t use it now, it’s helpful to know how this would work. It would look something like this:

hierarchy

And, in code, it would look like the following block — this is just for reference, so don’t add it to your playground:

class Shape {
  var strokeWidth = 1
  var strokeColor = CSSColor.named(name: .black)
  var fillColor = CSSColor.named(name: .black)
  var origin = (x: 0.0, y: 0.0)
  func draw(with context: DrawingContext) { fatalError("not implemented") }
}

class Circle: Shape {
  override init() {
    super.init()
    strokeWidth = 5
    strokeColor = CSSColor.named(name: .red)
    fillColor = CSSColor.named(name: .yellow)
    origin = (x: 80.0, y: 80.0)
  }

  var radius = 60.0
  override func draw(with context: DrawingContext) {
    context.draw(self)
  }
}

class Rectangle: Shape {
  override init() {
    super.init()
    strokeWidth = 5
    strokeColor = CSSColor.named(name: .teal)
    fillColor = CSSColor.named(name: .aqua)
    origin = (x: 110.0, y: 10.0)
  }

  var size = (width: 100.0, height: 130.0)
  override func draw(with context: DrawingContext) {
    context.draw(self)
  }
}

In order to make object-oriented programming safer, Swift introduced the override keyword. It requires that you, the programmer, acknowledge when you’re overriding something.

line

Despite how common this pattern is, there are some drawbacks to this object-oriented approach.

The first problem you’ll notice is in the base-class implementation of draw. Shape wants to avoid being misused, so it calls fatalError() to alert derived classes that they need to override this method. Unfortunately, this check happens at runtime time and not compile time.

Secondly, the Circle and Rectangle classes have to deal with the initialization of the base-class data. While this is a relatively easy scenario, class initialization can become a somewhat involved process in order to guarantee correctness.

Thirdly, it can be tricky to future proof a base class. For example, suppose you wanted to add a drawable Line type. In order to work with your existing system, it would have to derive from Shape, which is a little bit of a misnomer.

Moreover, your Line class needs to initialize the base class’s fillColor property, and that doesn’t really make sense for a line.

Finally, classes have the reference (shared) semantics that were discussed earlier. While Automatic Reference Counting (ARC) takes care of things most of the time, you need to be careful not to introduce reference cycles or you’ll end up with memory leaks.

If you add the same shape to an array of shapes, you might be surprised when you modify the color of one shape to red and another one also seems to randomly change.

Why Even Use a Class?

Given the above downsides, you might wonder why you would ever want to use a class.

For starters, they allow you to adopt mature and battle-tested frameworks like Cocoa and Cocoa Touch.

Additionally, classes do have more important uses. For example, a large memory-hogging, expensive-to-copy object is a good candidate for wrapping in a class. Classes can model an identity well. You may have a situation in which many views are displaying the same object. If that object is modified, all of the views also reflect changes in the model. With a value type, synchronizing updates can become an issue.

In short, classes are helpful anytime reference versus value semantics come into play.

Check out this two-part tutorial on the subject: Reference vs. Value Types in Swift.

Implementing Computed Properties

All named model types let you create custom setters and getters that don’t necessarily correspond to a stored property.

Suppose you want to add a diameter getter and setter to your Circle model. It’s easy to implement it in terms of the existing radius property.

Add the following code to the end of your playground:

extension Circle {
  var diameter: Double {
    get {
      return radius * 2
    }
    set {
      radius = newValue / 2
    }
  }
}

This implements a new computed property that is purely based on the radius. When you get the diameter, it returns the radius doubled. When you set the diameter, it sets the radius to the value divided by two. Simple!

More often than not, you only want to implement a special getter. In this case, you don’t have to include the get {} keyword block and can just specify the body. Perimeter and area are good use cases for this.

Add the following to the Circle extension you just added:

// Example of getter-only computed properties
var area: Double {
  return radius * radius * Double.pi
}
var perimeter: Double {
  return 2 * radius * Double.pi
}

Unlike classes, struct methods are not allowed to modify, or mutate, stored properties by default, but they can if you declare them as mutating.

For example, add the following to the Circle extension:

func shift(x: Double, y: Double) {
  center.x += x
  center.y += y
}

This tries to define a shift() method on circle, which moves the circle in space — i.e., it changes the center point.

But this generates the following error on the two lines, which increment the center.x and center.y properties.

// ERROR: Left side of mutating operator has immutable type ‘Double'

This can be fixed by adding the mutating keyword, like so:

mutating func shift(x: Double, y: Double) {
  center.x += x
  center.y += y
}

This tells Swift that it’s OK that your function mutates the struct.

Retroactive Modeling and Type Constraining

One of the great features of Swift is retroactive modeling. It lets you extend the behavior of a model type even if you don’t have the source code for it.

Here’s a use case: Suppose you’re a user of the SVG code and you want to add an area and perimeter to Rectangle just like Circle.

To see what this all means, add this to your playground:

extension Rectangle {
  var area: Double {
    return size.width * size.height
  }
  var perimeter: Double {
    return 2 * (size.width + size.height)
  }
}

This adds an extension to add area and perimeter to an existing model, and, now, you’ll formalize these methods into a new protocol.

Add this to your playground:

protocol ClosedShape {
  var area: Double { get }
  var perimeter: Double { get }
}

That gives you an official protocol.

Next, you’ll tell Circle and Rectangle to conform to this protocol retroactively by adding the following to your playground:

extension Circle: ClosedShape {}
extension Rectangle: ClosedShape {}

You can also define a function that, for example, computes the total perimeter of an array of models (any mix of structs, enums or classes) that adopt the ClosedShape protocol.

Add the following to the end of the playground:

func totalPerimeter(shapes: [ClosedShape]) -> Double {
  return shapes.reduce(0) { $0 + $1.perimeter }
}

totalPerimeter(shapes: [circle, rectangle])

This uses reduce to calculate the sum of perimeters. You can learn more about how it works in An Introduction to Functional Programming.

Where to Go From Here?

In this tutorial, you learned about enum, struct and class — the named model types of Swift.

All three have key similarities: They provide encapsulation, can have initializer methods, can have computed properties, can adopt protocols, and can be modeled retroactively.

I hope you have enjoyed this whirlwind tour of the named model types in Swift. If you’re looking for a challenge, consider building a more complete version of the SVG rendering library. You’re off to a good start!

As always, if you have questions or insights you would like to share, please use the forums below!

Contributors

Comments