AWS Amplifyを使ってCognito User PoolsログインするSwiftUIのサンプル

# はじめに

最近Amplifyを利用し始めたのですが、Amplify CLIでちょっとコマンドを実行するだけで、アプリからAWSのリソースを利用できるようになり、すごく便利に感じています。


ここでは、Amplifyを使って、Cognito User Poolsログインする処理をSwiftUIで行う方法を説明します。

ドキュメントには、ログインやログアウトなど一つ一つの例はあるのですが、具体的にアプリにどのように組み込むかまでは記載されていません。そこで、このサンプルが具体的にアプリにどう組み込むか悩んでいる方に役に立つのではないかと思い、書きました。


# 前提
バージョンは次の通りです。

- Xcode 11.2.1
- Amplify iOS SDK 2.0


また、この記事では、サインインの処理を具体的にどう組み込むか**のみ**を説明します。

CocoaPodsに依存関係を設定して、`$ amplify add auth`して、...のような基本的な手順は割愛します。基本的な利用手順については、以下のドキュメントをざっと読んでいただければと思います。
https://aws-amplify.github.io/docs/sdk/ios/authentication

# イメージ

アプリを起動して、ユーザーがログイン済で出ない場合、ログイン画面を開きます。
ログインしたら、ホーム画面に遷移します。

![ログインサンプル.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94278/97b46c6e-f3fa-fdd3-e294-9a1711435e1b.gif)


# Podfile

``` Podfile
target 'MyApp' do
  use_frameworks!

  pod 'AWSMobileClient', '~> 2.12.0'

  target 'MyAppTests' do
    inherit! :search_paths
  end

  target 'MyAppUITests' do
  end

end
```

# SceneDelegate
ポイントは以下の2つです。

- `AWSMobileClient.initialize(completionHandler:)`でAWSMoblieClientを初期化し、結果に応じてホーム画面またはログイン画面を開く
- `AWSMobileClient.addStateListener(object:callback:)`でuserStateをobserveし、userStateの変化に応じてホーム画面またはログイン画面を開く

``` SceneDelegate.swift

import UIKit
import SwiftUI

import AWSAppSync
import AWSMobileClient

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            
            AWSMobileClient.default().initialize { [weak self, window] (userState, error) in
                guard let self = self else { return }

                if let error = error {
                    print("### AWSMobileClient.initialize.error", error.localizedDescription)
                    return
                }
                
                if let userState = userState {
                    switch (userState) {
                    case .signedIn:
                        self.showHomeView(in: window) // ホームを開く
                    case .signedOut:
                        self.showSignInView(in: window) // ログインページを開く
                    default:
                        AWSMobileClient.default().signOut()
                        self.showSignInView(in: window) // ログインページを開く
                    }
                }
            }
            
            AWSMobileClient.default().addUserStateListener(self) { [weak self, window] (userState, info) in
                guard let self = self else { return }

                switch (userState) {
                case .signedIn:
                    self.showHomeView(in: window) // ホームを開く
                case .signedOut, .signedOutUserPoolsTokenInvalid:
                    self.showSignInView(in: window) // ログインページを開く
                default:
                    AWSMobileClient.default().signOut()
                    self.showSignInView(in: window) // ログインページを開く
                }
            }
        }
    }
    
    private func showHomeView(in window: UIWindow) {
        DispatchQueue.main.async {
            window.rootViewController = UIHostingController(rootView: HomeView())
            self.window = window
            window.makeKeyAndVisible()
        }
    }
    
    private func showSignInView(in window: UIWindow) {
        DispatchQueue.main.async {
            window.rootViewController = UIHostingController(rootView: SignInView(viewModel: SignInViewModel()))
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}
```


# SignInView

パスワードの初期化とサインアップは割愛させていただきます。

```SignInView.swift
import SwiftUI

struct SignInView: View {
    @ObservedObject var viewModel: SignInViewModel
    var body: some View {
        VStack(spacing: 20) {
            Spacer()

            Text("MyApp")
                .bold()
                .font(.title)

            Spacer()
            
            TextField("Username", text: $viewModel.userName)
                .autocapitalization(.none)
                .padding()
                .overlay(
                    RoundedRectangle(cornerRadius: 6)
                        .stroke(Color.gray, lineWidth: 1)
                )

            SecureField("Password", text: $viewModel.password)
                .padding()
                .overlay(
                    RoundedRectangle(cornerRadius: 6)
                        .stroke(Color.gray, lineWidth: 1)
                )
            
            Button(action: {
                print("### signinButton did tap")
                self.viewModel.signIn()
            }) {
                HStack {
                    Spacer()
                    Text("Sign in")
                    Spacer()
                }
                .padding()
            }
            .accentColor(.white)
            .background(Color.green)
            .cornerRadius(6)
            .alert(isPresented: $viewModel.showAlert) {
                Alert(title: Text("Sign in error"), message: Text(viewModel.errorMessage))
            }

            HStack {
                Spacer()
                Button(action: {
                    fatalError("forget password hasn't be implemented.") // 省略
                }) {
                    Text("Forget password?")
                }
            }
            
            Spacer()

            HStack {
                Spacer()
                Text("Don't have an account?")
                Button(action: {
                    fatalError("sign up hasn't be implemented.") // 省略
                }) {
                    Text("Sign up")
                }
                Spacer()
            }
            
            Spacer()
        }
        .padding(EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15))
    }
}

struct SignInView_Previews: PreviewProvider {
    static var previews: some View {
        SignInView(viewModel: SignInViewModel())
    }
}
```

# SignInViewModel
```SignInViewModel.swift
import Foundation

import AWSMobileClient

class SignInViewModel: ObservableObject {
    @Published var userName: String = ""
    @Published var password: String = ""
    @Published var showAlert = false
    @Published var errorMessage: String = ""

    func signIn() {
        AWSMobileClient.default().signIn(username: userName, password: password) { (result, error) in
            if let error = error {
                print("### signin error:", error.localizedDescription)
                self.showAlert = true
                self.errorMessage = error.localizedDescription
            }

            print("### Signin success")
        }
    }
}
```

# HomeView

```HomeView.swift
import SwiftUI

struct HomeView: View {
    var body: some View {
        Text("Home")
            .font(.title)
    }
}

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        HomeView()
    }
}
```

以上です。