Wikimedia Apps/Team/iOS/BestPractices

From mediawiki.org
< Wikimedia Apps‎ | Team‎ | iOS

iOS Best Practices[edit]

This guide outlines the best practices I've collected working on various teams/projects.

Introduction[edit]

TBD

Style[edit]

Our Style Guide contains information on formatting code and other concrete coding techniques. Because Objective-C is a dynamic language and is intimately intertwined with Cocoa and its conventions, the line between style and best practices tends to be a bit blurry.

General Principles[edit]

Below are some general principles to guide you when architecting your code.

Always use the highest abstraction possible[edit]

When choosing a technology for implementing behavior, always choose the highest level abstraction that meets your needs. This keeps you from introducing unneeded complexity too soon in your application (YAGNI / Pre-optimization).

Some examples: AVPlayer vc RemoteIO Autolayout vs Frame calculation code NSNotifications vs KVO NSOperations vc GCD

GCD[edit]

Although GCD is a lower level abstraction, the API is rather straightforward and quick to implement. This makes GCD / dispatch blocks extremely good for quickly trampolining to and from main thread or as a replacement for "performSelector:afterDelay". However, if your code begins to require state or order of operation is important, you should move to something more structured like NSOperations.

Immutability[edit]

When possible favor immutability in your objects. In practical terms, this means pass all needed information in during initialization. This reduces the complexity by reducing the number of possible states of your object. This makes the objects implementations more straightforward to write since you do not have to worry about existence of properties and indeterminate states.

Dependency Injection[edit]

With immutability comes dependency injection. In it's most basic form, this simply means passing all required parameters on initialization. This has a few effects - one is adding immutability, the other is declaring dependencies in a formal way that can be enforced at build time instead of at run time.

Idempotence[edit]

Unless explicitly stated in the class or method documentation, methods should be idempotent. TODO: provide examples

Order of Operation[edit]

Any time methods/operations need to be called in a certain order, the order with which they should be called should either be:

  1. Abstracted away by the public API, but clearly documented in the implementation
  2. Enforced via assertions in the implementation
  3. Made explicit by the API (e.g. using a builder-like pattern to force calls to be chained in a certain order)

Subclass only if needed[edit]

Subclassing is useful for organizing code, but can quickly become rigid and hard to deal within Objective-C codebases. Apple encourages composition over subclassing, and expresses this through it's APIs. (Composition over inheritance)

Before subclassing, consider the following:

  1. Can I use a category instead?
  2. Can I separate this into another object and use the delegate pattern to coordinate behavior?

Views[edit]

See Apple's guide: Alternatives to Subclassing.

View Controllers[edit]

Specifically, you should generally avoid creating an base View Controller class that is used throughout your app. Adding behavior in this way can make difficult to make visual changes to views later on. Additionally, view controllers from 3rd party libraries will be unable to inherit your custom behavior, and so you may end up duplicating code anyways.

Note: This is not proposing that you never create an abstract view controller class, instead keep such view controllers scoped to a specific set of related view controllers.

Use Categories to share UI code.[edit]

Building on the category principal, a great way to share code between your view controllers is to encapsulate flows and behaviors in category methods that can be accessed from all view controllers.

By adding a category to hold your logic, view controllers can now "opt in" to the behavior you defined

What should be public[edit]

Only expose properties and methods needed by other classes.

Minimal[edit]

You should always strive for publicly exposing the fewest number of properties and methods. This reduces the number entry points into the class which reduces the complexity of how the class can be used.

Expose all initialization parameters[edit]

Any parameters passed in during initialization should be exposed as a public, and if possible, a readonly property. This allows users of your class to differentiate between instances using the arguments passed in when creating them.

Structure and complex behavior[edit]

NSOperations are good abstractions for asynchronous, dependent tasks. (Command Pattern)

These types of classes provide structure to your business rules and workflows and encapsulate that behavior into a concrete object while providing a standard API for progress, cancelation, and errors.

Type Checking[edit]

Type checking / Protocol conformance should always be performed on an object that has an unknown type before calling other methods and properties. This is true even if you "know" what it is as a developer. If the compiler doesn't know then the check should be performed.

