Nice and smart people

To say that I was full of emotions as I was leaving my first full-time job would be an understatement. I had been there for almost four years (and even interned prior to graduating). It was there where I learned how to write production software, as opposed to how to pass computer science exams. To work as part of a team. To earn the trust of my colleagues and embrace the opportunity afforded by meritocracy. I knew that I was ready for a new challenge, but really wasn’t sure what to expect. Speaking with my soon-to-be-former boss, I lamented that everyone who we currently worked with was so smart, and more importantly, so nice. That they had truly become some of my best friends, not simply coworkers.

He laughed, and in no uncertain terms, told me that having nice and smart coworkers would be a terrible reason for me to stay.

He helped me realize that I should consider working with nice and smart people to be a requirement, not merely a perk. That he didn’t think I’d accept a new job unless I believed that my new coworkers would be equally as likely to become great friends, even if this belief was subconscious. That the world is full of smart and nice people, and that it’d be foolish simply to cling to those who I already knew.

Of course, he was right. In my subsequent years at my next job, I forged a number of meaningful new friendships that I remain incredibly thankful for today. But crucially, this didn’t make it harder for me to leave there when I knew that the time had come.

It made it far easier.

View controller lifecycle behaviors

My pal Soroush recently floated the idea of implementing reusable behaviors that pertain to a view controller’s lifecycle – such as analytics logging – using additional view controllers. There’s a lot to like about this idea; since it’s easy to compose view controllers, you don’t need to worry about adding custom storage to each of your view controller subclasses or manually overriding and hooking into each their lifecycle methods.

His post ended up being a bit of a divisive one. While view controller containment seems universally liked, using a view controller to model logic that doesn’t have its own associated view can feel wrong. While I empathize with this sentiment, I find the concept of “lifecycle behaviors” so enticing that I’m willing to forgive their implementation perhaps feeling a bit unorthodox.

First, let’s define what exactly a lifecycle behavior would look like. Any of UIViewController’s standard lifecycle methods could theoretically be worth hooking into, which yields a protocol that looks something like the following:

protocol ViewControllerLifecycleBehavior {
    func afterLoading(viewController: UIViewController)

    func beforeAppearing(viewController: UIViewController)

    func afterAppearing(viewController: UIViewController)

    func beforeDisappearing(viewController: UIViewController)

    func afterDisappearing(viewController: UIViewController)

    func beforeLayingOutSubviews(viewController: UIViewController)

    func afterLayingOutSubviews(viewController: UIViewController)
}

In order for a behavior to optionally implement whichever lifecycle methods are pertinent, we can provide empty implementations by default:

extension ViewControllerLifecycleBehavior {
    func afterLoading(viewController: UIViewController) {}

    func beforeAppearing(viewController: UIViewController) {}

    func afterAppearing(viewController: UIViewController) {}

    func beforeDisappearing(viewController: UIViewController) {}

    func afterDisappearing(viewController: UIViewController) {}

    func beforeLayingOutSubviews(viewController: UIViewController) {}

    func afterLayingOutSubviews(viewController: UIViewController) {}
}

Let’s say that our app uses UINavigationController and that the navigation bar is visible by default, but the occasional view controller needs to hide it. Modeling this behavior in a concrete ViewControllerLifecyleBehavior is trivial:

struct HideNavigationBarBehavior: ViewControllerLifecycleBehavior {
    func beforeAppearing(viewController: UIViewController) {
        viewController.navigationController?.setNavigationBarHidden(true, animated: true)
    }

    func beforeDisappearing(viewController: UIViewController) {
        viewController.navigationController?.setNavigationBarHidden(false, animated: true)
    }
}

Now, how do we actually integrate these behaviors into our view controller’s lifecycle? Since view controllers forward their lifecycle methods to their children, creating a child view controller is the easiest way to hook in:

extension UIViewController {
    /*
     Add behaviors to be hooked into this view controller’s lifecycle.

     This method requires the view controller’s view to be loaded, so it’s best to call
     in `viewDidLoad` to avoid it being loaded prematurely.

     - parameter behaviors: Behaviors to be added.
     */
    func addBehaviors(behaviors: [ViewControllerLifecycleBehavior]) {
        let behaviorViewController = LifecycleBehaviorViewController(behaviors: behaviors)

        addChildViewController(behaviorViewController)
        view.addSubview(behaviorViewController.view)
        behaviorViewController.didMoveToParentViewController(self)
    }

    private final class LifecycleBehaviorViewController: UIViewController {
        private let behaviors: [ViewControllerLifecycleBehavior]

        // MARK: - Initialization

        init(behaviors: [ViewControllerLifecycleBehavior]) {
            self.behaviors = behaviors

            super.init(nibName: nil, bundle: nil)
        }

        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        // MARK: - UIViewController

        override func viewDidLoad() {
            super.viewDidLoad()

            view.hidden = true

            applyBehaviors { behavior, viewController in
                behavior.afterLoading(viewController)
            }
        }

        override func viewWillAppear(animated: Bool) {
            super.viewWillAppear(animated)

            applyBehaviors { behavior, viewController in
                behavior.beforeAppearing(viewController)
            }
        }

        override func viewDidAppear(animated: Bool) {
            super.viewDidAppear(animated)

            applyBehaviors { behavior, viewController in
                behavior.afterAppearing(viewController)
            }
        }

        override func viewWillDisappear(animated: Bool) {
            super.viewWillDisappear(animated)

            applyBehaviors { behavior, viewController in
                behavior.beforeDisappearing(viewController)
            }
        }

        override func viewDidDisappear(animated: Bool) {
            super.viewDidDisappear(animated)

            applyBehaviors { behavior, viewController in
                behavior.afterDisappearing(viewController)
            }
        }

