2
\$\begingroup\$

I am using Moya to make network calls in Swift.

In my ViewController I am calling the WebService and creating a model from response.

import UIKit
import Moya

class LoginViewController: UIViewController {
  let provider = MoyaProvider<Api>()

  override func viewDidLoad() {
    super.viewDidLoad()
    generateAccessToken()
  }

  private func generateAccessToken() {
    provider.request(.generateAccessToken(
        mobile: phoneTextField.text!,
        countryCode: "91",
        name: "Saurabh",
        otp: "9612")
    ) { result in
        switch result {
        case .success(let response):
            let data = response.data
            let user = try? JSONDecoder().decode(User.self, from: data)
            if let authToken = user?.authToken {
                AuthManager.setToken(token: authToken)
            }
        case .failure(let error):
            print(error.response?.statusCode as Any)
        }
    }
  }

I want to move the WebService call to other file, say NetworkManager.swift

import Foundation
import Moya

struct NetworkManager {
  public static let shared = NetworkManager()
  private let provider = MoyaProvider<Api>()

  func generateAccessToken(with mobile: String, countryCode: String, name: String, otp: String)  {
    // Get result and return to calling class
  }
}

But it is strongly recommended not to use Singleton Class.

How should I call my WebService and return the response to the LoginViewController?

\$\endgroup\$
2
\$\begingroup\$

I would recommend looking into MVVM architecture. You can see a simple example in this repo.

One problem with it is that its author declares and assigned view models directly in a ViewController which can be a bit problematic when writing tests. A better approach would be to inject view model so it can be easily replaced with mock. To improve that approach you can take a look at coordinators patterns, a bit more advanced example can be found here

In most cases the WebService should go in the ViewModel in the MVVM pattern. Services can be injected to ViewModels, which are responsible for managing data provided by these services. Then when they perform all logical transformations (filtering, ordering, multiplying etc.) they provide this information to a view (ViewController in our case) which just consumes it. It allows us to easily write mocks and tests. There are of course more approaches like state machine, but ViewModel as content provider and meeting point for services is most common and most universal one in my opinion.

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for this. I have reading about MVVM. Just a quick clarification: should the WebService go in ViewModel in MVVM pattern? \$\endgroup\$
    – saurabh
    Apr 12 '20 at 17:06
  • \$\begingroup\$ In most cases - yes. Services can be injected to ViewModels, which are responsible for managing data provided by these services. Then when they perform all logical transformations (filtering, ordering, multiplying etc.) they provide this information to a view (ViewController in our case) which just consumes it. It allows us to easily write mocks and tests. There are of course more approaches like state machine, but ViewModel as content provider and meeting point for services is most common and most universal one in my opinion. \$\endgroup\$
    – Kamajabu
    Apr 12 '20 at 17:10
1
\$\begingroup\$

I think what you are missing is dependency injection and dependency inversion.

You should have something like

let keychainService = KeychainService()
let apiClient = ApiClient(keychainService: keychainService)
let loginService = LoginService(apiClient: apiClient, keychainService: keychainService)
let loginViewController = LoginViewController(loginService: loginService)

everything about headers and http goes to ApiClient, as well as adding the token to some requests, and retries. everything about requesting a new user goes to loginService. for reading and saving tokens and perhaps current user info, you can use KeychainService.

Note that you have to use protocols, so, the view controller relies on an abstraction, not a concrete implementation.

To manage this, you can use factory methods (a method that returns the view controller properly initialised) or frameworks like winject or DIP.

BTW, when using dependency inversion and injection, it is super easy to apply unit testing. You can very easily to create a mock of the login service and test that your view controller works fine.

\$\endgroup\$
-1
\$\begingroup\$

Could you make the following changes:

1) Create a file named LoginViewModel.swift

func getAccessToken(with mobile: String, code: String, otp: String, name: String, completion: @escaping(Bool, String)->Void) {
      networkManager.getAccessTokenFrom(mobile: mobile, code: String, otp: code, name: name) { (isSuccess, error) in
        if isSuccess {
            // Do neccessary logic
            // Set access token logic here
            completion(true, "")
        }
        else {
            completion(false, error.description)
        }
    }
}

2) Create a similar function in NetworkManager class to interact with Moya framework

let provider = MoyaProvider<Api>()

func getAccessTokenFrom(mobile: String, code: String, otp: String, name: String, completion: @escaping(error, String)->Void) {
      provider.request(.generateAccessToken(
        mobile: mobile,
        countryCode: code,
        name: name,
        otp: otp)
    ) { result in
        switch result {
    case .success(let response):
            let data = response.data
            let user = try? JSONDecoder().decode(User.self, from: data)
            if let authToken = user?.authToken {
                completion(true, authToken)
            }
        case .failure(let error):
        completion(false, error.description)

        }   
}}
}
  1. Inside LoginViewController.swift:
let loginViewModel = LoginViewModel()

func viewDidload() {
    // start activity loader
    loginViewModel.getAccessToken(with mobile: mobileTField.text, code: codeTField.text, otp: otpTField.text, name: nameTField.text) { (isSuccess, error) in
        if isSuccess {
            // Hide activity loader
        }
        else {
            // Show alert based on the "error" variable value
        }
    }
}
\$\endgroup\$
1
  • 2
    \$\begingroup\$ Welcome to code review. Good answers are observations about the code, and not necessarily alternate implementations. It would improve your answer, if you explained why each of these modifications are necessary. A good answer MUST include at least one observation about the code, and this answer doesn't contain any observations about the code. \$\endgroup\$
    – pacmaninbw
    Apr 19 '20 at 16:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.