Using structs like enums

Jan 30, 2019 · Follow on Twitter and Mastodon swift

Swift enum and struct are two powerful tools. In this post, I’ll discuss how you typically use them and how to use structs like enums when you need more flexibility.

Enums

Swift Docs describes enums like this: “An enumeration defines a common type for a group of related values and enables you to work with those values in a type-safe way within your code.”

In short, if you have a fixed set of optional, related values, enums are great. Swift enums are very powerful and support associated values, extensions, nesting etc.

For instance, the following enum specifies food options on a menu:

enum Food {
    case 
    hamburger, 
    cheeseBurger, 
    veggieBurger,
    chickenNuggets(pcs: Int)
}

I will not go into further detail on how enums work. If you want to learn more, check out the official docs.

Structs

Swift Docs describes structs like this: “Structs are general-purpose, flexible constructs that become the building blocks of your program’s code. You define properties and methods to add functionality to your structures and classes using the same syntax you use to define constants, variables, and functions.”

In short, if you need to pass values around, structs are great and give you better data integrity than you get with classes. It took me a while to start using structs, but now I can’t live without them.

For instance, the following struct defines a food order with an immutable order number and food items:

struct FoodOrder: Codable {

    let orderNumber: Int
    let items: [Food]
}

As with enums, I will not go into further detail on how structs work. If you want to learn more, check out the official docs.

Using structs instead of enums

Given the discussion above, consider that we’d like to specify a list of possible shadow styles as well as an extension that can be used to apply it to UIKit views.

If you’d asked me some years ago, I would have implemented this using an enum:

enum ShadowStyle {
    
    case
    small,
    medium,
    large
}

We can then attach properties to this enum:

extension ShadowStyle {
    
    var alpha: Float {
        switch self {
        case .small: return 0.2
        case .medium: return 0.26
        case .large: return 0.6
        }
    }
    
    var blur: CGFloat {
        switch self {
        case .small: return 8
        case .medium: return 15
        case .large: return 40
        }
    }
    
    var color: UIColor {
        return .black
    }
    
    var spread: CGFloat {
        return 0
    }
    
    var x: CGFloat {
        return 0
    }
    
    var y: CGFloat {
        switch self {
        case .small: return 4
        case .medium: return 5
        case .large: return 10
        }
    }
}

I would then have created a UIView extension that can be used to apply shadow types to views:

extension UIView {
    
    func applyShadow(_ shadow: ShadowStyle) {
        layer.applyShadow(
            color: shadow.color,
            alpha: shadow.alpha,
            x: shadow.x,
            y: shadow.y,
            blur: shadow.blur,
            spread: shadow.spread)
    }
}

private extension CALayer {
    
    func applyShadow(
        color: UIColor = .black,
        alpha: Float = 0.5,
        x: CGFloat = 0,
        y: CGFloat = 2,
        blur: CGFloat = 4,
        spread: CGFloat = 0) {
        shadowColor = color.cgColor
        shadowOpacity = alpha
        shadowOffset = CGSize(width: x, height: y)
        shadowRadius = blur / 2.0
        shouldRasterize = true
        rasterizationScale = UIScreen.main.scale
        if spread == 0 {
            shadowPath = nil
        } else {
            let dx = -spread
            let rect = bounds.insetBy(dx: dx, dy: dx)
            shadowPath = UIBezierPath(rect: rect).cgPath
        }
    }
}

We can now create shadow instances and use the enum as arguments, like this:

let myShadow = Shadow.medium

let view = UIView(frame.zero)
view.applyShadow(.medium)

This works great, but the enum approach has a big limitation. If the shadow is defined in a library, e.g. in an open source library, we’d be stuck with the fixed values that it provides. There would be no way for an app to create custom shadows.

Sometimes, this is EXACTLY what you want - a fixed set of options. If so, use enums with a smile on your face. However, if you find that the enum model is holding you back, you can simulate enums using structs and get a much more flexible result.

To do so, first create a Shadow struct:

struct Shadow {
    
