Daily coverage of Apple’s WWDC 2019 conference, by John Sundell.

A Swift by Sundell spin-off.

Preparing a code base for WWDC

Making preparations for WWDC can seem a bit like preparing for something completely unknown. Apart from rumors, leaks, and reports — no one outside of Apple really knows exactly what will be released during the conference — but that doesn’t mean that we necessarily have to enter next week completely unprepared.

Based on my almost 10 years of experience as an iOS developer (it does look like I’m getting old, doesn’t it?), and more importantly, close to a decade of migrating code bases to new versions of Xcode and other developer tools — here are my top 5 tips on how to prepare a code base for WWDC.

⚠️ Cleaning up old warnings

Cleaning up any existing warnings within an Xcode project might seem like an obvious thing to do — but it’s so incredibly common for small, apparently harmless warnings to slip through the cracks. ”Let’s just fix it for the next release” is an easy thing to say, but come next week some of those warnings (especially deprecation ones) might start turning into either errors or — more dangerously — unpredictable behavior.

The next version of Xcode and the SDKs for Apple’s various platforms are also highly likely to bring new warnings and deprecations, so by ensuring that our project is in a good state warning-wise before next week — we’ll at least make the process of adopting new SDKs and build tools a bit easier.

Another thing to consider is what Swift version our project is using. The current shipping version of the Swift language is 5.0, but the next version of Xcode (presumably Xcode 11), is highly likely to ship with Swift 5.1. While migrating to new Swift language versions have become easier and easier as the Swift team and the open source community has a heavy focus on maintaining backward compatibility, migrating to 5.0 ahead of time will surely make the jump to 5.1 much smoother.

✂️ Reducing strong coupling

When an app’s various parts, types, and features are too strongly coupled, that often leads to a code base becoming much less flexible. If our UI layer makes too many assumptions about which database we’re using, if we’re strongly tying our current networking stack to our core logic, or if we’re assuming that our app will always run under a specific set of circumstances — that could make it much likely that we’ll be able to adopt some of the new features announced next week in a timely manner.

The key here is to improve the separation of concerns within a code base. By introducing clear abstractions between the various parts of a code base, and by clearly defining how those parts in turn interact with the system, we’ll often be able to use those abstractions to quickly adopt new APIs — without having to make any big, sweeping changes across our code base.

Let’s take a look at an example from last year’s WWDC, when the heavily used UIWebView class (used to render web content within a native app) was deprecated in favor of its more modern counterpart — WKWebView. Now let’s imagine that, at the time, we were working on an app that needed to present web views in many different contexts — and that it was doing so by interacting with UIWebView directly, making the transition to WKWebView quite slow and painful.

Instead, what if our app wasn’t strongly coupled to UIWebView as a concrete implementation, but rather relied on an abstraction in order to present web content? After all, most of our code base ideally shouldn’t have to be concerned with exactly how a certain piece of content is rendered, but rather rely on a more abstract API and tell that what to render.

Such an abstraction could be as simple as a small, wrapping UIViewController, which could then be presented or composed in whichever context we’d need to present web content:


class WebViewController: UIViewController {
    private lazy var webView = UIWebView()

    override func loadView() {
        view = webView
    }

    func renderContent(from url: URL) {
        let request = URLRequest(url: url)
        webView.loadRequest(request)
    }
}
                

With the above in place, migrating to WKWebView (or perhaps even SFSafariViewController) would be a lot simpler, since we now have a single entry point into displaying web content from anywhere in our code base, rather than having calls to UIWebView scattered all over the place.

While it can be really difficult to predict exactly which APIs that will be introduced or deprecated during this year’s WWDC — a solid set of abstractions around some of our core functionality can both reduce code duplication, and also make our code base more flexible when it comes to future changes — such as new APIs.

🤖 Testing the core

Few things can make a code base more capable of handling a large number of unknown changes than a solid suite of unit and UI tests. We’re not talking 100% test coverage here, but rather just a bit of a “safety net” around the core features of our app, which can help us ensure that some of our most important functionality will keep working as expected — even as we migrate to new SDKs and system APIs.

