Blog

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.

Stay up-to-date with our newsletter or follow us on Twitter.

Back to the Blog

recent posts