The Use and Adaptation of the VIPER Architecture on Our Own Example of Development, part 4

The Use and Adaptation of the VIPER Architecture on Our Own Example of Development, part 4

Table of contents

We’re happy to bring to your attention the last part of the article series on using and adapting the Viper architecture. In the first and second parts, we were talking about the basics and structuring project skeleton for VIPER. And last time we’d discussed creating a module in terms of VIPER architecture. Now we’re going to deal with the most difficult topic: VIPER example with Alamofire+PromiseKit+ObjectMapper.

Let's consider it in order.

APIService

There is a rather popular issue regarding API service implementation, namely coverage of all the logic by a single class. It leads to the service overload that makes fixing, testing and adding new elements processes very hard. Let’s see how we can separate responsibility between several classes (RequestBuilder, RequestExecutor, APIService).

1. RequestBuilder creates requests with parameters that should be passed to the Method

class RequestBuilder {
    
    // MARK:
    
    let endpoint: String = "http://api.openweathermap.org"
    
    let appId: String = "API Key" //openweathermap api key
    
    // MARK:
    
    func requestUrl(path: String) -> URLConvertible {
        
        return endpoint + path
        
    }
    
    // MARK: Requests
    
    func weather(for cityId: Int) -> URLRequest? {
        
        let url = requestUrl(path: "/data/2.5/weather")
        
        let parameters: [String: Any] = ["id" : cityId, "appid": appId]
        
        return try? URLEncoding.default.encode(URLRequest(url: url, method: .get), with: parameters)
        
    }
    
    
    
}

2. RequestExecutor runs requests built with RequestBuilder and processes the response

class RequestExecutor {
    
    // MARK:
    
    fileprivate let builder: RequestBuilder
    
    fileprivate let configuration: URLSessionConfiguration = URLSessionConfiguration.default
    
    fileprivate let queue: DispatchQueue
    
    fileprivate let session: SessionManager
    
    //MARK:
    
    init(requestBuilder: RequestBuilder) {
        
        queue = DispatchQueue.global(qos: .utility)
        
        builder = requestBuilder
        
        session = SessionManager(configuration: configuration)
        
    }
    
    // MARK:
    
    fileprivate func processResponse(response: DataResponse<Any>, checkToken: Bool, completion: @escaping (Any?, APIError?) -> Void) {
        
        let statusCode = response.response?.statusCode ?? 500
        
        if statusCode == 200 {
            
            completion(response.result.value, nil)
            
        } else {
            
            completion(nil, APIError(message: "Server error"))
            
        }
        
    }
    
    //MARK:
    
    func runRequest(request: URLRequest, completion: @escaping (Any?, APIError?) -> Void) {
        
        session.request(request)
            
            .validate(statusCode: [200])
            
            .responseJSON(queue: queue) { (response) in
                
                self.processResponse(response: response, checkToken: true, completion: completion)
                
        }
        
    }
    
}

3. RequestExecutorExtension defines all possible types of requests and methods of handling them (e.g the response is a mappable object, an array of mappable objects or other elements like String, Int, etc). So, it will be a mediator between APIService and core logic of the RequestExecutor.

extension RequestExecutor {
    
    func promiseQuery<ResultType: Mappable>(request: URLRequest?) -> Promise<ResultType> {
        
        return Promise<ResultType> { (fullfit, reject) in
            
            guard let request = request else {
                
                reject(APIError(message: "Invalid request"))
                
                return
                
            }
            
            self.runRequest(request: request) { (json, err) in
                
                if let err = err {
                    
                    reject(err)
                    
                } else {
                    
                    if let obj = Mapper<ResultType>(context: nil).map(JSONObject: json) {
                        
                        fullfit(obj)
                        
                    } else {
                        
                        reject(APIError(message: "Mapping error"))
                        
                    }
                    
                }
                
            }
            
        }
        
    }
    
}

4. APIService is a protocol which defines all possible methods it's responsible for.

protocol APIService {



   func obtainCurrentWeather(for cityId: Int) -> Promise<CurrentWeather>



}

5. APIServiceImpl is APIService implementation which calls RequestExecutor using the request created by RequestBuilder.

class APIServiceImpl {
    
    fileprivate let executor: RequestExecutor
    
    fileprivate let builder: RequestBuilder
    
    
    
    init() {
        
        builder = RequestBuilder()
        
        executor = RequestExecutor(requestBuilder: builder)
        
    }
    
}



extension APIServiceImpl: APIService {
    
    func obtainCurrentWeather(for cityId: Int) -> Promise<CurrentWeather> {
        
        return executor.promiseQuery(request: builder.weather(for: cityId))
        
    }
    
}

We use PromiseKit for APIService.

PromiseKit is a thoughtful and complete implementation of Promises for iOS and macOS platforms that has first-class support for both Objective-C and Swift.

  • Powerful. Promises transform asynchronous operations into composable flexible objects.

  • Easy. Promises make developing with asynchronicity very easy.

  • Delightful. Promises for Foundation, UIKit, etc. make iOS developing a dream come true.

