In the previous article(http://trikalabs.com/aws-integrating-user-pools-for-ios-apps/) we integrated User Pools into an iOS project. Also in the article http://trikalabs.com/clean-architecture/,we learned how to use Clean Architecture in an iOS app.
In this article, we will transform our user pool project in Clean Architecture. Since we are planning to explore more AWS features ( PinPoint, AppSync, etc) in the next articles, it’s a good idea to tidy up our code.
As we know the key parts of the Clean Architectures are the Interactor which communicates with the workes, the entities, the Present and the UI layer. All of these parts are configured in an specific for that purposeobject , the module composition.
The following image shows the files structure:
As we see from the image we group files related to one specific feature together.
Let’s start first by creating the composition root. We don’t want to have a lot of setup code inside the Appdelegate. So we will create the composition root object which will get the initial UINavigationController as input and will create and manage all the dependencies.
Let’s have a look at the Appdelegate:
1 2 3 4 5 6 7 8 9 10 11 12 |
private let compositionRoot = CompositionRoot() func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { guard let navigationViewController = self.window?.rootViewController as? UINavigationController else { return true } compositionRoot.start(navigationController: navigationViewController, launchOptions: launchOptions) return true } |
All we are doing in the didFinishLaunchingWithOptions method is calling start on the composition root with the initial UINavigationController.
Let’s what is the code inside the composition root.
1 2 3 4 5 6 7 8 9 |
import UIKit class CompositionRoot { func start(navigationController: UINavigationController, launchOptions: [UIApplication.LaunchOptionsKey : Any]?) { let authenticationManager = AuthenticationManager() AuthModuleComposition(navigationController: navigationController, authenticationManager: authenticationManager) } } |
The role of the composition root is to create the dependencies and pass them to the modules. In the above code, we create the AuthModuleComposition with the authenticationManager and the navigation controller.
The AuthModuleComposition creates and compose all the objects that this class will need:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import UIKit class AuthModuleComposition { let navigationController: UINavigationController let authenticationManager: AuthenticationProtocol @discardableResult init(navigationController: UINavigationController, authenticationManager: AuthenticationProtocol) { self.navigationController = navigationController self.authenticationManager = authenticationManager start() } private func start() { guard let vc = navigationController.viewControllers.first as? ViewController else { fatalError("") } let presenter = AuthPresenter() let interactor = AuthInteractor(navigationController: navigationController, authenticationManager: authenticationManager) interactor.authPresenter = presenter vc.authUseCase = interactor presenter.authViewModel = vc } } |
To keep our code modular we use protocols for the communication between,the presenter, the interactor and the UI.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import UIKit // UI protocol AuthViewModel { func update(model: AuthViewModelData) } struct AuthViewModelData { let shouldShowSignInButton: Bool let shouldShowSignOutButton: Bool } // Presentation protocol AuthPresenterProtocol { func updateEror(error: Error?) func updateSignedIn() func updateSignedOut() func updateUnknow() } // UseCase protocol AuthInteractorUseCase { func initialise() func signIn() func signOut() } |
Let’s now have a look at the VieController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import UIKit class ViewController: UIViewController { @IBOutlet weak var signInButton: UIButton! @IBOutlet weak var signOutButton: UIButton! var authenticationManager: AuthenticationProtocol = AuthenticationManager() var authUseCase: AuthInteractorUseCase? override func viewDidLoad() { super.viewDidLoad() authUseCase?.initialise() } @IBAction func didTapSignIn(_ sender: Any) { authUseCase?.signIn() } @IBAction func didTapSignOut(_ sender: Any) { authUseCase?.signOut() } } extension ViewController: AuthViewModel { func update(model: AuthViewModelData) { print(model) signInButton.isHidden = !model.shouldShowSignInButton signOutButton.isHidden = !model.shouldShowSignOutButton if signInButton.isHidden { let storyBoard = UIStoryboard(name: "Main", bundle: nil) let vc = storyBoard.instantiateViewController(withIdentifier: "ShowTodoItemsViewController") self.navigationController?.pushViewController(vc, animated: true) } } } |
The ViewController all it does is to call initialise on the useCase. This will give us the sign in state of the user. Also, it calls sign in or sign out when the relevant button is pressed. Finally, it updates the visibility of the buttons when the update method is called (by the presenter).
Let’s now look at the interactor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
import UIKit class AuthInteractor: AuthInteractorUseCase { let authenticationManager: AuthenticationProtocol let navigationController: UINavigationController var authPresenter: AuthPresenterProtocol? init(navigationController: UINavigationController, authenticationManager: AuthenticationProtocol) { self.authenticationManager = authenticationManager self.navigationController = navigationController } func initialise() { authenticationManager.initialise { [weak self] (userState, error) in self?.handleAuthResponce(userState: userState, error: error) } } func signIn() { authenticationManager.signIn(navigationController: navigationController) { [weak self] (userState, error) in self?.handleAuthResponce(userState: userState, error: error) } } func signOut() { authenticationManager.signOut(completion: { [weak self] (userState, error) in self?.handleAuthResponce(userState: userState, error: error) }) } private func handleAuthResponce(userState: UserState?, error: Error?) { guard let userState = userState else { authPresenter?.updateEror(error: error) return } switch(userState) { case .signedIn : authPresenter?.updateSignedIn() case .signedOut : authPresenter?.updateSignedOut() case .unknow : authPresenter?.updateUnknow() } } } |
The interactor calls the AuthManager worker to handle all the requests that are related to authentication. When the worker finishes a task, the interactor calls the method handleAuthResponce. This method parses the response of the worker, gets the user state and inform the presenter about the state (without leaking any domain knowledge).
Let’s look now at the presenter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class AuthPresenter: AuthPresenterProtocol { var authViewModel: AuthViewModel? func updateEror(error: Error?) { // show alert } func updateSignedIn() { let data = AuthViewModelData(shouldShowSignInButton: false, shouldShowSignOutButton: true) authViewModel?.update(model: data) } func updateSignedOut() { let data = AuthViewModelData(shouldShowSignInButton: true, shouldShowSignOutButton: false) authViewModel?.update(model: data) } func updateUnknow() { // show alert } } |
The presenter all it does is when it receives a call from the interactor, calls the viewController with the appropriate logic for the visibility of the buttons.
In this tutorial we don’t handle the error case, something that we should always do in production code.
For the sake of completeness the AuthenticationManager code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
import UIKit enum UserState { case signedIn, signedOut, unknow } protocol AuthenticationProtocol { func signIn(navigationController: UINavigationController, completion: @escaping (UserState?, Error?) -> Void) func signOut(completion: @escaping (UserState?, Error?) -> Void) func initialise(completion: @escaping (UserState?, Error?) -> Void) } import AWSMobileClient class AuthenticationManager: AuthenticationProtocol { func signIn(navigationController: UINavigationController, completion: @escaping (UserState?, Error?) -> Void) { AWSMobileClient.sharedInstance().initialize { (userState, error) in if let userState = userState { switch(userState){ case .signedIn: DispatchQueue.main.async { completion(.signedIn, nil) } case .signedOut: AWSMobileClient.sharedInstance().showSignIn(navigationController: navigationController, { (userState, error) in if(error == nil){ DispatchQueue.main.async { completion(.signedIn, nil) } } }) default: AWSMobileClient.sharedInstance().showSignIn(navigationController: navigationController, { (userState, error) in if(error == nil){ DispatchQueue.main.async { completion(.signedIn, nil) } } }) } } else if let error = error { print(error.localizedDescription) DispatchQueue.main.async { completion(.unknow, error) } } } } func signOut(completion: @escaping (UserState?, Error?) -> Void) { AWSMobileClient.sharedInstance().signOut() DispatchQueue.main.async { completion(.signedOut, nil) } } func initialise(completion: @escaping (UserState?, Error?) -> Void) { AWSMobileClient.sharedInstance().initialize { (userState, error) in if let error = error { print("Error initializing AWSMobileClient: \(error.localizedDescription)") } else if let userState = userState { switch(userState){ case .signedIn: DispatchQueue.main.async { completion(.signedIn, nil) } case .signedOut: DispatchQueue.main.async { completion(.signedOut, nil) } default: DispatchQueue.main.async { completion(.unknow, nil) } } } } } } |