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 3 of 4 of this article. Click here to view the first page.

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.