We won’t take a look in prses in details, so if you want to learn more, see PromiseKit GitHub page or PromiseKit Documentation

Put it shortly, promises allow us to make asynchronous requests very easy to use.

We also use ObjectMapper for mapping the response into models applying a “map” method. The model should conform the “Mappable” protocol and define two methods: required init?(map: Map) and func mapping(map: Map). For Instance:

class CurrentWeather: Mappable {
    
    var clouds: Float?
    
    var humidity: Float?
    
    var pressure: Float?
    
    var temperature: Float?
    
    var city: String?
    
    var windDegree: Float?
    
    var windSpeed: Float?
    
    var visibility: Float?
    
    
    
    required init?(map: Map) {
        
        mapping(map: map)
        
    }
    
    
    
    func mapping(map: Map) {
        
        clouds <- map["clouds.all"]
        
        humidity <- map["main.humidity"]
        
        pressure <- map["main.pressure"]
        
        temperature <- map["main.temp"]
        
        city <- map["name"]
        
        windDegree <- map["wind.deg"]
        
        windSpeed <- map["wind.speed"]
        
        visibility <- map["visibility"]
        
    }
    
}

Now let’s take a look at our example of the app which shows the weather in different cities. We have two modules here: StartScreen and DetailsScreen. StartScreen contains the list of cities, DetailsScreen is responsible for displaying the weather in a selected city.

ApplicationRoot is responsible for starting the application; it also chooses the module that should be displayed first. In our case, it is StartScreen.

Let’s examine the VIPER module using the example.

When StartScreen controller is loaded it calls Presenter by StartScreenViewOutput and informs that View is ready by calling func viewIsReady(). It could perform a fetch request, for instance, or anything else needed after View has been loaded.

// MARK:

var output: StartScreenViewOutput!

// MARK: Life cycle

override func viewDidLoad() {
    
    super.viewDidLoad()
    
    output.viewIsReady()
    
}

Then, Сontroller needs cities to show on the screen, so StartScreenViewOutput defines var cities: [City] { get } and doesn’t care how presenter will get them. It’s used as a usual variable called by a function:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

   return output.cities.count

}

In this example, we have a hardcoded list of cities, so there is no need to wait until it’s retrieved from the web. Below, we will describe how to work with async API request.

Presenter communicates with Interactor by the next protocol:

protocol StartScreenInteractorInput {
    
    var cities: [City] { get }
    
}

and Presenter acts as a proxy between View and Interactor

var cities: [City] {
    
    return interactor.cities
    
}

Interactor doesn’t contain any information and decides where to retrieve data (e.g. to call a specific service or obtain data from database, etc.). In our example, cities are stored in WeatherService and Interactor has a reference to it (as it’s configured with required cities).

class StartScreenInteractor {
    
    var weatherService: WeatherService!
    
}

extension StartScreenInteractor: StartScreenInteractorInput {
    
    var cities: [City] {
        
        return weatherService.cities
        
    }
    
}

WeatherService conforms to

protocol WeatherService {
    
    func obtainCurrentWeather(for cityId: Int) -> Promise<CurrentWeather>
    
    func city(with id: Int) -> City?
    
    var cities: [City] { get }
    
}

and implementation

class WeatherServiceImpl {
    
    let apiService: APIService!
    
    lazy fileprivate var _cities: [City] = {
        
        return [
            
            City(title: "London", id: 2643743),
            
            City(title: "New York", id: 5128638),
            
            City(title: "Paris", id: 2968815),
            
            City(title: "Kyiv", id: 703447),
            
            City(title: "California", id: 5332921),
            
            City(title: "Madrid", id: 3117735),
            
            City(title: "Berlin", id: 2950159)
            
        ]
        
    }()
    
    init(apiService: APIService) {
        
        self.apiService = apiService
        
    }
    
}



extension WeatherServiceImpl: WeatherService {
    
    
    
    func obtainCurrentWeather(for cityId: Int) -> Promise<CurrentWeather> {
        
        return apiService.obtainCurrentWeather(for: cityId)
        
    }
    
    
    
    func city(with id: Int) -> City? {
        
        return _cities.first { (city) -> Bool in
            
            return city.id == id
            
        }
        
    }
    
    
    
    var cities: [City] {
        
        return _cities
        
    }
    
    
    
}

As we can see, each item is responsible for a specific task. ViewController shows the data on View, Presenter retrieves data from Interactor and prepares it for View (our example is easy enough and works as proxy). Interactor is the only one knowing where the information needed could be gotten from, and Service is responsible for the data.

When a user selects a city, View calls Presenter to show the next screen with weather details:

func showWeather(for cityId: Int) {

   self.output.showWeatherDetails(parameter: "\(cityId)")

}

And Presenter decides how to show it (push/present etc) and calls the corresponding method in AppRouter.

func showWeatherDetails(cityId: Int) {
    
    let urn = DetailsScreenFactory.shared.createModuleURN(cityId: cityId)
    
    self.router.pushModule(byUrn: urn, animated: true) { (parameters) in
        
        
        
    }
    
}

