• iOS
  • MOBILE

Head First Mobile Application Architecture

Paul Dmitriev
14 Dec 2019
17 Min Read
Head First Mobile Application Architecture

If we try to measure time, spent by software engineers discussing current application architectures and inventing new ones, we’d need to come up with a new measurement unit “man-eon”. Mobile applications and iOS/Swift per se didn’t become an exception. We’ve got dozens of popular architectures and thousands of relatively unknown, used in the small cubicles all over the world. Just to name a few:

  • MVC with fearful monster view controllers
  • MVVM which always tries to become an MVC and start growing it’s View Controller
  • VIPER that forces you to use code generation tools to create those 5-6 classes, necessary to add a single label
  • ELM architecture which will leave you wondering: “what the hell is that ELM?”

And so on, and so forth.

So, I’ve decided to showcase my approach, which I’m using in relatively small applications. I didn’t come up with some fancy name, probably it’s still MVVM or one of its offsprings, but done with respect to the first principles (I suppose).

Main features of suggested approach:

  • less additional classes mean fewer thoughts on “where should I put that”
  • maximum usage of Xcode features and UIKit powers
  • simplified testing
  • overall simple approach (even your juniors can handle it)

First, let’s outline basic concepts.

  • we’re using RxSwift for gluing different layers of our application. It also helps us dealing with asynchronous tasks
  • we’re trying to move everything from our ViewController, but without fanaticism
  • we’re utilizing dependency injection to decouple our classes
  • we’re using protocols to represent dependencies to simplify mocking and testability

If you can’t spend man-eon of time for discussing and need somebody to get things done – just book a call with us.

Book a strategy session

