Swift Tip: Improving Readability
In the final episode of our Swift Talk series, Refactoring Large View Controllers, we factored out view code from the view controller into a custom view class.
Aiming to simplify the view controller, we decided to focus on rearranging existing code. In the process of doing so it became apparent that the code we were moving could be improved, making it easier to read, more robust, and more readily testable.
As an example, here's the original code for pan gesture recognizer events:
@objc func handlePanGesture(_ panGR: UIPanGestureRecognizer) {
switch panGR.state {
// ...
case .cancelled:
let currentHeight = heightConstraint.constant
let newState: State
if currentHeight <= midHeight {
let min: Bool = currentHeight - minHeight <= midHeight - currentHeight
if min {
newState = .min
} else {
newState = .mid
}
} else {
let mid: Bool = currentHeight - midHeight <= maxHeight - currentHeight
if mid {
newState = .mid
} else {
newState = .max
}
}
set(overlayState: newState, withVelocity: panGR.velocity(in: self).y, animated: true)
// ...
}
}
The pan gesture resizes an overlay view. When it ends, this code determines the new state of the overlay: minimized, maximized, or at mid-height. To understand how this code functions the underlying logic should be clear and readable.
With this in mind, we move the logic into a separate method, and create a higher-level abstraction in the gesture recognizer delegate method. The method returns the new overlay state depending on its current height:
private func state(for height: CGFloat) -> State {
let newState: State
if height <= midHeight {
let min: Bool = height - minHeight <= midHeight - height
if min {
newState = .min
} else {
newState = .mid
}
} else {
let mid: Bool = height - midHeight <= maxHeight - height
if mid {
newState = .mid
} else {
newState = .max
}
}
return newState
}
The gesture recognizer delegate method is now much easier to understand:
@objc func handlePanGesture(_ panGR: UIPanGestureRecognizer) {
switch panGR.state {
// ...
case .cancelled:
let newState = state(for: heightConstraint.constant)
set(overlayState: newState, withVelocity: panGR.velocity(in: self).y, animated: true)
// ...
}
}
There is still work to do: the logic in our new private method could be better implemented; we have four branches in a doubly-nested if
statement, though we only have three possible results (.min
, .mid
, and .max
). Reading the code at a later date, we might wonder whether this was intentional.
An exact understanding of the boundary conditions also requires very careful reading. For example, does the midpoint between the minHeight
and midHeight
result in a .min
or .mid
state?
A switch statement should help us here:
private func state(for height: CGFloat) -> State {
let midRange = (minHeight+midHeight)/2..<(midHeight+maxHeight)/2
switch height {
case ..<midRange.lowerBound: return .min
case midRange.upperBound...: return .max
default: return .mid
}
}
The first line defines the range of height values that should result in a .mid
state. Using a half-open range, the boundary conditions are clear: the mid-point between minHeight
and midHeight
is part of the .mid
state, but not the mid-point between midHeight
and maxHeight
. The switch statement itself has exactly three cases, one for each possible result.
To complete the last step in this micro-refactoring, we pull out the code from our custom view class. We can express this logic as a pure function, as shown in the first episode of the Refactoring Large View Controllers series:
extension OverlayView.State {
static func state(for height: CGFloat, midRange: Range<CGFloat>) -> OverlayView.State {
switch height {
case ..<midRange.lowerBound: return .min
case midRange.upperBound...: return .max
default: return .mid
}
}
}
This function is not only easy to understand, it's also simple to test, having no dependencies and producing no side-effects.
We recently published a book about App Architecture, but it's important to remember that the time and care spent to improve code on the micro-level is at least as important as high-level architectural decisions. There are no fancy acronyms or complicated flow diagrams associated with this kind of work — it's just part of the daily, humble routine that keeps everything running.