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?
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.