Trikalabs
  • Home
  • Best online TDD videos
  • Book Suggestions
  • About Me
  • Contact
Trikalabs
No Result
View All Result

AWS: Integrate User Pools with Clean Architecture

by fragi
July 25, 2019
in AWS
Share on FacebookShare on Twitter

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:

Swift
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.

Swift
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:

Swift
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.

Swift
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:

Swift
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:

Swift
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:

Swift
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:

Swift
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)
                    }
                }
                
            }
        }
    }
}

Tags: Amazon CognitoAmplify
fragi

fragi

Related Posts

AWS: AppSync – Real-time data with subscriptions.
AWS

AWS: AppSync – Real-time data with subscriptions.

July 25, 2019

In the previous article (http://trikalabs.com/aws-appsync-swift/) we set up AppSync into our app. In this article, we will see how we...

AWS: Build data-driven apps with real-time and offline capabilities by using AppSync and Swift.
AWS

AWS: Build data-driven apps with real-time and offline capabilities by using AppSync and Swift.

July 25, 2019

In this article, we will use the AppSync framework to fetch and create data for our iOS app. By using...

AWS: Use Amazon PinPoint to collect analytics
AWS

AWS: Use Amazon PinPoint to collect analytics

July 25, 2019

In this article, we will add analytics to an iOS app by using Amplify and PinPoint. Create a project Firstly...

AWS: Integrate User Pools on iOS Apps
AWS

AWS: Integrate User Pools on iOS Apps

July 25, 2019

Authenticate and manage users is a common task/need for many apps. In this article, we will set up an Amazon Cognito...

Next Post
AWS: Use Amazon PinPoint to collect analytics

AWS: Use Amazon PinPoint to collect analytics

AWS: Build data-driven apps with real-time and offline capabilities by using AppSync and Swift.

AWS: Build data-driven apps with real-time and offline capabilities by using AppSync and Swift.

AWS: AppSync – Real-time data with subscriptions.

AWS: AppSync - Real-time data with subscriptions.

  • Advertise
  • Privacy & Policy
  • Contact

© 2019 Trikalabs Ltd. All rights reserved.

No Result
View All Result
  • Home
  • About Me
  • A curated list with the best free online TDD videos
  • Book Suggestions
  • Pinner Code Club

© 2019 Trikalabs Ltd. All rights reserved.