Blog

Latest Post

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.

Previous Posts

Swift Tip: Notifications

In Episode 107 of our Swift Talk series on refactoring large view controllers, we move code from a view controller to a child view controller. The resulting child view controller deals with keyboard notifications.

In this week’s tip, we’ll show how to factor out that code as well.

In multiple places throughout the code base there are notification listeners for keyboard events. In each of the callback methods the properties are manually extracted, for example:

@objc func keyboardChanged(notification: NSNotification) {
  guard let userInfo = notification.userInfo,
        let frameValue = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
  let keyboardScreenFrame = frameValue.cgRectValue
  // ...
}

Instead of repeating this kind of code over and over, we can make life easier by writing a single struct that contains all the properties:

struct KeyboardInfo {
    var animationCurve: UIView.AnimationCurve
    var animationDuration: Double
    var isLocal: Bool
    var frameBegin: CGRect
    var frameEnd: CGRect
}

We can now write an initializer that constructs the struct from a notification:

extension KeyboardInfo {
    init?(_ notification: Notification) {
        guard notification.name == UIResponder.keyboardWillShowNotification || notification.name == UIResponder.keyboardWillChangeFrameNotification else { return nil }
        let u = notification.userInfo!

        animationCurve = UIView.AnimationCurve(rawValue: u[UIWindow.keyboardAnimationCurveUserInfoKey] as! Int)!
        animationDuration = u[UIWindow.keyboardAnimationDurationUserInfoKey] as! Double
        isLocal = u[UIWindow.keyboardIsLocalUserInfoKey] as! Bool
        frameBegin = u[UIWindow.keyboardFrameBeginUserInfoKey] as! CGRect
        frameEnd = u[UIWindow.keyboardFrameEndUserInfoKey] as! CGRect
    }
}

In our keyboardChanged(notification:) method, we can construct that struct, rather than parsing the user info dictionary manually:

@objc func keyboardChanged(notification: NSNotification) {
  guard let payload = KeyboardInfo(notification) else { return }
  let keyboardScreenFrame = payload.frameEnd
  // ...
}

Our code is a little bit more concise, and it’s also a little safer (the dictionary reading is now in a single place, rather than spread across the code base). When we want to access other properties, we can now type payload. and auto complete will suggest the different names.

In Swift Talk Episode 27 (a public episode), we take this approach a bit further, and we also add a way to create type-safe observers.

If you’d like to follow the series, the first episode is public, and you can become a subscriber to see how we progress. πŸ‘

Swift Tip: Refactoring with Deprecations

During a large refactoring, we often work for a long time without being able to compile and run our code. In last week’s Swift Talk, we factored out a child view controller from a very large view controller, and we tried to avoid this danger by using computed properties and deprecation warnings. Let’s look at an example.

The view controller we were working on β€” Wikipedia’s places view controller β€” has many outlets, some of which we wanted to pull out into the new child view controller:

class PlacesViewController: PreviewingViewController /* ... */ {
    // ...
    @IBOutlet weak var listAndSearchOverlayFilterSelectorContainerView: UIView!
    @IBOutlet weak var listAndSearchOverlaySearchBar: UISearchBar!
    // ...
}

In total, we moved about ten outlets from the places view controller into a new child view controller. These outlets are used throughout the places view controller, and removing them breaks a lot of code; without any special measures we would be working for a few hours before the code compiles again. To keep the code compiling, we moved the original outlets to the new view controller and created computed properties in the original view controller:

class PlacesViewController: PreviewingViewController /* ... */ {
    // ...
    var searchViewController: PlacesSearchViewController! // this refers to the new child view controller

    var listAndSearchOverlayFilterSelectorContainerView: UIView! {
        return searchViewController.listAndSearchOverlayFilterSelectorContainerView
    }
    var listAndSearchOverlaySearchBar: UISearchBar! {
        return searchViewController.listAndSearchOverlaySearchBar
    }
    // ...
}