While unit tests are great for automatically testing an isolated piece of code, and for verifying how some of that code interacts with system APIs — something that can potentially be even more useful when adopting new SDKs and APIs, is UI tests. Since UI tests run our whole application and actually interact with it through simulated taps, swipes and other gestures — they often have a higher chance of detecting regressions caused by changes in our underlying tools.

For example, let’s say that our app places custom controls in our app’s navigation bar, and that iOS 13 will (for the sake of argument) introduce a new navigation bar style. When that happens, chances are quite high that our custom solution might break, and if we had UI tests covering navigation through those controls — then we’d be notified automatically.

Here’s what a simple version of such a test could look like:


class NavigationUITests: XCTestCase {
    func testNavigatingToProfile() {
        let app = XCUIApplication()
        app.launch()

        let navigationBar = app.navigationBars["Home"]
        let button = navigationBar.buttons["Profile"]
        button.tap()

        // Here we verify that the app has navigated to the
        // correct screen by asserting that "Home" is no longer
        // visible in the navigation bar, while "Profile" is.
        XCTAssertTrue(app.navigationBars["Profile"].exists)
        XCTAssertFalse(app.navigationBars["Home"].exists)
    }
}
                

Again, we’re probably not able to cover everything by writing tests like the above, but doing so at least lets us eliminate many different paths from our manual regression testing — which can both save time, and help us avoid bugs when something is missed.

🧐 Doing a dependency check

When doing any kind of quality improvement work within a code base, it’s important not to forget about its third party dependencies. At the end of the day, any dependencies that an app uses is merged into that app’s binary one way or another — making those pieces of code just as much a part of our code base as the code we’ve written ourselves.

As someone who has spent years maintaining a decent number of open source projects, I know first-hand just how frustrating it can be for developers when they attempt to migrate their app to a new version of Xcode, only to find that a framework they rely on now breaks the build. And while I’m sure many open source maintainers do their very best to ensure compatibility with the new toolchains as soon as possible, wouldn’t it be better if we could take matters into our own hands, rather than having to wait?

My advice is to spend a few hours this week, before WWDC, doing a quick “dependency check” — by simply browsing the code bases of our dependencies, learning more about their internal architecture, and trying to gain enough knowledge about how they work in order to be able to self-service on any issues that we might encounter next week. It’s also a great incentive to learn more about some of the building blocks we’ve used to build our app, which isn’t only useful when migrating to new SDKs — it can also help us solve a number of different issues in the future as well.

🤝 Modernizing system integrations

If it’s one thing that many people in the Apple developer community (myself included) have learned over the years, is that it almost always pays off to listen closely to what the various Apple presenters say during their sessions — and to try to pick up on any clues and patterns that might give us a hint about where the system and the devices it runs on might be going in the future.

Take Auto Layout and size classes as an example. Before Apple started introducing iPhones with larger screen sizes, they spent multiple sessions re-iterating just how important it was to build scalable UIs using the more modern, and more flexible, layout tools that they had been introducing. The same can also be said for APIs like NSUserActivity, which when introduced might’ve seemed like an API that was easy to ignore, but now acts as the foundation for multiple core features and technologies — including Spotlight search, Siri shortcuts, ClassKit, Handoff, and more.

So the question is; does our app integrate with the system in the most modern, up-to-date way, or are we still relying on old APIs and conventions? For example, do our view controllers use the modern APIs for status bar appearance, have we adopted safe area layout guides everywhere, have we modularized our code base in order to adopt system extensions, and have we migrated all of our assets to use Asset Catalogs?

The answer to some of the above questions, and others, might very well determine just how prepared our code base is for bigger system changes — like the much-rumored iOS Dark Mode.

Conclusion

While it’s probably impossible to make a code base fully prepared for something that’s still mostly unknown, it doesn’t hurt to try to ensure that our code base is in the very best shape it can be — before we start migrating to new SDKs and system APIs during the coming weeks and months.

Having a “clean slate” when it comes to deprecations and other warnings, ensuring that we’re building our app according to the most modern conventions, getting to know our dependencies a bit better, and improving the structure of our code base through decoupling and increased testing — are all things that can help our code base move forward, not only next week, but the weeks after that as well.

What do you think? What are some of the ways that you like to prepare your code base for WWDC? Let me know on Twitter @johnsundell.

Thanks for reading! 🚀