Our general iOS application’s architecture stands on following patterns: Service layers, MVVM, UI Data Binding, Dependency Injection; and Functional Reactive Programming paradigm.
We can slice a typical consumer facing application into following logical layers:
Assembly layer is a bootstrap point of our application. It contains a Dependency Injection container and declarations of application’s objects and their dependencies. This layer also might contain application’s configuration (urls, 3rd party services keys and so on). For this purpose we use Typhoon library.
Model layer contains domain models classes, validations, mappings. We use Mantle library for mapping our models: it supports serialization/deserialization into
JSON format and
NSManagedObject models. For validation and form representation of our models we use FXForms and FXModelValidation libraries.
Services layer declares services which we use for interacting with external systems in order to send or receive data which is represented in our domain model. So usually we have services for communication with server APIs (per entity), messaging services (like PubNub), storage services (like Amazon S3), etc. Basically services wrap objects provided by SDKs (for example PubNub SDK) or implement their own communication logic. For general networking we use AFNetworking library.
Storage layer’s purpose is to organize local data storage on the device. We use Core Data or Realm for this (both have pros and cons, decision of what to use is based on concrete specs). For Core Data setup we use MDMCoreData library and bunch of classes - storages - (similar to services) which provide access to local storage for every entity. For Realm we just use similar storages to have access to local storage.
Managers layer is a place where our abstractions/wrappers live.
In a manager role could be:
- Credentials Manager with its different implementations (keychain, NSDefaults, ...)
- Current Session Manager which knows how to keep and provide current user session
- Capture Pipeline which provides access to media devices (video recording, audio, taking pictures)
- BLE Manager which provides access to bluetooth services and peripherals
- Geo Location Manager
So, in role of manager could be any object which implements logic of a particular aspect or concern needed for application working.
We try to avoid Singletons, but this layer is a place where they live if they are needed.
Coordinators layer provides objects which depends on objects from other layers (Service, Storage, Model) in order to combine their logic into one sequence of work needed for certain module (feature, screen, user story or user experience). It usually chains asynchronous operations and knows how to react on their success and failure cases. As an example you can imagine a messaging feature and corresponding
MessagingCoordinator object. Handling sending message operation might look like this:
- Validate message (model layer)
- Save message locally (messages storage)
- Upload message attachment (amazon s3 service)
- Update message status and attachments urls and save message locally (messages storage)
- Serialize message to JSON format (model layer)
- Publish message to PubNub (PubNub service)
- Update message status and attributes and save it locally (messages storage)
On each of above steps an error is handled correspondingly.
UI layer consists of following sublayers:
In order to avoid Massive View Controllers we use MVVM pattern and implement logic needed for UI presentation in ViewModels. A ViewModel usually has coordinators and managers as dependencies. ViewModels used by ViewControllers and some kinds of Views (e.g. table view cells). The glue between ViewControllers and ViewModels is Data Binding and Command pattern. In order to make it possible to have that glue we use ReactiveCocoa library.
We also use ReactiveCocoa and its
RACSignal concept as an interface and returning value type of all coordinators, services, storages methods. This allows us to chain operations, run them parallelly or serially, and many other useful things provided by ReactiveCocoa.
We try to implement our UI behavior in declarative way. Data Binding and Auto Layout helps a lot to achieve this goal.
Infrastructure layer contains all the helpers, extensions, utilities needed for application work.
We tried to go with Swift twice, when Swift 1.2 and Swift 2.0 had been released. Because we rely on many 3rd party libraries needed for our architecture implementation, we are constrained to compatibility of those libraries with Swift. We tried to find alternatives and found most of them, but many of them still not ready for production usage. So, until we have a good compatibility of all required libraries with Swift, we stick with Objective C.
You can find more details about our development process in special blog post here.
The first step of our work after initial communication with a client and getting the full specs/scope is estimation process. We use our internal tool Project Tree for projects estimation. You can look at our architectures here.
Using this tool we design domain model and go through the features list and note what’s required for feature implementation.
After this process we can provide time and budget required for implementing a solution.
We use Trello for task management. We like to divide the scope into several sprints and provide a build at the end of each sprint using TestFlight.
You can find more details about why we use Trello here.
We use Slack for daily team communication. We also like to have sprint planning and sprint retrospective calls via Skype.
We use Github as a source control for our repos and as a Code Review tool using Pull Requests.
Author: Alex Petropavlovsky @petropavlovsky.