class PlacesSearchViewController: UIViewController {
    // ...
    @IBOutlet weak var listAndSearchOverlayFilterSelectorContainerView: UIView!
    @IBOutlet weak var listAndSearchOverlaySearchBar: UISearchBar!
    // ...
}

To be clear, this is a temporary fix used to make the code compile, not a long-term solution. We want to move all code that depends on these views into the new child view controller, but we want to do it step-by-step.

Now that our code compiles, it’s easy to forget to move code out of the old view controller. To prevent such oversights (and the temptation to stop short of our original goal), we add deprecation annotations to the computed properties:

class PlacesViewController: PreviewingViewController /* ... */ {
    // ...
    @available(*, deprecated) var listAndSearchOverlayFilterSelectorContainerView: UIView! {
        return searchViewController.listAndSearchOverlayFilterSelectorContainerView
    }
    @available(*, deprecated) var listAndSearchOverlaySearchBar: UISearchBar! {
        return searchViewController.listAndSearchOverlaySearchBar
    }
    // ...
}

With these annotations in place, each usage of a computed property shows up both as an inline warning and in the issue navigator β€” a good reminder of the work we still have to do (and a better reminder than “todo” comments, which are all too easy to ignore!).

If you’d like to follow our refactoring series, the first episode is public.

To see how we progress, become a subscriber.

 

Swift Tip: Separating UI and Model Code

Our current Swift Talk series looks at different techniques to refactor a large view controller, using an example from the open source Wikipedia iOS app.

In the first three episodes, we extract pure functions, model code, and networking-related code. You may notice a common theme to all these refactorings: in each episode we try to separate the code which updates the UI from the code that provides the data for updating the UI.

For example, in last week’s episode we start with a method like this:

class PlacesViewController: UIViewController {
    func updateSearchCompletionsFromSearchBarTextForTopArticles() {
        // ...
    }
}

This method is called whenever the text in the search bar changes. It performs these main tasks:

  • Request search completions from Wikipedia for the current search term
  • Transform the results to the type needed to drive the UI
  • Request location-based completions if the first request doesn’t return enough results
  • Transform the second batch of results
  • Update the UI with the completions after each request
  • Control the state of a progress bar at various success and failure points

The first thing we do is to separate this method into two; one method deals with the logic for search completion fetching, and the other only updates the UI:

func updateSearchCompletionsFromSearchBarTextForTopArticles() {
    fetchSearchSuggestions(for: searchBar?.text ?? "") { result, done in
        // Update UI
    }
}

func fetchSearchSuggestions(for searchTerm: String, completion: @escaping ([PlaceSearch], Bool) -> ()) {
    // ...
}

To prevent breaking any other code in the view controller, we keep the existing method signature. However, the original method now only deals with updating the UI in response to search suggestions; all the fetching logic is handled by the second method, which uses a completion handler to asynchronously report its results.

With this separation in place, we can easily move the non-UI method out of the view controller β€” for example into a model or a service class.

You can watch the full refactoring in Swift Talk #105.

We often find this approach very helpful for writing maintainable view controllers. When we refactor, we always try to pull out code from a view controller that fetches, transforms, or prepares data. The view controller should be left with the task of updating the views.

 

Swift Tip: Quick Performance Timing

When we want to quickly test the performance of some code, we paste the following function into our project:

@discardableResult
func measure<A>(name: String = "", _ block: () -> A) -> A {
    let startTime = CACurrentMediaTime()
    let result = block()
    let timeElapsed = CACurrentMediaTime() - startTime
    print("Time: \(name) - \(timeElapsed)")
    return result
}

The above helper measures the execution time of the function we pass in, and prints it out to the console. Because it’s generic over the result, we can use it to measure single expressions:

let result = measure { (0..<1_000_000).reduce(0, +) }

Or we can use it to to measure the time of a method call:

measure {
    computeLayout()
}

For real performance testing, we like to use Instruments (the new os_signpost API looks like a great addition for exactly these kinds of things) or XCTest’s measure, but when all we need is a quick check, the above snippet is really useful.

For your own code, there are many variations you could write: include the line number and name of the calling function, timing asynchronous calls, use more precise logging, and so on.