Application Design: Enums (with bonus tuple)

In the zoom example app, to make life easier for me, I went for a cyclical zoom: 2x magnification, 3x magnification, 1x magnification. When Apple first announced Swift, I liked many things about it, but the thing that made me swoon was enums. Swift enums are wonderful tools to shape your application flow.

Here, we have three states (I reserved the right to play with actual values, otherwise I’d have given them more definite names):

enum ZoomTransformation {

case neutral
case step1
case step2

This is a mini state machine, and thus we have a ‘next’ variable:

var next: ZoomTransformation{
switch self {
case .neutral: return .step1
case .step1: return .step2
case .step2: return .neutral
}
}

As state machines go, this is the simplest case: each state transforms into exactly one other, and there’s no calculation involved at all.

This allowed me to give the main viewController a

var transformationState: ZoomTransformation = .neutral

property.

Instead of enshrining the zoom values in the viewController, I placed them in the enum:

var transform: CGAffineTransform {
switch self {
case .neutral:
return CGAffineTransform.identity
case .step1:
var transform1 = CGAffineTransform.identity
transform1 = transform1.translatedBy(x: -100, y: -300)
transform1 = transform1.scaledBy(x: 2, y: 2)
return transform1
case .step2:
var transform2 = CGAffineTransform.identity
transform2 = transform2.translatedBy(x: -200, y: -600)
transform2 = transform2.scaledBy(x: 3, y: 3)
return transform2
}
}

which allowed the zoom function to be simplified greatly:

func twoStepZoom(){
transformationState = transformationState.next
bluebellImageView.layer?.setAffineTransform(transformationState.transform)
}

This starts with setting the transformationState to step1. There’s no good way around this: I wanted the app to start with neutral state, and wanted .neutral to be the reset-to-normal transformation. I felt this was more important than starting with step1 or rotating the code, so we increment the ‘counter’ before zooming.

When the time came to use the magnification of NSScrollView, extending the enum was trivial:

func setMagnificationFor(_ rect: NSRect) -> (factor: CGFloat, centeredAt: NSPoint) {
let centrepoint = NSPoint(x: rect.height / 2, y: rect.width/2)
switch self {
case .neutral:
return(factor: 1, centeredAt: centrepoint)
case .step1:
return(factor: 2, centeredAt: NSPoint(x: centrepoint.x-100, y:centrepoint.y-300))
case .step2:
return(factor: 3, centeredAt: NSPoint(x: centrepoint.x-200, y:centrepoint.y-600))
}
}

This function takes an NSRect as an argument (the bounds of the scrollView) and returns a tuple. Tuples are wonderful lightweight data structures – they’re typed, which makes them harder to get wrong than dictionaries.

The reason behind this odd structure is that the entirety of the twoStepZoom function can now be changed to

let factor = transformationState.setMagnificationFor(scrollView.bounds)
scrollView.setMagnification(factor.factor, centeredAt: factor.centeredAt)

We calculate the two values that the scrollView’s setMagnification method needs, and then pass them in.
Ordinarily, resetting a scrollView to its standard size is done with

scrollView.magnify(toFit: scrollView.bounds)

but at this has a different signature, one would have to branch the code.

And that’s it: a very simple state machine, the use of enums to keep all of the calculation code together and tuples allow us to pass very differnt types without the need for a custom datatype, since we don’t want to _model_ a CGFloat/NSPoint item, we just want some values to call a function with. I feel these are very Swift ways of doing things, and I find the resulting code easy to understand.