Ids[edit]

Always check the type of any object that is returned as an id from property or method

Generics[edit]

Always check the type when iterating data in a non-generic collection class. We should add generic annotations to collection classes whenever possible to improve type checking.

Network Data[edit]

Network data should always be converted to a proper model object. Don't let dictionaries leave the model layer. Always handle type checking and validation when converting JSON data into model objects so higher level objects do not need to worry about types.

Javascript and Web Views[edit]

Javascript tends to send NSNulls back from JS APIs. Be extra defensive here. Check the types of all containers and sub-objects before making any calls.

Global Access[edit]

In general global access should be avoided when possible. Sometimes it appropriate - one example is a user session which may be needed by many classes in an app. It can be overkill to pass this as a initializer parameter in every class, so this is a good candidate for global access.

Singletons[edit]

The most common way to provide global access. Singletons should be used sparingly. Refrain from having any view controller or view singletons - most of the time you can scope the behavior to specific class or a category (add a UIViewController category). Many times a singleton sees like a good idea, but eventually you may need "2" of that object - and many assumptions your code made will be wrong. Singletons also make unit testing much harder.

App Delegate[edit]

It is common for people to use the App Delegate as a singleton - which in turn leads to the app delegate containing and externalizing objects that are not its responsibility. Many times people do this to get a reference to the entry point of the view(controller) hierarchy - try creating a purpose built object for this instead.

NSObject Categories[edit]

Adding categories to NSObject usually is a bad idea - since almost everything is an NSObject. Try to better scope your code.

Concrete Models vs Generic Collections[edit]

As a general rule, use NSObject subclasses (i.e. "model" objects) as entity objects instead of instances of NSDictionary. Using NSDictionary to model entities/objects has several disadvantages that make it impractical:

  1. No interface to declare expected behaviors/fields
  2. No code completion for ease of use
  3. No type checking
  4. No implementation for validating fields, behaviors, etc.

Logging[edit]

The app uses CocoaLumberjack for logging. Logs are written to files which are attached to any crash logs to assist in debugging. In order to do this, all logs should be done using the "DDLog" methods like DDLogWarn and DDLogDebug instead of NSLog. Only use NSLogs for local debugging and don't commit them to the repository.

Code Organization[edit]

Where you write code is as important as how you write code. Logical code placement makes you code easier to understand for other developers and your future self.

View Layout [edit]

Layout code should be contained in Nibs, Storyboards, or custom view classes (or in the case of collection views, layout classes) - not in a view controller. If you need to perform layout in code, it is usually a good idea is to write your first pass of layout logic in a view controller, and later abstract it out into a view subclass. Leaving layout code in the view controller couples your layout to a specific implementation and makes the view harder to reuse and breaks encapsulation (some view logic is in the view, other logic is in the view controller).

View Configuration[edit]

Package up configuration of a view (think colors, fonts, etc…) within the view or a view category. It is a good idea to group styling of views into specific "+Style" categories that can be shared within the app.

View Content[edit]

Setting the content of a view (like the text of an NSString), should be handled in view controllers.

Initial View Controller Loading[edit]

It is a good practice to create an "RootViewController" that can handle the loading of initial screens and as a starting point for navigation.

Navigation Flow[edit]

Navigation flow should be handled within individual view controllers. Shared navigation flows should be placed within UIViewController categories.

For better control of complex flows and where you need deterministic control of the flow (think custom URL protocol handling), use a protocol based system to query individual view controllers about what navigation types they can perform.

Avoid creating a "god" object that understands the entire navigation flow - this creates an extremely rigid structure and will force you to define all possible navigation states. In a sense, this anti-pattern encourages you to recreate UIKit navigation logic.

Event Handling[edit]

Handle events in view controllers. Do not make business decisions within view subclasses. Updating UI state (selection state, background color, etc…) based on an event is ok to handle in view classes, but should usually kick off a delegate call back so the view controller can make other model changes as well.

Business Logic[edit]

Business logic, domain knowledge should be placed where it can be shared among view controllers - use Model Controllers, Service Objects, and Operations to hold this logic.

