Highly maintainable app architecture
Expanded transcript from my talk at NSSpain 2017
Maintainable can mean different things, depends on many factors: project size, goal, 3rd-party service dependence and availability. But perhaps most importantly it depends on the size of the team at hand. Situation is quite different if you have 1 or 5 or 50 developers on the project. Thus it helps to get some bearings where I’m coming from.
Most of my iOS projects are done by me, as one-man team. In last few years I worked on dozen-ish 3rd party client projects. The deal usually has two parts: I develop the app and support it for a while after publishing. However, the client can take over the maintenance at any time, usually when they deem the cost of my retainer too large compared to how much ongoing development there is.
Thus when client forms a team to maintain the app, I need to handover the code. It’s rare to even call it a team since it’s usually just one person, often someone who is coming to iOS/Swift as a developer on other platforms. If you are in the contract / freelancing world, I’m sure this is familiar to you. This talk will be most beneficial to you but the key takeaways here and architecture approach I’m presenting is equally applicable to larger teams, say when you add new employee.
Another important factor is that I rarely have a chance to work with closely integrated front / backend. Quite often the back-end is already “done” and client is unwilling or unable to invest in large (if any) updates. Thus I need an architecture that can adapt to any craziness I will encounter in the backend systems.
Lastly, app I’m making is regularly part of a larger ecosystem of services and front-ends made by multiple companies which may not have the best communication channels between them. So it happens that changes appear in the systems my app is depending on, without me ever being consulted about the impact. It happens, more often than I would like.
Thus this talk in essence is how to navigate these waters without ripping all your hair along the way. When speaking about app architecture, I can’t think of any better way to explain one than to build an app using it.
I’ll use an example of the clothing shop app, loosely based on the work done for one of my favorite local clients in Serbia.
(Everything mentioned here, including the demo app, is MIT licensed and available on my GitHub profile).
UI·UX + Data
So what’s an app made of, that it needs architecture? From the most big-picture aspect, there are two parts:
- UI & UX = how the app will fulfill its purpose from the point of the Customer
- Data = how the app will do what it needs to do from the point of the Owner (Provider)
UI without the actual live data is Dribbble.com. Beautiful, inspiring, lifts your spirit and brightens your day. Every interaction is amazing and life is awesome.
And then you di…err, experience the joy of enterprise-company production data.
Production data is Excel (and I mean this in the nicest way). Often unstructured and denormalized, available to you in formats invented and (possibly) abandoned before most of you got out from high school. (It was a crazy world out there before the JSON took over.)
(In the example app, I have created few JSON files as base data set loaded from remote server, deliberately structured differently than the nice clean model used in the app.)
Thus when spec-ing out your app architecture, ignore the production data and tailor-build data model for the given UI and workflows.
I start with a good clean data model which:
- best supports what the app needs to do
- fits nicely into the UIKit (and iOS SDK in general)
Base your app on the data types your platform gives you and use them in the best possible way. I dug my own hole too many times before learning not to care in what format the data is coming from.
It’s much easier to transform data from one structure to another than to cram badly architected data into UI components.
Thus I hide all the complexity of original data I’m given. The task of transforming data is certainly not easy nor particularly light on the CPU. I need to compartmentalize this task into a narrow, self-sufficient module, independent from anything else in the app.
Data Manager
I start with Data Manager.
This is central module, the heart of the architecture. It has knowledge about high-level data types in the app: Product, Season, Color etc. It knows how to convert into them and from them.
If you are using data persistence (Core Data, Realm etc), DataManager
is the module which imports into the local storage. Also prepares reading from the local storage, if preparation is needed.
It does not care nor knows where the source data is coming from. It makes no assumption is it local .plist file, API web service which returns JSON or whatever. That’s some other object’s business and DataManager
will call that object to receive the data it will work with.
Raw data usually comes from web service APIs for which you need to write wrappers.
API wrapper(s)
Wrapper is very thin native client around web service API. It speaks with the server and knows the intricate details of that communication.
final class IvkoService {
static let shared = IvkoService()
private init() {
urlSessionConfiguration = {
let c = URLSessionConfiguration.default
...
return c
}()
queue = {
let oq = OperationQueue()
oq.qualityOfService = .userInitiated
return oq
}()
}
fileprivate var urlSessionConfiguration: URLSessionConfiguration
fileprivate var queue: OperationQueue
}
If API needs download / upload of files, it works with local files and returns a path, success info. If it’s JSON-based API, API client will convert from Data
to JSON
. If it’s some String, it converts the binary blob it received to String
with proper encoding.
If the service requires OAuth, it transparently handles that entire process. If the service is based on web session cookies, API wrapper handles cookie storage and management.
Data Manager knows nor cares about any of that.
How to model APIs?
Take for example the Spotify web API. As you look closely, all these endpoints:
- have a common prefix
- have one identifying path component
- (may) have one or more parameters
Swift has a perfect tool to model these API endpoints: enum with associated values. From the demo app:
enum Path {
case promotions
case seasons
case products
case details(styleCode: String)
}
API wrapper is entirely unaware of app’s data model. It knows nothing about Product
, Color
, Season
etc. Data Manager must convert from those types into String
, Int
, Double
, Bool
or any collection of those.
A series of enum methods are then used to transform that data into fully-qualified URLRequest
instance, tailored for particular API service.
enum Path {
...
private var method: Method {
return .GET
}
private var headers: [String: String] {
var h: [String: String] = [:]
switch self {
default:
h["Accept"] = "application/json"
}
return h
}
You can switch per self
inside these methods and do custom processing for each case, as needed.
This one creates the URL part:
private var fullURL: URL {
var url = IvkoService.shared.baseURL
switch self {
case .promotions:
url.appendPathComponent("slides.json")
case .seasons:
url.appendPathComponent("seasons.json")
case .products:
url.appendPathComponent("products.json")
case .details:
url.appendPathComponent("details")
}
return url
}
This collects all parameters into a dictionary:
private var params: [String: String] {
var p : [String: String] = [:]
switch self {
case .details(let styleCode):
p["style"] = styleCode
default:
break
}
return p
}
Which is then encoded as needed, either into query string or as JSON if it goes in the HTTPBody. Or anything custom you may need, this is where you do it.
private var encodedParams: String {
switch self {
case .details:
return queryEncoded(params: params)
default:
return ""
}
}
private func queryEncoded(params: [String: String]) -> String {
if params.count == 0 { return "" }
var arr = [String]()
for (key, value) in params {
let s = "\(key)=\(value)"
arr.append(s)
}
return arr.joined(separator: "&")
}
private func jsonEncoded(params: JSON) -> Data? {
return try? JSONSerialization.data(withJSONObject: params)
}
Finally this method uses all the previous ones to build the final URLRequest
value:
fileprivate var urlRequest: URLRequest {
guard var components = URLComponents(url: fullURL, resolvingAgainstBaseURL: false) else { fatalError("Invalid URL") }
switch method {
case .GET:
components.query = queryEncoded(params: params)
default:
break
}
guard let url = components.url else { fatalError("Invalid URL") }
var r = URLRequest(url: url)
r.httpMethod = method.rawValue
r.allHTTPHeaderFields = headers
switch method {
case .POST:
r.httpBody = jsonEncoded(params: params)
break
default:
break
}
return r
}
API wrapper then initiates a network call to fetch data and when it receives the response, it converts that response into a base type form that’s suitable for further processing: String, JSON, properly typed numbers (Decimal, Int etc)…which it then feeds back into the DataManager
.
typealias ServiceCallback = ( JSON?, IvkoServiceError? ) -> Void
func call(path: Path, callback: @escaping ServiceCallback) {
let urlRequest = path.urlRequest
// OAuth handling
// JSON conversion
// upload / download
execute(urlRequest, path: path, callback: callback)
}
API wrapper is initiating the network call but it does not handle the details of the network call.
Network mini library
Networking is pretty straight-forward business for which iOS already has perfect library: URLSession & friends. To perform network request, you need URLRequest
. As result, you get your data + accompanying response metadata (HTTP status code, headers) or an error.
I’ve made a very thin wrapper around URLSession with sole goal to collect all relevant instances - original request, response headers, data, error etc - into one object: NetworkPayload
struct.
struct NetworkPayload {
let originalRequest: URLRequest
var urlRequest: URLRequest
init(urlRequest: URLRequest) {
self.originalRequest = urlRequest
self.urlRequest = urlRequest
}
/// Any error that URLSession may populate
/// (timeouts, no connection etc)
var error: NetworkError?
/// Received HTTP response.
/// Process status code and headers
var response: HTTPURLResponse?
/// Received stream of bytes
var data: Data?
/// Moment when the payload was prepared.
/// May not be the same as `tsStart`
let tsCreated = Date()
/// Moment when network task is started
/// (`task.resume()`)
var tsStart: Date?
/// Moment when network task has ended.
/// Used together with `tsStart` makes for simple speed metering
var tsEnd: Date?
}
This micro-library handles networking only stuff, like HTTP authentication challenges, SSL pinning etc. The bulk of the code is in NetworkOperation
which is a subclass of my own AsyncOperation which is itself a subclass of Operation
. This allows you to use OperationQueue
on the API wrapper, play around with Quality of Service, to auto-adjust the number of concurrent network ops depending on the speed of the network. All this while the rest of your app has no idea about this.
This Network library has no shared state. Each NetworkOperation
creates its own instance of URLSession
, takes the given URLRequest
and when done calls back. Each request is independent from any other.
This Network library makes no assumptions about the data, it does not process the received Data
blob at all. Whatever it receives, it passes it to the caller. API wrapper knows what it requested and knows how to handle what’s returned.
…
If you combine these two layers - API wrapper + Network layer - you get Alamofire / AFNetworking. I used to swear by AFNetworking until I started encountering less-than-ideal APIs which mostly return JSON until the moment they hit server-side error, in which case I get raw java.sql.exception
inside application/json
MIME type. At which point AFNetworking will wrap this into ResponseSerializationError, completely hiding the original error received from URLSession. Thus I have to do this:
failure:^(NSURLSessionDataTask *task, NSError *error) {
strongify(self);
// parse and extract the actual error response...
if ([error.domain isEqualToString:AFURLResponseSerializationErrorDomain]) {
// ...from the multitude of AFNetworking envelopes
if (error.userInfo) {
NSHTTPURLResponse *response = error.userInfo[AFNetworkingOp
NSData *data = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey];
if ([task.response.MIMEType isEqualToString:@"application/json"]) {
NSError *jsonError = nil;
NSDictionary *apiError = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError];
if (apiError && !jsonError) {
// now we have the actual error response from server
// example:
// {"type":"NoSessionException","message":"Session expired"}
// re-wrap into local error codes
NSString *apiErrorCode = apiError[@"type"];
NSString *apiErrorMessage = [apiError[@"message"] stringByReplacingOccurrencesOfString:@"java.sql.SQLException: " withString:@""];
DDLogError(@"API error response (%ld, %@): \n%@", (long)response.statusCode, task.response.MIMEType, apiError);
NSError *reportError = [ErrorManager apiErrorForType:apiErrorCode message:apiErrorMessage];
This is terrible and the source of the issue is obviously on the server-side, where it should be handled and fixed. The key word: should. That fix may be coming in next release scheduled for November or maybe early next year. Thus the code like the one above is needed due to zero flexibility with the library that expects valid JSON or else…
Separation of the API wrapper and actual networking gives me a chance to cleanly handle this nonsense and return properly formatted error to the API. I document my local error and send it to client / back-end team to implement it on the server. If the back-end ever gets fixed and improved, the only change on my end would be to delete the code handling these exceptions.
…
Important thing to remember is that DataManager
will have one method for each API endpoint. Even if you have dozen or so API endpoints that look to return the same data structure, do not combine them into one method on Data Manager side. At some point over time it’s virtually guaranteed that at least few of those API endpoints will be changed to return something slightly different and then you start doing switch/case
per API endpoint in both request and response parts of the DataManager
method. I’ve been burned enough times that I know this is not the time to be DRY. You can extract the processing parts that are the same - like conversions to/from JSON, validation of the returned response…but never actual methods.
One API endpoint == one Data Manager method and the future you will thank you.
Business logic
On the other side of the Data Manager is client-side business logic. What do I mean by this..?
Say you have a login method which results in Customer object. Where do you keep it? Job of the DM is to fetch and deliver that object but not to keep a reference to that one specific instance.
To complicate matters a bit more - I encountered APIs where I need to make multiple backend request before Customer object is properly formed. So my previously simple DataManager
now becomes aware of transactions and how objects it creates are related to one another… Nah! Data transformations it does already are more than enough for it.
Middleware is the term usually reserved for the part of the architecture between the front-end and database. In iOS apps, I create a series of thin, strictly-focused managers.
I create an AccountManager
which is thin wrapper on top of the DM and it keeps a reference to that specific instance and gives it business meaning. I need 3 API calls to get customer details, his balances etc? AccountManager
will call 3 DataManager
methods and combine the results into one object.
This frees you to independently optimize how the data processing in DataManager
is done, without the need to care about the meaning of actual objects.
In most larger apps, I would have half a dozen or more small managers that are all part of this business logic layer.
To re-iterate: DataManager
creates instances of Product, Category, Season etc. But it makes no assumptions what they are.
Every single one of those objects is some thing with a name.
🙂 🙂 🙂 🙂 🙂 🙂 🙂
↓ ↓ ↓ ↓ ↓ ↓ ↓
👮♀️ 👨🏼🌾 👩🏾🎤 👩🏽💻 👩🏿💻 👨🏻🎨 👩🏽🚀
Business meaning is assigned by higher-level managers.
AccountManager
will ask DataManager
to provide it with an Account object for given user/pass. AccountManager
is the one that knows that particular instance is the currently logged-in customer. DataManager
couldn’t care less, it’s simply yet another Account
instance.
Similarly, CartManager
will ask DataManager
for an update (price, stock availability) for the particular Product
. CartManager
is the one who knows in what intervals should updates be performed. DataManager
simply creates/refreshes the instance and gives the new one back.
CatalogManager
contain data displayed in the shop browsing views. It knows when they should be refreshed, can optionally cache them if needed etc.
…
This separation into layers is amazingly flexible.
- Middleware
- Data Wrangling (Management)
- Data Sourcing
- Networking (if needed)
Each of them has strictly defined role and as such they are very suitable to automated testing, easy to debug and mock etc.
I can have multiple vastly-different API wrappers which all in the end need to execute URLRequest
. DataManager
may need to pick up some data like ads from 3 or 10 different platforms. They usually all have the same set of data and thus can be modeled with the same model instance.
This granularity and thin layering greatly simplifies maintenance. Big part of this is that it simplifies automated testing as you can test CartManager
with any static combination of data you can think of, completely avoiding the process of actually sourcing the objects. Mocking is very easy.
Similarly, you can test DataManager
with static files and API wrappers with zero care about networking. API wrapper should work 100% the same if data comes using URLSession
or through local JSON file in the bundle. NetworkPayload
is struct – hard-code any possible combination you need to test without actually making the network call.
No matter what your raw data looks like, your job is really straight-forward:
- write API wrapper part first
- write
DataManager
method and processing code - write middleware Manager method to call its corresponding DataManager
Once you write one or two data flows, you have learned all there is to learn.
…
This architecture approach is nothing new, mostly common sense. But I feel that the craft of building layered architecture got a bit lost in the (used to be) simple world of mobile apps. In this beautiful new startups world with teams working in unison to create both front-end and back-end for their app/service. Thus you get things like GraphQL, Realm Mobile Platform where most of this stuff is done for you under the hood.
But there’s so much work opportunity in the “older industry” world, where people work in systems created a decade or more ago. Where JSON import/export may not even exist. A layered approach like this can help you a great deal to step into that world and keep yourself and your clients happy in the support & maintenance phase.
…
Now, on the UI side, things are equally interesting.
UI
I hope by now it’s obvious that UI will only communicate with middleware part. I aim to avoid direct communication between the UI and the Data Manager.
UI should never, ever communicate directly with Network or API wrapper. Never!
Here’s an example from the demo app, the front view of the shop app:
To display the horizontal scroller of products, I need the CatalogManager().promotedProducts
array and feed it into UICollectionView
.
But how will this actually work? Should PromoContainerCell
here call CatalogManager
to get the list of promoted products? Will HomeController
do that and pass it to PromoContainerCell
?
This view is also the earliest chance my customer can buy something; that green Buy Now button which sits on top of the cell is obviously related to a particular product. So this Cell instance knows about the button and it knows about the Product instance.
To complete the purchase, I need some simple way to write something like:
let product = promotedProducts[indexPath.item]
cartAddProduct(product)
...
cartBuyProductNow(product)
and job done. Simple, readable, easy to explain. I’m not asking for anything unusual - UIKit already works this way. If you want to push some UIVC instance into current UINC, you simply do show(vc, sender: self)
from any darn-deep level in your UIVC hierarchy and it magically slides from the side.
Side-note: if you are still writing
self.navigationController.pushViewController(vc)
- please stop (unless you still need to support iOS 7 or earlier).
But how will this actually work? Will PromoCell
directly access CartManager
to add the chosen product to the cart?
Solution: Singletons
One way to solve this is to make all those managers Singletons and then call them directly. This is quick and easy solution, requires very little code to implement.
let product = promotedProducts[indexPath.item]
CartManager.shared.add(product)
There are quite a few issues with hard-coded connection likes that one, as proved – proved!, I tell ya – by innumerable flame wars in the programming community since…well, forever. After 20 years of development, I mostly just 🙄 and look for practical benefits and applications. Singletons are omni present in the Cocoa SDKs and are very useful when used pragmatically.
But…
When used carelessly like in the example above – like some omni-present deity objects that accept your prayers wherever and whenever – they have pretty significant practical issues which regularly appear in more complex apps. Example: what if your action - like add to cart - needs to update some other data or view up in the UI chain as well as CartManager
? Say a cart-counter widget in the HomeController
.
Oh easy - I can solve this with Notifications, right..? CartManager
will post DidUpdateCartNotification
, HomeController
will observe it and update its cart widget.
Oook…but what if the cart state is actually implemented on the server? Now you need to wait for network request, thus it makes sense to show some loading spinner and update the cart item counter when it finishes. Oh easy, just add another notification - StartCartUpdate
and EndCartUpdate
…
🙅🏻♂️ Using Singletons like this is no-go.
Each new notification adds more and more indirect pathways to worry about. Really quickly this goes into unmaintainable mess.
Solution: Delegate pattern
Let’s then try a different solution: Delegate pattern. PromoCell
will have a delegate method promoCellDidTapBuyNow
and someone will implement it and become a delegate to handle that.
But who?
PromoCell
is only known to its direct parent, PromoContainerCell
. Which has no idea (yet) that CartManager
exists. So we go up to HomeController
which might know - it’s a Controller, after all, right? So we add another Delegate implementation: PromoContainerCell().delegate = HomeController()
.
Poor PromoContainerCell
– it acts like both delegate and delegator for an action that it cares nothing about. Its only job is to receive [Product]
and feed it to UICollectionView
. Not to care what happens inside PromoCell
- for all intents and purposes it should not even know that there’s a button in it.
Maintenance is becoming a real headache now. If you try to move promoted products scroller to some other place in the UI, you need to make sure to replicate such delegate chain as well. 😣
…
While I’m thinking this through, I have barely touched on the 🐘 in the room: why would any of these views / controllers even know that CartManager
exists?
🤔 🤷🏻♂️
Strictly speaking, their job is to get the data they receive and display it. Their job is to collect the touch/action and pass it along to someone else but that does not mean they need to know who that someone is.
- Get data, display it.
- Receive action, pass it along.
That’s what UIVC/UIView are supposed to do.
There are multiple problems with just about any approach here and UIKit - in its current form - is not really helping. But it’s the thing we have and the thing we need to work with.
Let’s go few steps back…
What I want is a replica of what I have in the app’s backend: a set of focused components with clearly defined inputs and outputs. Translated into UIKit world, what I want boils down to this:
UIViewController
should not know nor care about the hierarchy it’s presented in (is it inside the nav, split, popover, whatever)- It must be able to receive data it needs or ask for it, without knowing who exactly is delivering the data
- It should be able to send info about events that happened in it — button taps, row/cell taps, swipes, any kind of interaction — to its semantic parent
This means that each UIVC should be configured using Dependency Injection and apart from those local data sources it should not need anything else to do its job.
In Swift, DI = (stored) properties. Either on UIViews or UIVCs, what you define as Data source stored properties is all that is needed.
If the Controller is able to feed its View(s) using only that Model then you have properly implemented MVC.
- Embrace MVC. It’s great pattern, easy to explain, easy to use.
- Use MVC properly. Use it for what it was designed. Use it for one UI entity only, to coordinate between its local data model and the view.
- Think thrice before reaching out to anything else.
final class HomeController: UIViewController {
// UI Outlets
@IBOutlet fileprivate weak var collectionView: UICollectionView!
@IBOutlet fileprivate weak var notificationContainer: UIView!
// Local data model
var season: Season? {
didSet {
if !self.isViewLoaded { return }
updateData()
}
}
var promotedProducts: [Product] = [] {
didSet {
if !self.isViewLoaded { return }
collectionView.reloadData()
}
}
var categories: [Category] = [] {
didSet {
if !self.isViewLoaded { return }
collectionView.reloadData()
}
}
}
If the UIVC in question is actually your custom container controller (like HomeController
is), the same rules should apply. With the help of property observers, you can keep your embedded controllers happy and pass the data along.
Note: pay attention to isViewLoaded
, it’s crucial to check this inside property observers to avoid premature view loading. All the work you do in property observers should be repeated in the viewDidLoad
and these two moments of setup should never happen in sequence.
…
Still…the original problem remains: how to pass the data into top-level UIVC in the first place. Where will the connection between the UI components and middleware happen?
How then will cartBuyNow(product)
find its way up to CartManager / PaymentManager / something?
If the UIVCs should not know about all these managers, who then will keep the references to them?
Well, that’s obvious; it must be the only thing left in our UI stack that’s not UIViewController:
AppDelegate!
…
😖 No.
Coordinators
Two years ago at NSSpain, Soroush Khanlou proposed Coordinators as possible solution for the problem of data flow between app’s model and UIs.
Coordinators completely solve the data flow in the UI layer. They don’t hold nor contain any sort of app’s data - their primary concern is to shuffle data from middleware into front-end UI.
What they do:
- Create instances of the VCs
- Show or hide VCs
- Configure VCs (set DI properties)
plus:
- Receive data requests from VC
- Route requests to middleware
- Route results back to VC
These are key things required in order to release UIVCs from the burden of knowing their place in the UI hierarchy and where they are used.
Coordinators wonderfully solve a plethora of issues in modern iOS apps:
- they naturally implement universal links / remove notification pathways through the app
- easily handle ForceTouch quick actions or Handoff continuation
- seamlessly handle communication with extensions etc.
Let’s see how to use them.
How to use Coordinators
Since Coordinator’s job is to handle data flow then it’s their job to know about the parent-child relationships in the app which means they deal with UI hierarchy. Each Coordinator can be parent and/or child of any other Coordinator.
In our shopping cart app, we would first need a high-level object that represents root Coordinator, more often called Application Coordinator. This object will create and hold instances of all the middleware Managers, like API wrapper, DataManager, CartManager etc. It also contains logic which decides what will be set as window.rootController
.
Said plainly — it does what people usually do in AppDelegate. This leaves AppDelegate alone to handle its already big pile of app’s sentry duties that UIKit gives it.
You then model big picture parts: AccountCoordinator
, CartCoordinator
, CatalogCoordinator
etc. Then you may have more focused stuff like PaymentCoordinator
which deals just with payments. You may also have NotificationCoordinator
that handles in-app presentation of the received push notifications. Etc.
The only object that’s always in the memory is ApplicationCoordinator
and is kept with strong property on the AppDelegate
. All the others are allocated and removed as needed. Each of those others can be a child of any other Coordinator, there is no fixed structure. So you can have any of these hierarchies currently active:
App → Catalog → Payment
App → Account → Payment
App → Catalog → Cart → Payment
App → Payment
AppCoordinator will have a bunch of (file
)private
properties to hold middleware instances. Those instances can be Singletons or not, as you wish. Frankly speaking, for bunch of them it makes sense to be Singletons and that is ooook.
Each Coordinator then employs Dependency Injection to receive a special struct called AppDependency
. This is simply a collection of references to middleware Manager instances. Each and every Coordinator will always get a set of references to all managers, passed-down automatically from AppCoordinator. Why this?
struct AppDependency {
var apiManager: IvkoService?
var dataManager: DataManager?
var assetManager: AssetManager?
var accountManager: AccountManager?
var cartManager: CartManager?
var catalogManager: CatalogManager?
var keychainProvider: Keychain?
var persistanceProvider: RTCoreDataStack?
var moc: NSManagedObjectContext?
init(apiManager: IvkoService? = nil,
persistanceProvider: RTCoreDataStack? = nil,
dataManager: DataManager? = nil,
moc: NSManagedObjectContext? = nil,
assetManager: AssetManager? = nil,
accountManager: AccountManager? = nil,
cartManager: CartManager? = nil,
catalogManager: CatalogManager? = nil,
keychainProvider: Keychain? = nil)
{
self.accountManager = accountManager
self.assetManager = assetManager
self.cartManager = cartManager
self.catalogManager = catalogManager
self.apiManager = apiManager
self.keychainProvider = keychainProvider
self.persistanceProvider = persistanceProvider
self.dataManager = dataManager
self.moc = moc
}
}
Some managers are “free” to instantiate since they are independent. API wrappers fall into this category. But others may not be. As I said previously - if you are using Core Data, Data Manager is the one that will handle Core Data objects, import, updates etc. Thus DataManager
needs a reference to the Core Data Stack at the moment it’s created; that stack is certainly not free to create. You need to wait few 100s of milliseconds for it and it’s not really wise to wait even that much before displaying your UI. It makes the app feel slow. Since entire middleware part is then dependent on the Data Manager, all of them must wait as well. You are in danger of approaching that loose cut-off point after which iOS runtime may decide that your app has stalled and it may kill it.
To avoid this, what I usually do is something like this:
override func start(with completion: @escaping () -> Void = {}) {
// singletons, independent of other objects
settings = Settings.shared
apiManager = Empires.shared
gamesManager = GamesManager.shared
adsManager = AdsManager.shared
keychainProvider = Keychain(service: Bundle.identifier)
dependencies = AppDependency(apiManager: apiManager,
gamesManager: gamesManager,
adsManager: adsManager,
settings: settings,
keychainProvider: keychainProvider)
// mark the coordinator started
super.start(with: completion)
coreDataStack = RTCoreDataStack() {
[unowned self] in
self.configureMiddleware()
self.dependencies = AppDependency(apiManager: self.apiManager,
gamesManager: gamesManager,
adsManager: adsManager,
settings: self.settings,
keychainProvider: self.keychainProvider,
persistanceProvider: self.coreDataStack,
dataManager: self.dataManager,
moc: self.coreDataStack.viewContext,
accountManager: self.accountManager,
cartManager: self.cartManager,
paymentManager: self.paymentManager)
}
...
}
func configureMiddleware() {
// create instance of DataManager
// and all the middleware ones
}
I fire up entire UI hierarchy while the Core Data Stack is warming up. My UI will display its LoadingState
with some loading indicators or maybe an intro tutorial if you have one in the app. Once the stack is ready, it will call back at which point I create instances of the DataManager
and its middleware friends and create updated AppDependency
struct which is then passed through the active chain. With Swift’s generics, that’s easy to write in one place and call from property observer on each Coordinator implementation.
protocol NeedsDependency: class {
var dependencies: AppDependency? { get set }
}
extension NeedsDependency where Self: Coordinating {
func updateChildCoordinatorDependencies() {
self.childCoordinators.forEach { (_, coordinator) in
if let c = coordinator as? NeedsDependency {
c.dependencies = dependencies
}
}
}
}
Since Coordinators will load and initiate display of View Controllers, they need some place to keep a reference to them. Hence each Coordinator has rootViewController
property which can be set to instance of any subclass of UIViewController
. Active Coordinator will always set itself as parentCoordinator
for its root view controller. (I’ll explain why in a minute.)
Usually that’s UINavigationController
instance and Coordinator will simply do rootViewController.show(vc, sender: self)
when it needs to show particular content controller.
…
Phew. This solves the input (DI) from Coordinator into UIVC.
Now, about output - how touch events / actions propagate from UIVC to Coordinators so they can route them to the Managers?
Why not Delegates?
In his talk, Soroush recommends Delegate pattern; not just for Coordinators but also for VCs where VC.delegate
will be set to their containing (parent) Coordinator. This works acceptably if our UIVCs are flat but in case they are containers…well, I already explained the issues with that.
Here’s again the Home view from the demo app:
That green button which triggers Buy Now for the given item? Here’s where it is located:
AppCoordinator
↖︎ CatalogCoordinator
↖︎ HomeController (inside UINavigationController)
↖︎ PromoContainerCell
↖︎ PromoCell
(This is about twice simpler case than some complicated UIs I had “pleasure” to implement in last several years.)
Can you count how many Delegates I would need just for this? It’s a maintenance headache in the long run. No go.
Inspiration
Looking for solution of any architectural issue, it’s good practice to first look at your framework of choice. Never fight the SDK; be friendly to it.
Lo and behold, UIKit already has very similar functionality; I mentioned it some minutes ago:
class UIViewController {
func show(_ viewController: UIViewController, sender: Any?)
}
Smells like a good candidate: one method without any required boilerplate in my code; that you call on any UIViewController and with default implementation that simply calls parent.show(…)
. UINC (and few others) override this method and implement their own specific behavior — for UINC that means doing push. Perfect message bubble-up through any given hierarchy with a single line of code you write.
Some observations:
show(vc...)
is based on UIViewController.parent
so it works just with UIVCs- I need it to work with Coordinator too
- I also need it to work with UIView, since original sender can be a simple UIButton in the UI*Cell or similar
Looking more closely at this I realized that this behavior is based on class inheritance and overriding a particular method (show
) and setting a property (parent
). And this lead me to the solution:
What’s the common ancestor for both
UIView
andUIViewController
?
UIResponder
Thus:
- make
class Coordinator: UIResponder {…}
- find a replacement for
parent
that spans bothUIView
andUIViewController
and - make sure it works with Coordinator too
What follows is an explanation of the Coordinator micro-library
First one is straight-forward.
Second one was easy once I read the docs on how UIResponder
actually works, meaning how it knows what’s the next responder in the chain:
class UIResponder : NSObject {
open var next: UIResponder? { get }
}
The parent entity will set itself as next
on any direct child entities it owns / creates.
Well, thank you UIKit — I can piggy-back on UIResponder.next
without actually interfering with it.
public extension UIResponder {
public var coordinatingResponder: UIResponder? {
return next
}
}
With coordinatingResponder, I can implement my own custom messaging system that will bubble up through the entire hierarchy in one-line method:
extension UIResponder {
dynamic func messageTemplate(args: Whatever, sender: Any?, callback: @escaping () -> Void) {
coordinatingResponder?.messageTemplate(args: args, sender: sender, callback: callback)
}
}
Anywhere I need a custom behavior or specific handler for this method, I simply override messageTemplate
and…done. Any unhandled message will simply bubble up until AppCoordinator (and AppDelegate) and be ignored if not handled anywhere.
Key fact: these methods can be used to request stuff by going up the chain and can receive stuff back through the
callback
’s chain.
Neat.
Inter-connect with Coordinator
Remaining issue to solve: if Coordinators are controlling the UIVCs, then in order for the bubble-up to work, each UIVC instance needs to know what is its (optional) parent Coordinator.
Simple 😇, with little help from wonderfully dynamic iOS runtime:
extension UIViewController {
private struct AssociatedKeys {
static var ParentCoordinator = "ParentCoordinator"
}
public var parentCoordinator: Any? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.ParentCoordinator)
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.ParentCoordinator, newValue, .OBJC_ASSOCIATION_ASSIGN)
}
}
}
(thanks Objective-C!)
I need to thread carefully here…Excerpt from UIResponder.next
documentation:
The UIResponder class does not store or set the next responder automatically, instead returning nil by default. Subclasses must override this method to set the next responder.
UIView implements this method by returning the UIViewController object that manages it (if it has one) or its superview (if it doesn’t);
UIViewController implements the method by returning its view’s superview;
UIWindow returns the application object, and UIApplication returns nil.
So I need to mimic UIViewController.next
and jump to the Coordinator if the UIVC.parentCoordinator
is not nil
. Otherwise, I need to replicate exactly what documentation describes:
override open var coordinatingResponder: UIResponder? {
guard let parentCoordinator = self.parentCoordinator else {
guard let parentController = self.parent else {
return view.superview
}
return parentController as UIResponder
}
return parentCoordinator as? UIResponder
}
That middle self.parent
check here shortens the walk up the responder chain and also enables usage of these message in viewDidLoad
. 🤘🏻
Lastly, on the Coordinator level:
open class Coordinator<T: UIViewController>: UIResponder, Coordinating {
open var parent: Coordinating?
override open var coordinatingResponder: UIResponder? {
return parent as? UIResponder
}
}
Benefits
- You can call the coordinatingResponder method from anywhere in the UI code
- It will travel up across all the views
- Once it gets to any UIViewController, it will continue on through UIVC chain (of fallback to views if there’s no parentController)
- Once it gets into any Coordinator, it will stay in the Coordinator chain
- You can override the chain loop at any point, do something and continue on the chain
This system is not perfect but is pretty straight-forward in all cases. Don’t go crazy and remove all the Delegate pattern implementations in your code. It still has its place. If you have a component that always needs just one parent then is probably nicer to handle that using Delegate pattern. But if there’s any chance that this particular child may need to connect to more than one parent in one call, then coordinatingResponder method is much better solution.
There are situations where you need to be pragmatic and not implement this to the core (say with Core Data driven views). But if my last 3 of 4 projects are any indications, this approach is amazingly versatile and applicable to pretty complex apps, with 10+ Coordinators, 100+ UIViewControllers, multiple API wrappers and services.
The only thing Coordinator truly needs to be perfect are two letters in front of it: UICoordinator
(👋🏻 , sherlock this, pretty please)
…
There’s no fancy name for this architecture. Probably the most suitable name to call it is LAYERS, which is not an acronym for anything.
Any feedback – please open an issue on Coordinator repo or talk to me on Twitter: @radiantav