Here Presenter should create module “urn” to allow the router to push it. Let’s see DetailsScreenFactory in more details.

var moduleURN: String {

   return "DetailsScreen:{cityId}"

}

As we can see, this module has a parameter “cityId” needed to know a selected city. It has a method that creates urn with this parameter

func createModuleURN(cityId: Int) -> String {

   return URNBuilder(string: moduleURN).buildWithArgs(args: ["\(cityId)"])

}

Then, AppRouter creates the module by urn and pushes it.

func pushModule(byUrn urn: String, animated: Bool, completion: ModuleCompletionHandler?) {
    
    guard let url = URL(string:"\(Compass.scheme)\(urn)") else {
        
        fatalError("Invalid URN: \(urn)")
        
    }
    
    guard let controller = createModule(byUrl: url, completion: completion) else {
        
        fatalError("Can't create controller by URL: \(url)")
        
    }
    
    navigationController.pushViewController(controller, animated: animated)
    
}

Now, when DetailsScreen module has been shown, let’s take a look at ViewInput and ViewOutput protocols.

protocol DetailsScreenViewOutput {
    
    func setupInitialState(withArguments args: NamedValuesType, completion: ModuleCompletionHandler?)
    
    func viewIsReady()
    
    func city(with id: Int) -> City?
    
    func requestWeather(for cityId: Int)
    
}

We have two standard methods setupInitialState and viewIsReady. Also, it contains

func city(with id: Int) -> City?

//for getting City model by its id and

func requestWeather(for cityId: Int)

//for getting a weather for city

protocol DetailsScreenViewInput: class {
    
    func assignWeather(_ weather: CurrentWeather)
    
}

As we are going to use the asynchronous API request, we also should allow View to know when the request is executed and the weather is received. That’s why we need assignWeather method.

All views conform protocol ModuleInputProtocol

extension DetailsScreenViewController: ModuleInputProtocol {
    
    func setupInitialState(withArguments args: NamedValuesType, completion: ModuleCompletionHandler?) {
        
        cityId = Int(args["cityId"] as! String)!
        
        output.setupInitialState(withArguments: args, completion: completion)
        
    }
    
}

We parse the cityId parameter transferred from the previous controller.

When View is loaded we ask Presenter (output) for a city by id we previously parsed. Then we call the method viewIsReady and requests for the weather.

override func viewDidLoad() {
    
    super.viewDidLoad()
    
    title = "Details Screen"
    
    if let city = output.city(with: cityId) {
        
        title = city.title
        
    }
    
    output.viewIsReady()
    
    output.requestWeather(for: cityId)
    
}

Let’s see how Presenter performs the asynchronous task.

func requestWeather(for cityId: Int) {
    
    //show progress indicator if needed
    
    interactor.obtainCurrentWeather(for: cityId)
        
        .then { (weather) -> Void in
            
            self.view.assignWeather(weather)
            
        }
        
        .catch { (error) in
            
            if let viewController = self.view as? UIViewController {
                
                self.showError(error, for: viewController)
                
            }
            
        }
        
        .always {
            
            //hide progress indicator if shown
            
    }
    
}

We can show any activity indicator if needed before performing the request and hide it in “always” unit as it is called in both cases whether the request has succeeded or failed.

The method obtainCurrentWeather returns Promise (see PromiseKit GitHub page or PromiseKit for details), so, in “catch” unit we show an error alert meaning that the request is failed, and in “then” unit we transfer the result (the weather) to view.

extension DetailsScreenViewController: DetailsScreenViewInput {

   func assignWeather(_ weather: CurrentWeather) {

      reloadUI(with: weather)

   }

}

The interactor is quite simple here as well.

protocol DetailsScreenInteractorInput {

      func city(with id: Int) -> City?

      func obtainCurrentWeather(for cityId: Int) -> Promise<CurrentWeather>
   }



   extension DetailsScreenInteractor: DetailsScreenInteractorInput {

      func city(with id: Int) -> City? {

      return weatherService.city(with: id)

   }

   func obtainCurrentWeather(for cityId: Int) -> Promise<CurrentWeather> {

      return weatherService.obtainCurrentWeather(for: cityId)

   }

}

As you can see, VIPER is not so complicated as it could appear. All the logic is separated between several classes making app testing process much more convenient.

You can find example on our GitHub page

This piece concludes our series of articles on Viper architecture. We hope you were following our blog and found something useful to yourself.

 

Rate this article
15 ratings, average 4.80 of out 5
Table of contents
Get in touch
Next posts
The use and adaptation of the VIPER architecture on our own example of development, part 1
The use and adaptation of the VIPER architecture on our own example of development, part 1
Sergii Avilov
Sergii Avilov
The use and adaptation of the VIPER architecture on our own example of development, part 2
The use and adaptation of the VIPER architecture on our own example of development, part 2
Helen Vakhnenko
Helen Vakhnenko
The use and adaptation of the VIPER architecture on our own example of development, part 3
The use and adaptation of the VIPER architecture on our own example of development, part 3
Helen Vakhnenko
Helen Vakhnenko