Decisions based on model state should placed at the lowest level possible. The view controller is the last choice for this logic because it becomes harder to test.

Do not put business logic within model objects. Model objects should only hold state, use one of the objects above to perform operations on models.

Model state[edit]

Do not hold model state in view controller instance variables. Model state should be contained in model objects or on instance variables of model controllers and services.

Object Responsibilities[edit]

In general you should follow Apple guidelines and design your apps using MVC principals. This section is similar to the previous Code Organization section and looks at many of the same principles from the object perspective.

Models[edit]

Make your models "dumb". Do not place methods on the models that control behavior. Models should reflect state.

They should not contain methods that control behavior. These methods should be contained in controller and service objects.

Methods on model objects should largely be confined to lazy and stateless data transformation, like returning a date as a string, or a sorted array of its children.

View Controllers[edit]

The primary purpose for view controllers is:

  1. Control the navigation of the application
  2. Provide a bridge between models and views. They pull in data and push it out to the views in the form they need.

Beyond this, view controllers tend to be a dumping ground for all code without a home. Look for ways to remove code by using collaborating objects. Below are some common behaviors that are included in View Controller implementations that can easily be moved into other files.

Datasources[edit]

A great way to reduce the amount of code in view controllers is to remove the boilerplate code for table views and collection views. This is a technique covered extensively on objc.io.

Model Controllers / Services[edit]

Model Controllers should perform model manipulations that are UI independent (like network and processing). Use these to encapsulate business logic, algorithms, and rules. Anything that doesn't require a UI class should be put in these objects so they can be shared in different parts of the code base. This also perpetuates composition over subclassing, i.e. placing behavior in a shared component instead of View Controller abstract superclass.

Views[edit]

Views should encapsulate layout code.

Plan for updates and animations[edit]

Place code that updates your views based on the model in a separate method in your view controller, like:

- (void)updateViewAnimated:(BOOL)animated;

Call this method whenever you need to update the view. The animated flag gives you the opportunity to animate changes as well. This also plays nicely with lifecycle methods:

- (void)viewDidAppear:(BOOL)animated{
  [super viewDidAppear:animated];
  [self updateViewAnimated:animated];
}

Auto Layout[edit]

Auto Layout was designed to allow rapid UI development when working with multiple screen sizes and localized content. It also provides a high level abstraction (constraints) to describe layouts.

Auto Layout should be favored over old style springs and struts autoresizing behavior and frame calculations in code which can be difficult to maintain and understand.

Nibs vs Storyboards vs Code[edit]

In general use the highest level abstraction you can. In order:

Storyboards[edit]

Storyboards are great for static workflows. For complex applications, one storyboard is onerous, if not impossible. In that case you can use multiple storyboards, each confined to a specific workflow. Login/Signup flows are great candidates for this.

