Skip to content
memo.d.foundation / about / playbooks / ios-best-practices-libraries-architecture-assetsConnect

Playbooks/Product engineering

Best practices in iOS development, part 2: libraries, architecture, stores and assets

Which libraries earn a place in an iOS app, how to structure models, views and controllers, where to keep data, and how to handle images.

Use with AI

This part covers the shape of the app itself: the few libraries worth adding, an architecture split into models, views and controllers, the stores that hold app data, and how to handle bitmap and vector images. It is a snapshot from 2015, written before SwiftUI and Swift concurrency. Part 1 explains what has changed since.

Part 2 of 4 in the iOS best practices series. Previous: Best practices in iOS development, part 1: getting started | Next: Best practices in iOS development, part 3: coding style, security, diagnostics and analytics

1Common libraries

Generally speaking, make it a conscious decision to add an external dependency to your project. Sure, this one neat library solves your problem now, but maybe later gets stuck in maintenance limbo, with the next OS version that breaks everything being just around the corner. Another scenario is that a feature only achievable with external libraries suddenly becomes part of the official APIs. In a well-designed codebase, switching out the implementation is a small effort that pays off quickly. Always consider solving the problem using Apple's extensive (and mostly excellent) frameworks first!

Therefore this section has been deliberately kept rather short. The libraries featured here tend to reduce boilerplate code (e.g. Auto Layout) or solve complex problems that require extensive testing, such as date calculations. As you become more proficient with iOS, be sure to dive into the source here and there, and acquaint yourself with their underlying Apple frameworks. You'll find that those alone can do a lot of the heavy lifting.

1.1AFNetworking/Alamofire

The majority of iOS developers use one of these network libraries. While NSURLSession is surprisingly powerful by itself, AFNetworking and Alamofire remain unbeaten when it comes to actually managing queues of requests, which is pretty much a requirement of any modern app. We recommend AFNetworking for Objective-C projects and Alamofire for Swift projects. While the two frameworks have subtle differences, they share the same ideology and are published by the same foundation.

1.2DateTools

As a general rule, don't write your date calculations yourself. Luckily, in DateTools you get an MIT-licensed, thoroughly tested library that covers pretty much all your calendar needs.

1.3Auto layout libraries

If you prefer to write your views in code, chances are you've heard of either Apple's awkward syntaxes – the regular NSLayoutConstraint factory or the so-called Visual Format Language. The former is extremely verbose and the latter based on strings, which effectively prevents compile-time checking. Fortunately, they've addressed the issue in iOS 9, allowing a more concise specification of constraints.

If you're stuck with an earlier iOS version, Masonry/SnapKit remedies the problem by introducing its own DSL to make, update and replace constraints. PureLayout solves the same problem using Cocoa API style. For Swift, there is also Cartography, which builds on the language's powerful operator overloading features. For the more conservative, FLKAutoLayout offers a clean, but rather non-magical wrapper around the native APIs.

2Architecture

  • Model-View-Controller-Store (MVCS)

    • This is the default Apple architecture (MVC), extended by a Store layer that vends Model instances and handles the networking, caching etc.
    • Every Store exposes to the view controllers either signals or void methods with custom completion blocks.
  • Model-View-ViewModel (MVVM)

    • Motivated by "massive view controllers": MVVM considers UIViewController subclasses part of the View and keeps them slim by maintaining all state in the ViewModel.
    • To learn more about it, check out Bob Spryn's fantastic introduction.
  • View-Interactor-Presenter-Entity-Routing (VIPER)

    • Rather exotic architecture that might be worth looking into in larger projects, where even MVVM feels too cluttered and testability is a major concern.

2.1“Event” Patterns

These are the idiomatic ways for components to notify others about things:

  • Delegation: (one-to-one) Apple uses this a lot (some would say, too much). Use when you want to communicate stuff back e.g. from a modal view.
  • Callback blocks: (one-to-one) Allow for a more loose coupling, while keeping related code sections close to each other. Also scales better than delegation when there are many senders.
  • Notification center: (one-to-many) Possibly the most common way for objects to emit “events” to multiple observers. Very loose coupling , notifications can even be observed globally without reference to the dispatching object.
  • Key-Value observing (KVO): (one-to-many) Does not require the observed object to explicitly “emit events” as long as it is Key-Value Coding (KVC) compliant for the observed keys (properties). Usually not recommended due to its implicit nature and the cumbersome standard library API.
  • Signals: (one-to-many) The centerpiece of [ReactiveCocoa][reactivecocoa-github], they allow chaining and combining to your heart's content, thereby offering a way out of "callback hell".

2.2Models

Keep your models immutable, and use them to translate the remote API's semantics and types to your app. For Objective-C projects, Github's Mantle is a good choice. In Swift, you can use structs instead of classes to ensure immutability, and use Swift's Codable to do the JSON-to-model mapping. There are also few third party libraries available. SwiftyJSON and Argo are the popular among them.

