Written by Michał Tuszyński
iOS Developer
Published June 30, 2015

8 tips on how to build a solid iOS app architecture

With every new iOS release, the whole platform is becoming more and more mature. Here are the best practices for modern iOS apps.

In this article I will review how some of the major APIs, which all iOS developers use during their daily work, changed throughout different versions of the system released in the past years. I will also give tips on how to make the best of them. 

1: Push, don’t poll!

There are many occasions when your application needs to listen to some kind of event or conditions in order to occur on an external server to provide functionality to the user. A common way to do this is to continuously issue requests to the server and check the response. This kind of approach not only uses an excessive amount of bandwidth but also contributes to increased battery usage by keeping the radio and CPU awake. Let’s remember that we’re working in a very constrained environment in terms of processing power, battery and data usage. There are several possible solutions to that issue.

Silent notifications

The first one is one of my favorites, which is not broadly mentioned, and it features iOS 7: silent notifications, which are a subtype of regular, remote notifications. The only difference from regular notifications is that they don’t display any alerts like regular ones do and we can still pass our data in the notification payload and we still get notified within our app when such notification comes.

That way, whenever our app needs to do something when a condition changes on a remote server, then instead of issuing  large amount of requests, we can simply listen to an incoming silent notification. Notifications will still be delivered even when the app isn’t running or is in the background. If our app isn’t running at the moment of delivery, the system will launch it, put it into background and call the appropriate method.

The only exception is when the user kills our application from the multitasking screen. In this case our silent notifications won’t get delivered until the user either launches our application again or the system is rebooted. It makes sense, because the operating system has to respect user’s decisions: if the user decided to force quit an app, then they probably did it for a reason and don’t want it to be launched again in the background.

Web sockets

The second way is to utilise web sockets. They work in a similar way to the push notifications: both need an active tcp socket connected to the remote server, which is used to send data from the server to the client. It’s a good approach if there is a need to support older versions of iOS, which don’t support silent notifications, or when there is a need for more customised implementation of notification (such as bypassing the hard limit).

Personally, I’m in favor of the first approach, since it’s very well optimised for the device and it’s working even when app isn’t running, as opposed to web sockets.

2: Split data source implementation from your view controllers

UITableView is a very powerful class and it’s present in almost every iOS application out there. Many applications make their view controllers which host the table view conform to the UITableViewDataSource protocol and implement the whole data-feeding logic inside the view controller. While this approach is perfectly fine for situations when we need a simple and fast table view in our app, however, it fails when there are multiple places in the app needing the same logic, because it’s tightly coupled with the hosting view controller.

A solution to that issue is similar to how adapters work in Android: create another class which conforms to UITableViewDataSource, implement the necessary logic and assign your table view’s dataSource property to an instance of your data source. That way, view controllers will have less code which will make them easier to maintain, and the data-feeding logic will be easily reused across different components.

3: Don’t abuse singletons

Singleton is a great pattern for creating shared instances of an object, but as it is with everything: it shouldn’t be overused. Singletons have many positive things, like not having to create multiple instances of an object and recreate it’s state every time, but they also have downsides. Shared state makes it hard to write tests for, and they make it easy to hardcode dependencies instead injecting them through appropriate interfaces.

In reality, before creating a singleton, we should ask ourselves the question: “Do I really want only one instance of that class or do I just want a single application instance of it?”. There are ways of making given object accessible by other components accessible throughout the application without preventing instantiation of the class to a single object (the most easy and straightforward way is placing them inside UIApplicationDelegate instance).

It’s also very tempting to hardcode singleton references inside our implementation. For example, consider this code:

//Bad: We've hardcoded a dependency into our class, which makes it
//difficult to inject during testing or customize in any way
public class MyClass {
    let fileManager = NSFileManager.defaultManager()
}

//Good: By utilising Swift optionals, we have a default value for our dependency, but
//it's also possible to inject the fileManager dependency
public class MyClass2 {
    var fileManager: NSFileManager
    
    public init(fileManager: NSFileManager = NSFileManager.defaultManager()) {
        self.fileManager = fileManager
    }
}

We have two classes, one hardcodes its dependencies while the other provides a default value with the possibility of specifying something else. If we could test both of these classes, we’d have an easier job with the second case, since we might easily create a mock object.

4: Use modern Core Data techniques

There were many changes to Core Data framework across new iOS releases. One of the most significant updates was about threading which was introduced in iOS 5.0 and greatly simplified multithreaded operations by introducing parent and child contexts.

Previously, the developer was responsible for creating contexts for each thread, making sure that no other thread touches it and changes were properly processed to the main context. Now, every context created with either NSPrivateQueueConcurrencyType or NSMainQueueConcurrencyType will create its own dispatch queue which we can enqueue operations on using a very easy API and we can simply specify a parent context to which all changes will be propagated to after saving the child context.

That makes it very easy to implement background persistent store writing by creating two contexts instead of one: the first will have NSPrivateQueueConcurrencyType, while the second, NSMainQueueConcurrencyType, with its parent context will be set to the first one. That way, when we need to perform asynchronous core data operations, we can create a child context using the second context as a parent. Any changes from the child context will be passed to the second context which will update its object graph and then pass on to the first to perform the save.

