If you’re intrigued by the word UBER and assuming that this article is about either the company UBER or its mobile app. Unfortunately it’s not :-). So then what is it all about?
I’d like to introduce you to an architecture style mind you not an architecture pattern neither a software design pattern. Which can be applied for building highly scalable, testable mobile applications. I’ve coined the term “UBER” for this style not because its fancy. However for what it does, that is…
UBER is an architecture style. It applies set of architecture , software design patterns in an efficient way. Which provides structural conformance of the overall architecture in a holistic manner. Rather limited to individual view, model and respective controller. This architecture style begins with focus on layers, concerns defining building blocks, ( User Interface, Business,Entities/Model and Exception, REST) control points, inter module communication which collectively sounds UBER :-).
At this point if you are mobile app developer either iOS/Android would be intrigued how is this different from the existing architecture patterns be it MVC, MVVM, MVP etc. What is that sets this apart? In this article we will try to find answer for that very question thats on your mind right now. So hold your horses and continue reading 🙂
Before we look at the UBER architecture style itself. Let’s look at the typical mobile app development approach. Back then during initial years of mobile app development apple introduced the MVC (Model View Controller) pattern. The fallout was it turned out to be as Massive View Controller. It was not because the architecture pattern was faulty rather it wasn’t used appropriately. Eventually we’ve seen the advent of MVVM, MVP, Reactiveto compliment MVC or address the gaps. Quite recently we’ve seen MVI from the android space. However the larger question here despite the fact we’ve new software architecture patterns periodically. Yet we continue to see the rise of mobile apps built in a way not helping the cause for the following which are quintessential for highly scalable mobile apps such as:
- Clear Separation Of Layers
- Centralized Exception Handling
- Testability of Business Services & Overall code coverage
- Scalability – Shipping new features without making an overhaul change to existing architecture
- Abstraction to isolate system dependancies
- Plug and Play
So where is the problem? Before we jump into the solution, let’s first try to understand briefly the difference between an architecture style vs architecture pattern vs software design patterns. For most people who doesn’t understand software architecture these 3 terms are synonymous. However they aren’t and here is why?
Architecture Style
An Architectural Style is a named collection of architectural design decisions that (1) are applicable in a given development context, (2) constrain architectural design decisions that are specific to a particular system within that context, and (3) elicit beneficial qualities in each resulting system.
Architecture style talks about, how to organize our code broadly. It’s the highest level of granularity and it specifies layers, high-level modules of the application and how those modules and layers interact with each other, the relations between them.
The diagram below illustrates few architecture styles.
In layman terms if we’ve to describe architecture style let’s say equating real world object a house to software system or application. The architecture style here would define that a house should have a entry and exit, should be multi-storied, should have n number of rooms etc. As you noticed the architecture style defines just the building blocks or components, however it doesn’t specify whether room 3 or 4 should be constructed in a way that it turns out to be a master bed room or something else. It’s left for the architecture pattern to define and then details of the room like windows, a closet etc are left to the design patterns.
So to sum up what’ve looked so far. I’d to like to share the quote below
“… An architecture style defines a family of systems in terms of a pattern of structural organization; a vocabulary of components and connectors with constraints on how they can be combined….” – David Garlan & Mary Shaw
Additionally if you’d like to hone this topic. I’d strongly recommend you to check out the below youtube videos
Architecture Pattern
A pattern is a recurring solution to a recurring problem. In the case of Architectural Patterns, they solve the problems related to the Architectural Style. For example, “what classes will we have and how will they interact, in order to implement a system with a specific set of layers“, or “what high-level modules will have in our Service-Oriented Architecture and how will they communicate“, or “how many tiers will our Client-server Architecture have“.
Architectural Patterns have an extensive impact on the code base, most often impacting the whole application either horizontally (ie. how to structure the code inside a layer) or vertically (ie. how a request is processed from the outer layers into the inner layers and back). Few examples of Architectural Patterns:
- Three-tier
- Microkernel
- Model-View-Controller
- Model-View-ViewModel
Design Patterns
Design Patterns differ from Architectural Patterns in their scope, they are more localised, they have less impact on the code base, they impact a specific section of the code base, for example:
- How to instantiate an object when we only know what type needs to be instantiated at run time (maybe a Factory Class?);
- How to make an object behave differently according to its state (maybe a state machine, or a Strategy Pattern?).
- How to make sure a library, framework or class is instantiated only once – Maybe a Singleton Pattern
Therefore design patterns talks about the implementation of a module or code base in great detail.
In summary, what we’ve looked so far is as follows:
- An Architectural Style is the application design at the highest level of abstraction;
- An Architectural Pattern is a way to implement an Architectural Style;
- A Design Pattern is a way to solve a localized problem.
Why Architecture Style – UBER?
Traditionally there is a lot of emphasis given from the get go whilst building mobile apps to choose an architecture pattern that is MVC, MVVM, Reactive or MVP etc. So let’s assume you chose MVC as your mobile app architecture pattern. As the mobile app evolves with more features and extensive code base. Let’s try to envision what the mobile architecture would look like? The diagram below illustrates a common scenario for a large number of mobile apps
Diagram 1a – Architecture Pattern MVC
Here as you can see with Model-View-Controller pattern we’ve redundant silos. Which ends up having it’s own exception handler, network module and persistence adapter’s. This may not be true for all the mobile apps. However a large number of mobile apps end up with a similar architecture. Now lets take a look at the UBER architecture style and put all of this together in perspective.
The diagram below illustrates what it would look like with architecture style – UBER.
Diagram 1b – Architecture Style UBER which encompasses architecture pattern MVC
So there seems to be an overhaul change from diagram 1a to diagram 1b. Let’s break this down. If you notice with diagram 1a the architecture pattern focuses on each sub-module, view controller etc. Rather the entire structural organization of the system itself. So what’s wrong here? That approach may be good enough if we’ve a small application which has fewer number of screens and simple functionality. When the application code base start to grow we may end up with too many silos of MVC or MVVM or MVP. At that point we need to have control points to regulate communication within the system, separate concerns and abstraction. In order to provide structural and organizational conformance of the overall system. Which will avoid everyone(module, views, controllers, services) speaking to everyone. This would steer the mobile app architecture to be scalable, maintainable, testable. This is where UBER – architecture style comes in handy which enables a clean, lean design.
How it works?
Architecture style – UBER begins with focusing on defining the building blocks, respective control point mechanism and communication paradigm with different modules inside the overall system. It then applies architecture, design patterns in the implementation of each layer. Here the building blocks constitutes 5 layers. The control point is defined in the shape of one manager or main controller in each layer that has information about the other. Last but not the least what kind of communication is allowed also well defined. This architecture style is also known as layered architecture and is commonly seen on server side applications, infra-structure deployment where clear demarcation of network zones and type of allowed communication is defined.
We’ve talked about the inner working a lot in theory. Now let’s put all of this together in code.
Let’s have a simple app to illustrate this architecture style which does the following
- iOS App with single window
- Consists of 3 views (UIViewController) namely VC1, VC2, VC3. All of them sub-classed from base class with the a field viewId and method reload
- VC1 will have 3 buttons “Navigate to VC2” – Will navigate the user to another view controller called VC2, “Reserve” – Invokes a business service or module called FeatureABC to make some dummy reservation, “Cancel” – Invokes a business service or module called FeatureABC to cancel a dummy reservation .
To begin with the app project structure resembles the diagram below:
UILayer
UIManager.swift – This is the bridge and control point for communication between views and rest of the app. All the communication to views will be channeled through this. It will also be responsible to manage navigation of views, routing view actions to respective business services. Receiving updates for views from business services
import Foundation
import UIKit
class UIManager{
static var sharedInstance = UIManager()
static let flowController:FlowController? = FlowController()
private var navCont:UINavigationController? = nil
static func reload( viewContext:ViewContext? ){
let viewId = viewContext?.id
let view = self.flowController?.getViewById(viewId: viewId!)
view?.reload(model: (viewContext?.model!)!)
}
static func displayOverlayProgress(){
print("Control coming here")
}
static func hideOverlayProgress(){
}
static func register( view: BaseVC ){
self.flowController?.register( view: view)
}
}
FlowController.swift – Its part of the UILayer responsible for managing the navigation flow between views.
import Foundation
import UIKit
class FlowController{
private var nav:UINavigationController? = nil
let appDelegate = UIApplication.shared.delegate as! AppDelegate
var history = Array<String>()
var views:Array<BaseVC> = Array<BaseVC>()
private let actionMapper:UIActionMapper? = UIActionMapper()
/*Maps the action url from view based on url parsing
to navigate from current view to another view. If not
maps the requested action to respective business service
*/
public func navigate( url: String ){
let str:Array = url.components(separatedBy: "myapp://")
let controllerName:String = str.last!
if let targetVC:UIViewController = viewControllerFromString(viewControllerName:controllerName){
targetVC.view.backgroundColor = .white;
targetVC.title = str.last!
if ( appDelegate.window?.rootViewController == nil ){
appDelegate.window = UIWindow(frame: UIScreen.main.bounds)
appDelegate.window?.makeKeyAndVisible()
self.nav = UINavigationController(rootViewController: targetVC)
appDelegate.window?.rootViewController = self.nav
history.append(url)
return;
}
self.nav?.pushViewController(targetVC, animated: true)
history.append(url)
self.actionMapper?.map(actionUrl: url)
}else{
self.actionMapper?.map(actionUrl: url)
print("View controller doesnt exist \(url)")
}
}
/*
When the business service needs to update respective view for
updates it will communicate to the UIManager with the respective view
id. This method enables the UIManager to lookup for that specific view
to propogate the update to that view.
*/
func getViewById(viewId: String) -> BaseVC{
let view = self.views.filter({ (view) -> Bool in
view.viewId == viewId
}).first
return (view == nil) ? BaseVC() : view!
}
//Registers the view controller for updates from business services
func register( view: BaseVC ){
if let viewExists:BaseVC = self.getViewById(viewId: view.viewId ),
viewExists.viewId != "SomeId"{
print( "View does exist \(viewExists)" )
return
}
self.views.append(view)
}
//Loads the view controller found in the custom uri scheme
func viewControllerFromString(viewControllerName: String) -> UIViewController? {
if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String {
print("CFBundleName - \(appName)")
if let viewControllerType = NSClassFromString("\(appName).\(viewControllerName)") as? UIViewController.Type {
return viewControllerType.init()
}
}
return nil
}
}
UIActionMapper.swift – Its part of the UILayer responsible for routing actions from view to business layer.
import Foundation
import UIKit
class UIActionMapper{
private let appDelegate = UIApplication.shared.delegate as! AppDelegate
let businessLayer:BusinessManager = BusinessManager.sharedInstance
//Forwards the url action to business layer to invoke the respective business service
public func map(actionUrl: String){
businessLayer.router?.action(url: actionUrl)
}
}
Views – All the view controllers are grouped into this.
BaseVC.swift
import UIKit
class BaseVC: UIViewController {
var viewId:String = "SomeId"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Divarse of any resources that can be recreated.
}
func onClick(){
}
//This is the method which will be invoked by UIManager//When the business service wants to notify respective for updates
func reload( model: View ){
print("Control coming here \(self.viewId)")
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
VC1.swift
class VC1: BaseVC {
override func viewDidLoad() {
super.viewDidLoad()
self.viewId = "View1"
UIManager.register(view: self)
// Do any additional setup after loading the view, typically from a nib.
let btn:UIButton = UIButton(frame: CGRect(x: 100, y: 200, width: 150, height: 50))
btn.setTitle("Navigate to VC2", for: .normal)
btn.titleLabel?.textColor = UIColor.white
btn.backgroundColor = UIColor.blue
btn.addTarget(self, action: #selector(onNavigate), for: .touchUpInside)
let btn2:UIButton = UIButton(frame: CGRect(x: 100, y: 300, width: 150, height: 50))
btn2.setTitle("Reserve", for: .normal)
btn2.titleLabel?.textColor = UIColor.white
btn2.backgroundColor = UIColor.blue
btn2.addTarget(self, action: #selector(onReserve), for: .touchUpInside)
let btn3:UIButton = UIButton(frame: CGRect(x: 100, y: 375, width: 150, height: 50))
btn3.setTitle("Cancel", for: .normal)
btn3.titleLabel?.textColor = UIColor.white
btn3.backgroundColor = UIColor.blue
btn3.addTarget(self, action: #selector(onCancel), for: .touchUpInside)
self.view.addSubview(btn)
self.view.addSubview(btn2)
self.view.addSubview(btn3)
}
/*
Here the view registers an action to UIManager
requesting navigation is required to another view controller
called VC2
*/
func onNavigate(){
UIManager.flowController?.navigate(url: "myapp://VC2")
}
/*
Informs UIManager through custom uri scheme below
that there is an action from this view "Reserve" which exists
under a module called "FeatureABC" and it relies on a parameter
called reservationId. Then its upto the UIManager to route this request
to respective business service
*/
func onReserve(){
UIManager.flowController?.navigate(url: "myapp://featureABC?cmd=reserve#reservationId=82348@viewId="+self.viewId)
print("You clicked me")
}
/*
Informs UIManager through custom uri scheme below
that there is an action from this view "Cancel" which exists
under a module called "FeatureABC" and it relies on a parameter
called reservationId. Then its upto the UIManager to route this request
to respective business service
*/
func onCancel(){
UIManager.flowController?.navigate(url: "myapp://featureABC?cmd=cancel#reservationId=82348@viewId="+self.viewId)
print("You clicked me")
}
/*
This method is invoked by the UIManager whenever there is an
update available for the view. Here the view can handle the
refreshing of its UI Elements
*/
override func reload(model: View) {
print("\(model.data!)")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
VC2.swift
import UIKit
class VC2: BaseVC {
override func viewDidLoad() {
super.viewDidLoad()
self.viewId = "View2"
let btn:UIButton = UIButton(frame: CGRect(x: 100, y: 200, width: 150, height: 50))
btn.setTitle("VC3", for: .normal)
btn.titleLabel?.textColor = UIColor.white
btn.backgroundColor = UIColor.blue
btn.addTarget(self, action: #selector(onNavigate), for: .touchUpInside)
self.view.addSubview(btn)
let btn2:UIButton = UIButton(frame: CGRect(x: 100, y: 400, width: 150, height: 50))
btn2.setTitle("Get Data", for: .normal)
btn2.titleLabel?.textColor = UIColor.white
btn2.backgroundColor = UIColor.blue
btn2.addTarget(self, action: #selector(onGetData), for: .touchUpInside)
self.view.addSubview(btn2)
}
/*
Informs UIManager through custom uri scheme below
that there is an action from this view "getData" which exists
under a module called "FeatureXYZ". Then its upto the UIManager
to route this request to respective business service
*/
func onGetData(){
UIManager.flowController?.navigate(url: "myapp://featureXYZ?cmd=getData@viewId="+self.viewId)
}
override func reload( model: View ){
super.reload(model: model)
print("Here is the data from my model :: \(model.data!)")
}
func onNavigate(){
UIManager.flowController?.navigate(url: "myapp://VC3")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
VC3.swift
import UIKit
class VC3: BaseVC {
override func viewDidLoad() {
super.viewDidLoad()
let btn:UIButton = UIButton(frame: CGRect(x: 100, y: 200, width: 150, height: 50))
btn.setTitle("VC3", for: .normal)
btn.titleLabel?.textColor = UIColor.white
btn.backgroundColor = UIColor.blue
btn.addTarget(self, action: #selector(onClick), for: .touchUpInside)
self.view.addSubview(btn)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func onClick(){
super.onClick()
print("You clicked me")
UIManager.flowController?.navigate(url: "myapp://View")
}
override func reload(model: View) {
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
BusinessLayer
BusinessManager.swift – This is responsible for the following:
- Routes the action from views coming from UIManager to respective business service
- Holds reference to network layer which the business services can have access to make network calls
- Holds reference to module manager which has a registry of all the service modules.
import Foundation
import UIKit
class BusinessManager{
static let sharedInstance = BusinessManager()
let networkManager:NetworkManager? = NetworkManager()
var router:Router? = nil
var moduleManager:ModuleManager? = nil
init(){
self.router = Router(businessLayer: self)
self.moduleManager = ModuleManager(businessLayer: self)
}
}
Router.swift – Responsible to map actions to respective module services
import Foundation
class Router{
private var businessMgr:BusinessManager? = nil
private var routerHelper:RouterHelper? = nil
init( businessLayer: BusinessManager ){
self.businessMgr = businessLayer
self.routerHelper = RouterHelper(businessLayer: self.businessMgr!)
}
/*
This method parses the url into different components namely
- module or feature
- module or feature action
- parameters
- view identifier
Note: The action method can be further optimized. This implementation
is quick and dirty implementation to get the job done FYI
*/
func action( url: String ){
let action:String = url.components(separatedBy: "myapp://").last!
let moduleData = action.components(separatedBy: "?")
let params = moduleData.last?.components(separatedBy: "#")
let moduleName = moduleData.first
let cmd = params?.first
let moduleCommand = cmd?.replacingOccurrences(of: "cmd=", with: "").components(separatedBy: "@").first
let query = params?.last?.components(separatedBy: "@")
let viewId = query?.last?.replacingOccurrences(of: "viewId=", with: "")
let queryParams = query?.first
if ( moduleName == "featureABC" ){
if moduleCommand == "reserve"{
self.routerHelper?.featureABCReserve(viewId: viewId!, params: queryParams!)
return
}
if moduleCommand == "cancel"{
self.routerHelper?.featureABCCancel(viewId: viewId!, params: queryParams!)
return
}
}
if ( moduleName == "featureXYZ" ){
if moduleCommand == "getData"{
self.routerHelper?.featureXYZGetData()
}
}
}
}
RouterHelper.swift – A utility or helper class to the main Router.swift class which provides module specific invocation interfaces
import Foundation
class RouterHelper{
private var businessMgr:BusinessManager? = nil
init( businessLayer: BusinessManager ){
self.businessMgr = businessLayer
}
func featureABCReserve(viewId: String, params:String){
self.businessMgr?.moduleManager?.featureABC?.reserve(viewId: viewId, params: params)
}
func featureABCCancel( viewId: String, params:String ){
self.businessMgr?.moduleManager?.featureABC?.cancel(viewId: viewId, params: params)
}
func featureXYZGetData(){
self.businessMgr?.moduleManager?.featureXYZ?.getData()
}
func testModelLayer(){
}
}
ModuleManager.swift – Registry for all the modules. Where the respective module or business services are initialized
import Foundation
class ModuleManager{
private var businessMgr:BusinessManager? = nil
var featureXYZ:FeatureXYZService? = nil
var featureABC:FeatureABCService? = nil
init( businessLayer: BusinessManager ){
self.businessMgr = businessLayer
self.featureXYZ = FeatureXYZService()
self.featureABC = FeatureABCService(businessLayer: self.businessMgr!)
}
}
FeatureABC -> FeatureABCService.swift
Modules or feature are grouped into the respective group name. Followed by their respective business services. Here the reservation and cancellation action are part of this sample module called FeatureABC which lies under the service FeatureABCService
import Foundation
class FeatureABCService{
private var businessMgr:BusinessManager? = nil
init( businessLayer: BusinessManager ){
self.businessMgr = businessLayer
}
/*
Here the interface reserve takes viewId and params associated
Since this is a example feature mocks making some reservation. We try to resemble
a network call with self.businessMgr?.networkManager?.restCall which will always
execute the onSuccess callback for now. Where we assume that we've got a successful
response from the server and inside the block we invoke the UIManager
to update the respective view with some static sample data which is
some string here
To update a view the UIManager needs something called ViewContext
The ViewContext is made up of the following:
- view identifier - viewId (String)
- view Model - model (View)
*/
func reserve(viewId: String, params: String){
var reservationId = params.replacingOccurrences(of: "reservationId=", with: "")
self.businessMgr?.networkManager?.restCall(url: "some service url goes here", onSuccess: {
let newModel = View(error: nil, data: "Reservation done successfully confirmation id: #8347838")
let context:ViewContext = ViewContext(viewId: viewId, model: newModel)
UIManager.reload(viewContext: context)
})
}
/*
Here the interface reserve takes viewId and params associated
Since this is a example feature mocks a cancellation of reservation.
We try to resemble a network call with self.businessMgr?.networkManager?.restCall which will always
execute the onSuccess callback for now. Where we assume that we've got a successful
response from the server and inside the block we invoke the UIManager
to update the respective view with some static sample data which is
some string here
To update a view the UIManager needs something called ViewContext
The ViewContext is made up of the following:
- view identifier - viewId (String)
- view Model - model (View)
*/
func cancel(viewId: String, params: String){
var reservationId = params.replacingOccurrences(of: "reservationId=", with: "")
self.businessMgr?.networkManager?.restCall(url: "some service url goes here", onSuccess: {
let newModel = View(error: nil, data: "Reservation cancelled successfully for reservation id: #\(reservationId)")
let context:ViewContext = ViewContext(viewId: viewId, model: newModel)
UIManager.reload(viewContext: context)
})
}
func update(){
//to be implemented
}
}
FeatureXYZ -> FeatureXYZService.swift
import Foundation
class FeatureXYZService{
func getData(){
//Get remote data from server and update the view.//Perhaps you can try it out by following the same approach//illustrated in FeatureABCService. This method is invoked by button//"Get Data" from VC2.swift
}
}
Models -> View.swift
The group Models is meant to contain all the models. Here the model View.swift has very basic fields data and error. This is associated with views (VC1, VC2, VC3) as the respective view model.
import Foundation
class View{
var error:String?
var data:String?
init( error: String?, data: String? ){
self.error = error
self.data = data
}
}
Models -> ViewContext.swift
The ViewContext is what the UIManager relies on for pushing updates to the respective view. Here the view id and model constitutes the view context that the business service has to provide in order to notify the updates.
import Foundation
class ViewContext{
var id:String? = nil
var model:View? = nil
init(viewId: String, model: View ){
self.id = viewId
self.model = model
}
}
NetworkLayer -> NetworkManager.swift
The NetworkLayer group is meant for network services and adapters. The NetworkManager has a very simple shell here. However the NetworkManager can have different adapters for different network services such as REST, MQTT, Bluetooth Connection etc. For the sake of this example instead of implementing full blown adapters. I’ve just implemented a method to mock a rest call. Perhaps you can take this and extend it further 🙂
import Foundation
class NetworkManager{
let mqttManager:Any? = nil
func restCall(url: String, onSuccess success: () -> Void){
success()
}
}
ExceptionLayer -> ExceptionManager.swift
The ExceptionManager.swift provides 2 interfaces one for publishing the error and the other for subscribing to errors. The modules or services can use the publish interface to throw error. All the errors can be caught and handled in a central place which is the subscriber callback. Please refer the inline comments of this class for API docs. Also we could capture these errors and log it to a remote server for remote debugging purposes as well.
import Foundation
class ExceptionManager{
private var logs: Array<Error>
private let loggingSystemURL:String?
private var callback: (Error?) -> ()
/**
Initializer takes the remote logging system url. The initializer will be evolved in the next versions based on configurations
- Author:
Haseeb Afsar
- parameters:
* logSystem: String takes the remote system URL
- returns:
Returns [ExceptionManager] object
*/
init( logSystem: String? ) {
self.loggingSystemURL = logSystem
self.logs = [Error]()
self.callback = {_ in }
}
/**
Interface to observe error.
- Author:
Haseeb Afsar
- parameters:
* observer: Closure(Error) It takes a closure and passes the Error object which is published from any other modules
- returns:
Returns nothing
*/
func setObserver(observer: @escaping (Error?) -> ()) {
self.callback = observer
}
/**
Interface to publish error.
- Author:
Haseeb Afsar
- parameters:
* error: Error
- returns:
Returns nothing
*/
func publish(error: Error){
self.logs.append( error )
self.callback( error )
}
}
Please find below the screenshot for usage of ExceptionManager.swift
Initialization and Subscription
Publishing Exception
Again for the sake of this architecture example. ExceptionLayer has a basic implementation. You can take this and extend it further.
To wrap this up last but not the least. I would like to show you the call stack propagation from view (VC2) to its respective business service (which is FeatureABCService). Also update back. Please refer the diagram below
So in the diagram above you can see it on the left side under “Thread 1” from 10 to 0. How the communication between different layers is streamlined. This provides a clean separation of concern which will make the mobile app scalable, testable and maintainable.
Lastly I would to thank you for reading it through this article. I hope this helps you in your journey of building mobile apps. Also this sample architecture project is available for your reference on GitHub
Please feel free to contribute and help the community build efficient mobile apps. Looking forward to hear from you on your thoughts, feedback.