    init(
        alpha: Float,
        blur: CGFloat,
        color: UIColor = .black,
        spread: CGFloat = 0,
        x: CGFloat,
        y: CGFloat) {
        self.alpha = alpha
        self.blur = blur
        self.color = color
        self.spread = spread
        self.x = x
        self.y = y
    }
    
    let alpha: Float
    let blur: CGFloat
    let color: UIColor
    let spread: CGFloat
    let x: CGFloat
    let y: CGFloat
}

This struct has the same properties as the enum, but they are much easier to overview. You define them when you create a shadow. After that, they’re immutable.

We now have a shadow type with properties, but no predefined styles, opposite to when we created the enum, where we started with the available options, then defined their properties.

I think this approach feels more natural. We can now redefine the shadow styles from before as struct extensions, to provide pre-defined shadow styles:

extension Shadow {

    static var small: Shadow {
        return Shadow(alpha: 0.2, blur: 8, x: 0, y: 4)
    }
    
    static var medium: Shadow {
        return Shadow(alpha: 0.26, blur: 15, x: 0, y: 5)
    }
    
    static var large: Shadow {
        return Shadow(alpha: 0.6, blur: 40, x: 0, y: 10)
    }
}

That’s it! We now have a set of shadow styles that we can apply just like the enum from before:

let myShadow = Shadow.medium

let view = UIView(frame.zero)
view.applyShadow(.medium)

If we would have to create another app-specific shadow for a unique tvOS app, we’d just have to define new, app-specific shadow styles like the ones above.

This is much more flexible. No enum is holding us back anymore. However, if you switch over to structs, there are some side-effects that you must be aware of.

Unexpected side-effects

You can handle structs in much the same way as an enum, e.g. compare two Equatable instances, switch over values etc.

For enums, it would look like this:

public enum Direction: Equatable {
    case up, down, left, right
}

let environment = Direction.down

if environment == .up { print("Up") }       // Won't happen
if environment == .down { print("Down") }   // Will happen

switch environment {
case .up: print("Up")                       // Won't happen
default: print("Not up")                    // Will happen
}

For structs, the equality check and switching looks identical:

struct Direction: Equatable {}

extension Direction {
    static var up: Direction { return Direction() }
    static var down: Direction { return Direction() }
    static var left: Direction { return Direction() }
    static var right: Direction { return Direction() }
}

let direction = Direction.down

if direction == .up { print("Up") }         // Will happen
if direction == .down { print("Down") }     // Will happen

switch direction {
case .up: print("Up")                       // Will happen
default: print("Not up")                    // Won't happen
}

…but the results does not. We create a .down direction, but when we check for equality and switch over it, it’s equal to both .up and .down. What’s going on here?

The reason is that all directions are identical! The Direction struct is implicitly equatable, but uses identical values for all four “cases”.

To solve this, every struct options must be unique. We can solve this by making sure that Equatable behaves correctly, for instance:

struct Direction: Equatable {
    let name: String
}

extension Direction {
    static var up: Direction { return Direction(name: "up") }
    static var down: Direction { return Direction(name: "down") }
    static var left: Direction { return Direction(name: "left") }
    static var right: Direction { return Direction(name: "right") }
}

let direction = Direction.down

if direction == .up { print("Up") }         // Won't happen
if direction == .down { print("Down") }     // Will happen

switch direction {
case .up: print("Up")                       // Won't happen
default: print("Not up")                    // Will happen
}

By making each direction unique, this now works! We don’t have to implement any equality logic, just provide the struct with a property that will be unique for each case.

Conclusion

Enums are great when you want a fixed set of options. However, structs that behave like enums are better when you want a type that can be extended with more options.

Another option is to used parameterized enum cases, but when this post was written, Swift enums couldn’t implement certain protocols if a case had parameters. This is however no longer the case.

If you decide to use structs in this way, just watch out for unexpected pitfalls.

Discussions & More

Please share any ideas, feedback or comments you may have in the Disqus section below, or by replying on Twitter or Mastodon..

If you found this text interesting, make sure to follow me on Twitter and Mastodon for more content like this, and to be notified when new content is published.

If you like & want to support my work, please consider sponsoring me on GitHub Sponsors.