Swift Tip: Switching with Associated Values
Enums are a powerful tool, allowing you to create expressive and precise APIs. However, extracting associated values with a switch
statement sometimes feels a little clunky.
Consider the following example:
enum Layout {
case view(width: CGFloat)
case space(width: CGFloat)
case newline
}
Here, we're modelling a UI layout: we can place either a view or a space on a line, both of which have an associated value for their width, or break to a new line. If we want to compute the total width of views and spaces during layout calculations, we might write something like this:
switch layout {
case let .view(width):
currentX += width
case let .space(width):
currentX += width
case .newline:
break
}
Alternatively, as long as the variables we're binding in the cases match up, we can combine the cases and write this in a more concise way:
switch layout {
case let .view(width), let .space(width):
currentX += width
case .newline:
break
}
This will also work with nested enums. For example, we might want to support different kinds of widths in our layout, such as absolute widths and flexible widths:
enum Width {
case absolute(CGFloat)
case flexible
}
enum Layout {
case view(Width)
case space(Width)
case newline
}
To calculate the minimum width of a line, we want to extract all the absolute width values. We could achieve this by writing an unwieldy switch
statement, duplicating much code:
switch layout {
case let .view(width):
switch width {
case let .absolute(x):
currentX += x
case .flexible:
break
}
case let .space(width):
switch width {
case let .absolute(x):
currentX += x
case .flexible:
break
}
case .newline:
break
}
Instead, we can express the same logic in a more elegant way:
switch layout {
case let .view(.absolute(x)), let .space(.absolute(x)):
currentX += x
case .view(.flexible), .space(.flexible), .newline:
break
}
This technique can result in unwanted complexity, especially where we need to describe many patterns in a single case
. If we add a minimum width value to the .flexible
case, we already have to specify four different patterns:
enum Width {
case absolute(CGFloat)
case flexible(minimum: CGFloat)
}
switch layout {
case let .view(.absolute(x)), let .view(.flexible(x)), let .space(.absolute(x)), let .space(.flexible(x)):
currentX += x
case .newline:
break
}
We can reduce this complexity by using a computed property to separate the matching on the Layout
enum from the matching on the Width
enum:
extension Width {
var minimum: CGFloat {
switch self {
case let .absolute(x), let .flexible(x): return x
}
}
}
switch layout {
case let .view(width), let .space(width):
currentX += width.minimum
case .newline:
break
}
This example is inspired by an upcoming Swift Talk series, where we build an adaptive layout library. Our goal is to define layouts in a way that makes it easy to accommodate large variations in screen size, font size, and so on.
Here's a little preview:
Coming soon to a Swift Talk near you: a new layout library that takes into account whether the content *fits* (rather than the combination explosion of size classes * dynamic type). Bonus feature: brutalist app design :p. https://t.co/wPiu6CJsH1
— Chris Eidhof (@chriseidhof) August 24, 2018