Tips for Modern iOS Development

February 12, 2017
Swift iOS Objective-C Xcode

Over the last few years iOS development has changed. There are more framework APIs available, more ways to use your app via extensions, more devices, and more languages to use. Along the way you develop practices designed to ease your pain and suffering. Most of these I’ve had to learn the hard way, but following them has made my life easier and apps better.

Add an extension early on, even if you don’t plan on shipping it.

It’s clear the future on iOS is extensions, and it affects how you architect your project. Your model, business logic, and networking code should be accessible from both your extension and your app.

For instance, if you are writing a news app, make sure throughout the dev process you can make calls to get stories from a today extension and make queries to your model.

This will ensure you don’t add dependencies in your model layer that are not available in your extension. It makes your code more reusable and, trust me, at some point you will have to add an extension.

And of course, use a CocoaPod or Cocoa framework for this code.

When adding features, make sure it’s easy to use the feature from various places within the app.

Say you are writing a mail app ;) You’ll obviously have a message list. When developing, start by adding two versions of your MessageListViewController. Make one a table view, the other a collection view. As you build your list, you’ll soon get tired of writing the same code in two places, which will force you to look for ways to extract code out of your view controller and into something more reusable. 

You may move code to a superclass, move code to swift protocol extensions, or use composition patterns, such as a mail list view datasource, that can work with both a UICollectionView or UITableView.

For the table or collection view cells, create your message view in a standard UIView that can be added to either a UICollectionView cell or UITableView cell.

Handle error cases and authentication issues first.

For a mail app, you’ll want to make a network call to get messages and display them in the list. And when you start developing you’ll usually go with the success case first to get your message list up and running. That’s the easy part.

Only later will you work on what happens if there is a network error, or you are offline, or the user is no longer authenticated, or there are no messages to display in the list.

Make sure its easy to simulate the error case. This can be as simple as stubbing your network calls to simply return an error.

Now you you have to work on: How do I show an alert to the user? What do i show in an empty message list? If it’s an auth error, how do I present a view to ask the user to re-authenticate?

Invest the time in adding awesome logging.

Ensure your logging gives you a clear view into what your app is doing. 

Network calls should include at least two statements - a begin statement identifying the api call, and a response statement that includes success / failure and a round trip time duration. Add the ability to make these more verbose by including the curl of the request and the full json response.  

Add a framework to assist such as cocoalumberjack, which makes it easy to log to the console as well as a file. Add a way to mail these logs from the app in debug builds - you should have something like a debug view controller.

If you are writing a framework or pod, use a logging delegate pattern rather than adding a logging framework dependency in your pod. At a minimum it needs one method:

public protocol LoggingDelegate {
    func log(_ log:String)
}

Of course, you’ll want to add various log levels. This allows the framework or pod consumer to decide how to implement logging, perhaps using a framework. Then the consumer can choose to write to disk, disable it for release builds, save to a location on disk that the framework won’t know about.

Use launch arguments for debugging.

The ProcessInfo object can be queried to look for flags passed in via the scheme for your target. This is an easy way to simply check buttons in your scheme to direct the app to use a different log level or switch between a production or testing backend environment.

public func isRunningLaunchArgument(_ argument:String) -> Bool {
    let args = ProcessInfo.processInfo.arguments
    return args.contains(argument)
}

Write as much code as you can in sample projects.

I’ve always found it helpful to begin work on a new feature or view in a separate app, go as far as you can, and then bring the code over to your actual project. This forces you to write more reusable code.

Build for release early and often.

Building is far more complicated these days - each extension is its own app, with entitlements and provisioning profiles, certificates, etc. You can spend a lot of time working on build issues, which is never tasks that add value to your app. You would much rather be fixing bugs and adding features. And when you’re facing deadlines to release, you never want to delay it by days working through build issues.

Localize, add accessibility labels, and send metrics from the beginning.

Product people love metrics, developers hate them. We have to add libraries to the project that add bloat and slow them down, or even crash. And they are boring and repetitive to work on. But it’s a necessary evil, and a real pain to go back and add later. 

Likewise, localization is much easier when done from the beginning.

And take the time to make the app accessible from the start. It’s the right thing to do for users, and also helps for things like UI automation testing.

Write a universal app (iPad, iPhone, event Apple Watch) from the beginning.

Especially if the requirement is iPhone only. Because at some point you _will_ have to create an iPad version. And you will have to rethink your entire view controller architecture later and spend a lot of time changing it.

Write unit tests.

Your model and network code should go in a pod, and you should have a sample project for that pod. CocoaPods gives you this easily when you use pod lib create. 

You can build your database, test that you can add / remove objects. Make it easy to capture server responses to a file so  you can mock your network class to return these responses, which will give you a known environment to work in. For example, in mail we can create an account in our unit tests that walks through the steps of getting a users settings, folders, message list, etc., but using a set of server responses on disk rather than making the calls. This gives you a known number of messages, folders, etc. to write your tests against.

We often use Core Data and each unit tests starts with creating a new in-memory store and then populating it with a set of canned network responses to populate a user’s account, message list, folders, etc.