2.3Views

With today's wealth of screen sizes in the Apple ecosystem and the advent of split-screen multitasking on iPad, the boundaries between devices and form factors become increasingly blurred. Much like today's websites are expected to adapt to different browser window sizes, your app should handle changes in available screen real estate in a graceful way. This can happen e.g. if the user rotates the device or swipes in a secondary iPad app next to your own.

Instead of manipulating view frames directly, you should use [size classes][size-classes] and Auto Layout to declare constraints on your views. The system will then calculate the appropriate frames based on these rules, and re-evaluate them when the environment changes.

Apple's recommended approach for setting up your layout constraints is to create and activate them once during initialization. If you need to change your constraints dynamically, hold references to them and then deactivate/activate them as required. The main use case for UIView's updateConstraints (or its UIViewController counterpart, updateViewConstraints) is when you want the system to perform batch updates for better performance. However, this comes at the cost of having to call setNeedsUpdateConstraints elsewhere in your code, increasing its complexity.

If you override updateConstraints in a custom view, you should explicitly state that your view requires a constraint-based layout:

Swift:

override class var requiresConstraintBasedLayout: Bool {
    return true
}

Objective-C:

+ (BOOL)requiresConstraintBasedLayout {
    return YES
}

Otherwise, you may encounter strange bugs when the system doesn't call updateConstraints() as you would expect it to. This blog post by Edward Huynh offers a more detailed explanation.

2.4Controllers

Use dependency injection, i.e. pass any required objects in as parameters, instead of keeping all state around in singletons. The latter is okay only if the state really is global.

Swift:

let fooViewController = FooViewController(withViewModel: fooViewModel)

Objective-C:

FooViewController *fooViewController = [[FooViewController alloc] initWithViewModel:fooViewModel];

Try to avoid bloating your view controllers with logic that can safely reside in other places. Soroush Khanlou has a good writeup of how to achieve this, and architectures like MVVM treat view controllers as views, thereby greatly reducing their complexity.

3Stores

At the "ground level" of a mobile app is usually some kind of model storage, that keeps its data in places such as on disk, in a local database, or on a remote server. This layer is also useful to abstract away any activities related to the vending of model objects, such as caching.

Whether it means kicking off a backend request or deserializing a large file from disk, fetching data is often asynchronous in nature. Your store's API should reflect this by offering some kind of deferral mechanism, as synchronously returning the data would cause the rest of your app to stall.

If you're using [ReactiveCocoa][reactivecocoa-github], SignalProducer is a natural choice for the return type. For instance, fetching gigs for a given artist would yield the following signature:

Swift + ReactiveSwift:

func fetchGigs(for artist: Artist) -> SignalProducer<[Gig], Error> {
    // ...
}

ObjectiveC + ReactiveObjC:

- (RACSignal<NSArray<Gig *> *> *)fetchGigsForArtist:(Artist *)artist {
    // ...
}

Here, the returned SignalProducer is merely a "recipe" for getting a list of gigs. Only when started by the subscriber, e.g. a view model, will it perform the actual work of fetching the gigs. Unsubscribing before the data has arrived would then cancel the network request.

If you don't want to use signals, futures or similar mechanisms to represent your future data, you can also use a regular callback block. Keep in mind that chaining or nesting such blocks, e.g. in the case where one network request depends on the outcome of another, can quickly become very unwieldy – a condition generally known as "callback hell".

4Assets

Asset catalogs are the best way to manage all your project's visual assets. They can hold both universal and device-specific (iPhone 4-inch, iPhone Retina, iPad, etc.) assets and will automatically serve the correct ones for a given name. Teaching your designer(s) how to add and commit things there (Xcode has its own built-in Git client) can save a lot of time that would otherwise be spent copying stuff from emails or other channels to the codebase. It also allows them to instantly try out their changes and iterate if needed.

4.1Using bitmap images

Asset catalogs expose only the names of image sets, abstracting away the actual file names within the set. This nicely prevents asset name conflicts, as files such as button_large@2x.png are now namespaced inside their image sets. Appending the modifiers -568h, @2x, ~iphone and ~ipad are not required per se, but having them in the file name when dragging the file to an image set will automatically place them in the right "slot", thereby preventing assignment mistakes that can be hard to hunt down.

4.2Using vector images

You can include the original vector graphics (PDFs) produced by designers into the asset catalogs, and have Xcode automatically generate the bitmaps from that. This reduces the complexity of your project (the number of files to manage.)

4.3Image optimisation

Xcode automatically tries to optimise resources living in asset catalogs (yet another reason to use them). Developers can choose from lossless and lossy compression algorithms. App icons are an exception: Apps with large or unoptimised app icons are known to be rejected by Apple. For app icons and more advanced optimisation of PNG files we recommend using pngcrush or ImageOptim, its GUI counterpart.