var storeCoordinator: NSPersistentStoreCoordinator //Assume exists

let backgroundContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
backgroundContext.persistentStoreCoordinator = storeCoordinator

let mainContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
mainContext.parentContext = backgroundContext

In the above example we can see a sample configuration of how to configure two contexts for background saving. Now what is left to do, is to implement merging changes from incoming notifications.

@objc func didSaveContext(notification: NSNotification) {
    if let notificationContext = notification.object as? NSManagedObjectContext {
        mergeChangesFromContext(notificationContext, notification: notification)
    }
}

func mergeChanges(fromContext: NSManagedObjectContext, notification: NSNotification) {
        if fromContext.isEqual(backgroundContext) {
            return
        }
        let context = (fromContext.isEqual(self.context)) ? backgroundContext : self.mainContext
        context.performBlock({
            var error: NSError?
            context.mergeChangesFromContextDidSaveNotification(notification)
            context.save(&error)
            if let coreDataError = error {
                println("Failed to save context: \(coreDataError.localizedDescription)")
            }
        })
    }

It’s worth to point out that the child context saving is a relatively expensive operation, since it needs to pass all of it’s changes one layer up. If we’re importing a large amount of objects into our store, it’s good to consider creating a context pointing directly to the persistent store.

There were also notable changes to Core Data in iOS 8, which take multithreading even further with NSAsynchronousFetchRequest, which makes it even easier to implement asynchronous fetches and makes it possible to track progress of these requests. It’s also possible to perform updates on multiple objects at once with the new NSBatchUpdateRequest.

5: Use correct UIKit components for custom controls

Most iOS applications need to implement custom controls or change the appearance of existing ones in order to look good and comply with modern design guidelines and with company’s branding colors.

It’s quite common to see developers reinventing the wheel in their own controls to implement something, which would work out of the box when subclassing a correct class. The most prominent example is with controls requiring simple user interaction. It’s common to see a subclass of UIView with a custom implementation for detecting touch events and a custom interface for notifying other objects about it.

When employing  UIControl instead of UIView directly our control would be easy to use, since it’d share the interface with other Cocoa Touch controls and we wouldn’t have to worry about implementing touch detection logic.

It’s also common to see custom UITableViewCell subclasses which display a single UILabel with text. It could be easily achievable using default cell with style UITableViewCellStyleDefault. 

Before rolling out our own implementation of something, we should look if there is an existing component we could use in the iOS SDK. Let’s also remember, that with UIAppearance protocol introduced in iOS 5, we can configure default UIKit control properties like background color, text color or tint color. The UIAppearance API even allows us to customise the look based on which component the control is contained in.

6: Don’t use third party libraries unless they really solve a problem

While this may not be an iOS development only related tip, but it’s also worth mentioning.

Since the inception of CocoaPods, iOS developers have a much easier job in maintaining dependencies of their applications. The iOS community has published numerous drop-in libraries which can save time.

However, before including a third party library into our application, let’s ask ourselves the question: do I really need this? If the library only solves a trivial problem, such as minor customisations of some UIKit control, it’s better to take a step back and see how long it would take to implement such solution and if it could be done fast enough. This way it’s going to be easier to maintain the application in the future.

Using third party libraries for every little thing in our app seemingly saves time upfront, but it may take longer to customise them later on to meet new requirements of the software we’re developing.

7: Don’t throw all of your view controllers into a single storyboard 

Storyboards are a big step forward when it comes to reducing boilerplate code in iOS applications to set up our views and view controllers.

However, it’s common to see applications with only a single storyboard file, with every single view controller the app is using placed in there.

This approach not only causes performance issues for Xcode to generate a preview of all these view controllers, but also creates an unnecessary mess as well. As our app grows, there is a big chance that the number of view controllers will do too.

UIStoryboard class provides nice interface for loading view controllers from a storyboard file, therefore it’s a good idea to split the view controllers into separate modules, each of them having its own storyboard. It takes just two lines of code to create a view controller out of a storyboard file.

let storyboard = UIStoryboard(name: "MyStoryboard", bundle: NSBundle.mainBundle())
let viewController = storyboard.instantiateInitialViewController()

Inside our view controller, we can have a class factory method to create an instance of it.

public class MyViewController: UIViewController {
    public class func createInstance() -> MyViewController {
        let storyboard = UIStoryboard(name: "MyStoryboard", bundle: NSBundle.mainBundle())
        return storyboard.instantiateInitialViewController() as! MyViewController
    }
}

8: Don’t ignore error handling in the Objective-C code

Last, but not least, let’s focus on error handling.

I’m going to focus on Objective-C in this paragraph, because with introduction of Swift 2 and try-catch error handling, it’s impossible to ignore thrown errors.

In Objective-C, the standard way of dealing with errors is passing an NSError pointer to a method. If the method encounters an error during execution, then it’ll initialise that pointer. It’s also possible to pass nil instead and ignore any reported errors, which makes it impossible to know if our app can continue or attempt to recover from the error somehow, at the very least by letting the user to try again instead of blindly going forward.

Written by Michał Tuszyński
iOS Developer
Published June 30, 2015