If your workflows are more dynamic, then storyboards provide little advantage over a traditional nib (Except: Only Storyboards have access to certain new features like static table views and prototype cells.

Nibs[edit]

Nibs are great for table view cells and collection view cells, as well as views that do not play nicely with storyboards (You need several top level objects to construct your view).

Code[edit]

Writing layout code is a necessity when performing animations. Remember that many times you can do your initial layout in a Nib/Storyboard, and modify the views in code at runtime.

If using Nibs/Storyboards with Auto Layout, you will need to expose your constraints as IBOutlets to modify them in code.

Object Collaboration[edit]

Types of communication[edit]

Depending upon the situation, use an appropriate communication pattern. The standard patterns are below listed in order of increasing complexity.

Message[edit]

Simply passing a message to an object and invoking a method. This is the most basic form of communication in Cocoa/Objective-C.

Target/Action[edit]

A form of message passing bound to specific control states. This is the default communication for UI events from UIControl subclasses and UIGestureRecognizers. This is only used for views communicating to other views or view controllers.

Blocks[edit]

Invoking a block of code before/after a task. Useful for asynchronous communication. A great way to decouple the message sender from the actual code that is executed. Can also send back "responses" via the block return value. Usually defined in line, so intent can be more clear than using delegation.

Delegation[edit]

A more specific form of message sending implemented through the use of protocols and is thus more loosely coupled as the sender does not know the receivers type. Useful for Informing another object before/after a task, or asking "should" this action take place. "Ok" for asynchronous communication, but with a little more overhead than a block. that also allows for "responses" via the method return value.

Notification Center[edit]

Posting a notification before/after task, useful for notifying multiple objects. Although it is more overhead, it is a more formal way to specify specific events,

KVO[edit]

Fine grained notifications at a property level. This is the most verbose and error prone of the techniques, and in general should be avoided if possible. If it must be used, look into a more resilient 3rd party wrapper like MAKVONotificationCenter or ReactiveCocoa.

Communication Strategies[edit]

Based on the types of objects involved, below is a list of appropriate ways to communicate:

Views[edit]

View -> View[edit]
  • Message
  • Delegation
  • Target/Action
View -> View Controller[edit]
  • Blocks
  • Target/Action
  • Delegation
View -> Model Controller[edit]
  • NEVER
View -> Model[edit]
  • NEVER

View Controllers[edit]

View Controller -> View[edit]
  • Message
View Controller -> View Controller[edit]
  • Use the Model Layer to communicate changes
  • Delegation
  • Blocks
View Controller -> Model Controller[edit]
  • Message
View Controller -> Model[edit]
  • Message

Model Controllers[edit]

Model Controller -> View[edit]
  • NEVER
Model Controller -> View Controller[edit]
  • Block
  • Notification
  • KVO
  • Delegation (not preferred for long lived objects that can change delegates)
Model Controller -> Model Controller[edit]
  • Block
  • Notification
  • KVO
  • Delegation (not preferred for long lived objects that can change delegates)
Model Controller -> Model[edit]
  • Message

Models[edit]

Model -> View[edit]

  • NEVER
Model -> View Controller[edit]
  • Notification
  • KVO
Model -> Model Controller[edit]
  • Notification
  • KVO
Model -> Model[edit]
  • Message
  • KVO - maybe, more limey this should be in a Model Controller

Dependencies[edit]

Evaluation[edit]

Dependencies should always be evaluated before bring them into the project. Dependencies generally come in two flavors:

Utility libraries and view components[edit]

These libraries are usually well encapsulated and perform a specific function. We can generally be much less rigorous when evaluating these. As long as the code is understandable and stable, it is generally a win and allows us to avoid writing code that has already been written.

Examples:

  • NSDate-Extensions
  • SVWebViewController
  • TUSafariActivity
  • NYTPhotoViewer

Frameworks or Fundamental Design Pattern Libraries[edit]

These libraries generally change how we write code and can be pervasive across one or more layers of the app. Because of this we should evaluate them much more stringently. As a general principle try to use libraries that embrace Cocoa design patterns.

Examples:

  • BlocksKit
  • PromiseKit
  • SSDataSources
  • OCMock
  • Quick/Nimble

Swift Frameworks[edit]

Because of tooling issues and framework loading bugs in the OS, we should avoid bringing in Swift dependencies when possible. Because this increases the number of frameworks, it can increase the complexity of the project and likelihood of crashing on some OS versions. If Swift code must be added, consider dragging it into source so it does not add another framework.

Git[edit]

Refrain from using "-f" or "--force" when running git commands[edit]

Using force switches causes the git command to ignore things like hooks and the gitignore file. You should ask why you need to do this, and possibly update other files to support the action you need to take. This won't always be the case, but like most best practices, using "-f" should be the exception, not the rule.

See this for a practical example of what can happen:

https://gerrit.wikimedia.org/r/#/c/202905/

Builds and Deployments[edit]

Minimum Supported OS[edit]

If possible, support only 1 OS prior to the current OS, with an eye on dropping support each summer as major OS betas are released.

In practice, different WMF teams set varying thresholds for dropping support of legacy devices (historically, browser UAs).

For example, some teams may draw the line at 1% of UAs, and others may draw the line higher. For iOS 5% seems to have been the rough line for consideration for releasing a "final" sunset build for an older version of iOS; the final decision on when to "drop support" of an older iOS version is made by the product manager.