Get actionable insights for your product

    Success!

    We’ll reach out to schedule a call

    With all that stuff in mind, let’s try to assemble a simple application for DomainsDB search. I’ve chosen this API as an example because it’s simple, and can be used without unnecessary trivial stuff like obtaining the keys. This API has a single endpoint: https://api.domainsdb.info/search?query=XXX and replies with following JSON:

    {
    "total": 11906,
    "time": 152,
    "domains": [
    	{
    		"hasWhois": 0,
    		"country": null,
    		"NS": "ns1.parkingcrew.net,ns2.parkingcrew.net",
    		"domain": "med-med-buy.com",
    		"expiry_date": null,
    		"create_date": null,
    		"update_date": "2018-01-11T05:29:12.517Z",
    		"isDead": false
    	},
    	// ...
    	]
    }
    

    If you need the full source of application, you can get it from GitHub.

    Warm up

    First of all, we’ll need to add dependencies. I’m using Cocoapods, and you can investigate Podfile in our repository to get an idea, what we’ll need.

    Now, let’s create a UI, it’s dead simple: UISearchBar on the top, and UITableView occupying the rest of our scene. Put in place all required constraints and create outlets for these two objects so that we can use them in our view controller. Additionally, I’ve added an UIView with UILabel in the center to show messages to the user (mainly they’re to showcase additional RxCocoa concepts). They will require outlets too.

    On to the next step: value objects to store information, received from API (aka “The Model”). Swift 4 introduced great new Decodable protocol, which will help us with JSON parsing. Information about domain names we’d like to get is limited so that this struct will be simple.

    struct Domain: Decodable {
         enum CodingKeys: String, CodingKey
         {
             case name = "domain"
             case updated = "update_date"
         }
     
         let name: String
         let updated: String
     }
     
     struct Domains: Decodable {
         let domains: [Domain]
    }
    

    For Domain class we’ll need to define CodingKeys, the rest compiler will do for us automatically.

    Network layer

    To fetch data from the net, we’ll need a particular class that accepts parameters, retrieve data, and wraps it into our value types, using their conformance to Decodable. But first, let’s think a little bit about testing. Most probably, in the tests we’d like to avoid real network requests, so we’ll want to substitute this class with some mock. To simplify it, we’ll introduce protocol, and all classes, depending on our network layer will depend on protocol, not on its specific implementation (do you feel that cool breath of Dependency Inversion Principle?). BTW, I’ve saved myself a bit of time and moved dealing with boilerplate code to RxAlamofire.

    protocol NetworkManager {
    var isLoading: Observable { get }
    func makeRequest(method: HTTPMethod, url: URLConvertible, params: Parameters) -> Observable
    }

    Naturally, I want my network code to deal with additional routines like showing network indicator, so we’re exposing handy isLoading observable. But let’s look at the specific implementation.

    Probably, someday, Swift will allow us to have stored properties in protocol extensions, and finally, we’ll be able to use the full power of partially implemented abstractions, but even now, this approach is a mighty way of code de-duplication.

    But let’s look at the specific implementation.

    struct AfNetworkManager: NetworkManager {
         private let disposeBag = DisposeBag()
         private let activity = PublishSubject()
     
         let isLoading: Observable
     
         init(bindNetworkActivityIndicator: Bool = true) {
             self.isLoading = activity.asObservable()
                 .share(replay: 1)
     
             if bindNetworkActivityIndicator {
                 isLoading.asDriver(onErrorJustReturn: false)
                     .drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
                     .disposed(by: disposeBag)
             }
         }
     
         func makeRequest(method: HTTPMethod, url: URLConvertible, params: Parameters) -> Observable {
             activity.onNext(true)
             return request(method, url, parameters: params)
                 .validate(statusCode: 200.. Model? in
                     return try? JSONDecoder().decode(Model.self, from: data)
                 }
                 .catchError { [activity] error in
                     print("!!! We've got an error \(error.localizedDescription)")
                     activity.onNext(false)
                     return Observable.just(nil)
                 }
         }
     }

    Due to active usage of RxSwift, the complex asynchronous code looks nice and declarative:

    • set activity status to true
    • request resource
    • validate response
    • set activity status to false
    • parse it and wrap to value object
    • handle error

    As you probably see, error handling is simplified here, in a real application we’d have to either use some enum to separate success from failure or propagate the error to a higher level of code for correct handling. In our case, we’ll return nil to indicate failing requests.

    As an additional level of service, we’re automatically binding our isLoading to UIApplication.shared.isNetworkActivityIndicatorVisible, but we’re leaving possibility to disable that functionality if some or AfNetworkManager users will need that.

    View model

    Next step is to implement the specific class to perform a search. In our case it will serve us as a view model. Here we’ll also use DIP and introduce the protocol.

    struct SearchParameters {
         let request: String
     }
     
     protocol SearchViewModel {
         init(networkManager: NetworkManager, parameters: Observable)
         var results: Observable { get }
         var isActive: Observable { get }
     }

    Few necessary notes. We’ll need the proper class to wrap parameters, passed to our model. Usually, we’ll need to get some user input, and based on it, perform some actions. That’s the purpose of that class, in this particular case it has just one field with request query, but usually, there will be more parameters.

    Our search model protocol defines the constructor with two parameters: an instance of a class, implementing NetworkManager protocol and observable with parameters. As parameters tend to change with time, our model has to respond to them. Model exposes two observables, first will return necessary data, if it’s present, second will help us to track model’s activity, to show some progress indicator to the user.

    Let’s take a look at model’s code.

    struct DomainerSearchViewModel: SearchViewModel {
         private static let url = "https://api.domainsdb.info/search"
         private let manager: NetworkManager
         private let params: Observable
         private let activity = PublishSubject()
     
         let results: Observable
         let isActive: Observable
     
         init(networkManager: NetworkManager, parameters: Observable) {
             self.manager = networkManager
             self.params = parameters
     
             self.results = params
                 .do(onNext: { [weak activity] _ in
                     activity?.onNext(true)
                 })
                 .flatMapLatest { [manager] params -> Observable in
                     guard !params.request.isEmpty else {
                         return Observable.just(nil)
                     }
                     let qryParams = ["query": params.request]
                     return manager.makeRequest(method: .get, url: DomainerSearchViewModel.url, params: qryParams)
                 }
                 .map { data in
                     return data?.domains ?? []
                 }
                 .do(onNext: { [weak activity] _ in
                     activity?.onNext(false)
                 })
                 .observeOn(MainScheduler.instance)
                 .share(replay: 1)
     
             self.isActive = activity.asObservable()
                 .observeOn(MainScheduler.instance)
                 .share(replay: 1)
         }
     }

    Once again, RxSwift helps us to make it simple and easy to understand. We’re subscribing to the changes in our parameters, translate them to requests to API, and just return them. Note, how we’re handling our isActive state, and transform our sequence to the observable at the end.

    Putting pieces together

    Finally, we’ve come to the final assembly. Let’s try to lift off with all that stuff. We’ll start pre-flight checks with variables definition.

    private let disposeBag = DisposeBag()
     private lazy var parametersObservable: Observable = {
         return searchBar.rx.text
             .orEmpty
             .debounce(0.3, scheduler: MainScheduler.instance)
             .distinctUntilChanged()
             .map { SearchParameters(request: $0) }
             .share(replay: 1)
     }()
     
     private lazy var model: SearchViewModel = DomainerSearchViewModel(networkManager: AfNetworkManager(),
                                                                       parameters: self.parametersObservable)
     
     private lazy var modelActive: Driver = self.model.isActive.asDriver(onErrorJustReturn: false)
     private lazy var gotResults: Driver = self.model.results.map { !$0.isEmpty }.asDriver(onErrorJustReturn: false)

    I suppose disposeBag and model don’t require additional explanations. modelActive and gotResults are two convenience drivers, we’re using to control our UI. parametersObservable in this case is pretty straightforward. If we have more input parameters, we’d have to use Observable.combineLatest to join them together. If we’d need some complex validation or actions like parameters storage, we’d introduce the additional class. But in this case, for the sake of simplicity, we’ll debounce input, filter it and wrap in a required class. Now, we’re ready to make final bindings. I usually do that in viewDidLoad.

    Our data goes directly to UITableView, using RxCocoa default binding.

    model.results
         .asDriver(onErrorJustReturn: [])
         .drive(resultsTable.rx.items) { (tableView, _, element) in
             let cell = tableView.dequeueReusableCell(withIdentifier: "TableCell")!
             cell.textLabel?.text = element.name
             cell.detailTextLabel?.text = element.updated
             return cell
         }
         .disposed(by: disposeBag)

    Now, we’d like to show some kind of HUD, so the user will be aware, that our app is working, instead of idly waiting for battery discharge.

    modelActive
         .drive(onNext: { state in
             if state {
                 HUD.show(.progress)
             } else {
                 HUD.hide()
             }
         })
         .disposed(by: disposeBag)

    Next step is optional, let’s pretend we’d like to block user interaction until we’re not done with the request. RxCocoa will quickly help us.

    modelActive
         .map(!)
         .drive(searchBar.rx.isUserInteractionEnabled)
         .disposed(by: disposeBag)

    By the way, if we’ll skip the previous step, our app will continue to work pretty nicely. If user change input while current request is still in progress, flatMapLatest in our observable will trigger a new request, ignoring previous one, so we’ll be sure that only final result will be shown.

    Now, the final touch, showing messages to the user in case we’ve got no results or no input provided.

    gotResults
         .drive(messageView.rx.isHidden)
         .disposed(by: disposeBag)
     
     Driver.combineLatest(gotResults, searchBar.rx.text.orEmpty.asDriver())
         .filter { !$0.0 }
         .map { $0.1.isEmpty ? "Please, search something" : "No result for this request" }
         .drive(messageLabel.rx.text)
         .disposed(by: disposeBag)

    First binding merely shows (or hides) our view with a message. The second one is a bit more complex, as it requires knowledge of two facts: do we have data at all, and was input query specified. We’re casting our searchBar text to driver, so we can combine it with parametersObservable using combineLatest, and after a few simple transformations and filtering, we can bind results to our label.

    Head First Mobile Application Architecture - photo 1

    Unit testing

    Now, we’ll add some tests to serve as a cherry on top of our cake. First, let’s implement mock NetworkManager which simply returns necessary JSON.

    class MockNetworkManager: NetworkManager {
         let isLoading: Observable = Observable.never()
     
         private let mockData: Data
     
         init(stubName: String) {
             let url = Bundle(for: MockNetworkManager.self).url(forResource: stubName, withExtension: "json")!
             self.mockData = try! Data(contentsOf: url)
         }
     
         func makeRequest(method: HTTPMethod, url: URLConvertible, params: Parameters) -> Observable {
             let parsed = try! JSONDecoder().decode(Result.self, from: self.mockData)
             return Observable.just(parsed)
         }
     }

    If necessary, we can capture request parameters, if we’d like to test if our view model makes correct requests, but in this test I’ve decided to skip that part to make everything simpler.

    Now, we can form instance of view model with our mock.

    private var stubModel: DomainerSearchViewModel {
         let manager = MockNetworkManager(stubName: "SearchStub")
         let params = Observable.just(SearchParameters(request: "Med"))
         return DomainerSearchViewModel(networkManager: manager, parameters: params)
    }

    We’re ready to test “normal flow” of our code.

    func testParsing() {
         let result = try! stubModel.results.toBlocking().single()
     
         XCTAssertEqual(result.count, 50)
     
         let sample = result.first!
     
         XCTAssertEqual(sample.name, "med-med-buy.com")
         XCTAssertEqual(sample.updated, "2018-01-11T05:29:12.517Z")
    }

    As you can see, observables are easily testable, using RxBlocking.

    Obviously, we can use “direct flow” to test observables too: just subscribe and capture results for further validation. But in this case we’ll need proper synchronization blocking to be put in place.

    func testActivity() {
         var result: [Bool] = []
         let disposeBag = DisposeBag()
     
         let model = stubModel
         model.isActive
             .subscribe(onNext: { state in
                 result.append(state)
             })
             .disposed(by: disposeBag)
     
         _ = try! model.results.toBlocking().single()
     
         XCTAssertEqual(result, [true, false])
    }

    Conclusion

    Let’s summarize. Our view controller is still working with necessary presentation logic, but anything besides that is moved to other classes. We can easily extend this schema, introducing additional classes with RxSwift working as glue. In next article, I’ll show yet another “home brew” solution — coordinator for app navigation.

    Book a strategy session

    Get actionable insights for your product

      Success!

      We’ll reach out to schedule a call