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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting to Know Enum, Struct and Class Types in Swift
35 mins
- It’s All About Those Types
- Shapes With Scalable Vector Graphics (SVG)
- Getting Started
- Using Enums
- CaseIterable
- Associated Values
- Protocols and Methods With an Enum
- Initializers With an Enum
- Namespaces With Enum
- Taking Stock of Enums
- Using Structs
- Dynamic Member Lookup
- Value vs. Reference Types
- Rectangle Model
- Show Me Some SVG
- Using Classes
- Why Even Use a Class?
- Implementing Computed Properties
- Retroactive Modeling and Type Constraining
- Where to Go From Here?
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:
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:
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:
- It can be
named
, in which case the associated data is aColorName
value. - It can be
rgb
, in which case the associated data is threeUInt8
(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
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
.
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.
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 struct
s.
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 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
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:
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.
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!