        override func viewWillLayoutSubviews() {
            super.viewWillLayoutSubviews()

            applyBehaviors { behavior, viewController in
                behavior.beforeLayingOutSubviews(viewController)
            }
        }

        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()

            applyBehaviors { behavior, viewController in
                behavior.afterLayingOutSubviews(viewController)
            }
        }

        // MARK: - Private

        private func applyBehaviors(@noescape body: (behavior: ViewControllerLifecycleBehavior, viewController: UIViewController) -> Void) {
            guard let parentViewController = parentViewController else { return }

            for behavior in behaviors {
                body(behavior: behavior, viewController: parentViewController)
            }
        }
    }
}

Lastly, adding this behavior to one of our custom view controllers is as simple as:

override func viewDidLoad() {
    super.viewDidLoad()

    addBehaviors([HideNavigationBarBehavior()])
}

And that’s all there is to it. Sure, the implementation might not be using view controllers in exactly the way that UIKit intends, but when has that stopped us before? Perhaps future enhancements to the language and SDK will allow our addBehaviors(behaviors: [ViewControllerLifecycleBehavior]) method to be more idiomatically implemented (increased dynamism could better facilitate aspect-oriented programming). But we won’t need to change all of our individual UIViewController subclasses if and when this happens.

Profiling your Swift compilation times

UPDATE: It’s possible to do this entirely from the command line, without modifying your Xcode project at all. Simply run the following (thanks to Mike Skiba and Eric Slosser for their help with this):

xcodebuild -workspace App.xcworkspace -scheme App clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | grep .[0-9]ms | grep -v ^0.[0-9]ms | sort -nr > culprits.txt

I had a problem. The new iOS application that I’m working on – written 100% in Swift – was noticeably taking much longer to compile than should, given its size (~200 files). More concerning, it was suddenly a lot slower than only a couple of weeks prior. I needed to get to the root of the problem as soon as possible, before it got any worse.

The first step was to add -Xfrontend -debug-time-function-bodies to my Swift compiler flags:

This causes the compiler to print out how long it takes to compile each function (thanks to Kevin Ballard for clueing me into these). The debug logs are visible in Xcode’s report navigator, but only by manually expanding each individual file:

The next step was to aggregate all of these logs together in one place in order to make sense out of them.

Rather than building via Xcode itself, using the xcodebuild command line tool results in the logs being printed to standard output, where we can massage them to our liking:

# Clean and build, capturing only lines containing `X.Yms` where X > 0, sorting from largest to smallest
xcodebuild -workspace App.xcworkspace -scheme App clean build | grep [1-9].[0-9]ms | sort -nr > culprits.txt

At this point, the question was whether I’d actually be able to derive actionable insights from the output, and I most certainly was. Thought my culprits file highlighted any function that took over a millisecond to compile, I actually had 1,200+ cases in which a function took more than a second, with many taking over three seconds. Thankfully, these 1,200+ lines were actually all the same three functions repeated many times over again (I don’t know enough about compilers to understand why this is the case, but the inclusion of “closure” in the output sample below does shed a bit of light).

3158.2ms	/Users/Bryan/Projects/App/FileA.swift:23:14	@objc get {}
3157.8ms	/Users/Bryan/Projects/App/FileA.swift:23:52	(closure)
3142.1ms	/Users/Bryan/Projects/App/FileA.swift:23:14	@objc get {}
3141.6ms	/Users/Bryan/Projects/App/FileA.swift:23:52	(closure)
3139.2ms	/Users/Bryan/Projects/App/FileA.swift:23:14	@objc get {}
3138.7ms	/Users/Bryan/Projects/App/FileA.swift:23:52	(closure)
3128.3ms	/Users/Bryan/Projects/App/FileB.swift:27:22	final get {}
3109.9ms	/Users/Bryan/Projects/App/FileA.swift:23:52	(closure)
3052.7ms	/Users/Bryan/Projects/App/FileA.swift:23:14	@objc get {}
3052.6ms	/Users/Bryan/Projects/App/FileA.swift:23:14	@objc get {}
3052.2ms	/Users/Bryan/Projects/App/FileA.swift:23:52	(closure)
3052.1ms	/Users/Bryan/Projects/App/FileA.swift:23:52	(closure)
3049.0ms	/Users/Bryan/Projects/App/FileB.swift:27:22	final get {}
3026.1ms	/Users/Bryan/Projects/App/FileB.swift:27:22	final get {}

Even crazier, each of these three functions was only a single line of code. Rewriting just these three lines of code caused my entire project to build 60% faster. I could see this enraging many, but honestly I was just so happy to have figured out the sources of the bottleneck, as well as to now know how to troubleshoot the next time I found myself in a similar situation.

You might be wondering what in the world these three lines looked like. All were (perhaps unsurprisingly) very similar, taking a form like:

return [CustomType()] + array.map(CustomType.init) + [CustomType()]

I can’t speak to whether or not the crux of the problem was the appending, the mapping, or the combination of the two. Rather than try and hone in on the smallest change I could make that would yield suitable performance, I simply rewrote these functions to be as naively imperative as I could: mutable intermediate variables, ostensibly unnecessary type definitions, the works. I’m hardly the first to discover that array appending is slow, but it took experiencing the pain firsthand to realize the ease with which one can find themselves having a bad time thanks to an elegant and seemingly innocuous line of code.

Swift is still an incredibly young language and it shouldn’t be surprising that there are still rough edges like this for us to, as a community, collectively find and help sand down. As we learn more about how to instrument our code, and in turn, what the compiler’s pain points are, we’ll become better at helping one another avoid such pitfalls, and helping those working on Swift itself prevent them from happening in the first place.