この記事では、SwiftUIとDjango-REST-Framework(Django-AllAuth)でログイン・ログアウト機能の実装方法を紹介します。
◆動作検証環境
・XCode:12.1
・django-allauth:0.43.0
・DjangoRESTframework: 3.12.1
・django-rest-auth: 0.9.5
ログイン画面の構成と動作内容
まずは、iOSアプリのログイン画面を作成します。
ログイン画面の構成内容は以下のとおりです。
- email入力欄
- パスワード入力欄
- ログインボタン
- ログアウトボタン
- パスワード再設定用リンク
通常のログイン画面には、ログアウト用のボタンはありませんが、今回は動作の確認用として同じ画面設置して機能を確認します。
パスワード再設定用リンクは、django-rest-framworkのエンドポイントを使って再設定する方法ではなく、django-allauthで設定しているパスワード再設定ページ(WEBサイト)を表示するようにしています。
django-rest-framworkのエンドポイントを使って再設定する方法は、入力したメールに設定用のリンクが貼られそちらから、再設定用の画面に遷移する仕組みです。
シングルページのWEBサイトの場合、この遷移先にエンドポイントを使う再設定ページを作成できますが、モバイルアプリの場合はメール画面から、再度アプリの画面へ遷移する事ができないため、この方法としています。
動きとしては、email、パスワードを入力し登録ボタンをタップ。
- 入力が正しければ、ログイン成功とコンソールに表示
通常はログイン成功後の画面に遷移させます - 入力が正しくなければ、alertを出し不備に内容を知らせる。
ログアウトボタンをタップした際は、ログアウト用のエンドポイントにアクセスし、ログアウトします。
今回はログアウトのメッセージをコンソールに表示します。
入力のバリデーションは、django-rest-framwork側で行い、エラーがある場合は、responseのメッセージをalertで表示させます。
ログアウト登録画面のコーディング
ログイン用画面のstructは以下の内容です。
NavigationViewに含まれる画面となっており、親の画面はユーザー登録用画面をしています。
LoginView
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 83 84 85 86 87 88 89 90 91 92 93 94 95 | import SwiftUI struct LoginView: View { @EnvironmentObject var accountAPI: AccountAPI @State var eMail:String = "" @State var password:String = "" var body: some View { VStack{ Text("ログイン") Divider() .padding() VStack(alignment:.leading){ Text("e-mail") .padding(.leading) TextField("e-mailを入力", text:$eMail ) .keyboardType(.emailAddress) .autocapitalization(.none) .disableAutocorrection(false) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding() Text("password") .padding(.leading) SecureField("passwordを入力", text:$password, onCommit:{ self.edittingPassword = false }) .autocapitalization(.none) .disableAutocorrection(false) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding() } Divider() .padding() Button(action:{ accountAPI.makeLogin(email: eMail, password: password) }){ Text("ログイン") .foregroundColor(Color.white) .frame(width: 100, height: 40, alignment: .center) .background(Color.green) .cornerRadius(50) .padding() } Button(action:{ accountAPI.makeLogout() }){ Text("ログアウト") .foregroundColor(Color.white) .frame(width: 100, height: 40, alignment: .center) .background(Color.orange) .cornerRadius(50) .padding() } HStack{ Text("パスワードをお忘れの方→") Button(action: { self.accountAPI.isActivePasswordResetView = true }) { Text("パスワード再設定") } }.padding() } .alert(isPresented: $accountAPI.isAlertLogin){ Alert(title: Text("入力に不備があります"), message: Text(""" \(accountAPI.alertMessageEmailLogin) \(accountAPI.alertMessagePasswordLogin) \(accountAPI.alertMessageNonFieldLogin) """) ) } Spacer() } struct LoginView_Previews: PreviewProvider { static var previews: some View { LoginView() } } } |
45、55行目:
ログイン、ログアウトそれぞれのボタンのタップで、makeLogin() 、makeLogout() を利用します。
67行目:
isActivePasswordResetView = true とする事で、WEBビューを利用するパスワードのリセット画面を表示させます。このビューはStackNavigattionの含まれ、対象のフラグが立つと画面遷移します。
74行目:
メールアドレス、パスワード等に入力の不備があった際に、アラートで表示します。
AccountAPI.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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | import Foundation import Combine class BlogAPI: ObservableObject { @Published var tokenKey:String = "" @Published var isAlertLogin:Bool = false @Published var alertMessageEmailLogin:String = "" @Published var alertMessagePasswordLogin:String = "" @Published var alertMessageNonFieldLogin:String = "" func makeLogin(email theemail:String, password thepassword:String) { var statusCode:Int? // バックグラウンドではなくDispatchQueue.main.asyncで @Published変数に値を代入するためメソッド内に変数を定義する var messageEmail:String? var messagePassword:String? var messageNonFiled:String? var key:String? let eMail = theemail let password = thepassword let endpoint: String = "https://sample.com/rest-auth/login/" guard let url = URL(string: endpoint) else { print("Error: cannot create URL") return } var urlRequest = URLRequest(url: url) urlRequest.httpMethod = "POST" urlRequest.httpBody = "email=\(eMail)&password=\(password)".data(using: .utf8) let session = URLSession.shared let task = session.dataTask(with: urlRequest) { (data, response, error) in guard error == nil else { print("error calling POST") print(error!) return } guard let responseData = data else { print("Error: did not receive data") return } // JSONデータのパース do { guard let receivedData = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any] else { print("Could not get JSON from responseData as dictionary") return } print("The request is: " + receivedData.description) //発行されたTokenの取得 if let receivedkey = receivedData["key"] { key = receivedkey as? String print("The key is: \(receivedkey)") }else{ key = "キー取得失敗用" print("Could not get key as int from JSON") } //emailのresponce if let emails = receivedData["email"] as? [String]{ let email = emails.first! messageEmail = String(describing: email) }else{ messageEmail = "OK" } //pasword1のresponce if let passwords = receivedData["password"] as? [String]{ let password = passwords.first! messagePassword = String(describing: password) }else{ messagePassword = "OK" } //non fieldのresponce if let nonFields = receivedData["non_field_errors"] as? [String]{ let nonField = nonFields.first! messageNonFiled = String(describing: nonField) }else{ messageNonFiled = "OK" } } catch { print("error parsing response from POST") return } guard let response = response as? HTTPURLResponse else { print("Error: did not response data") return } print("The response code is \(response.statusCode)") statusCode = response.statusCode DispatchQueue.main.async { //正常にAPI接続できたとき(statusコードが200の時) if statusCode == 200{ self.tokenKey = key! print("login成功") //正常にAPI接続できなかった時(statusコードが200以外の時) }else{ if messageEmail != "OK"{ self.alertMessageEmailLogin = "Eメール:\(messageEmail!)" }else{ self.alertMessageEmailLogin = "" } if messagePassword != "OK"{ self.alertMessagePasswordLogin = "パスワード:\(messagePassword!)" }else{ self.alertMessagePasswordLogin = "" } if messageNonFiled != "OK"{ self.alertMessageNonFieldLogin = "パスワード:\(messageNonFiled!)" }else{ self.alertMessageNonFieldLogin = "" } //入力ミスのアラートを出すためのフラグをON self.isAlertLogin = true } } } task.resume() } //ここからログアウト用メソッド func makeLogout() { let endpoint: String = "https://sample.com/rest-auth/logout/" guard let url = URL(string: endpoint) else { print("Error: cannot create URL") return } let urlRequest = URLRequest(url: url) let session = URLSession.shared let task = session.dataTask(with: urlRequest) { (data, response, error) in guard error == nil else { print("error calling POST") print(error!) return } guard let responseData = data else { print("Error: did not receive data") return } do { guard let receivedData = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any] else { print("Could not get JSON from responseData as dictionary") return } print("The request is: " + receivedData.description) } catch { print("error parsing response from POST") return } } task.resume() } } |
ポイントは以下のとおりです。
ログイン処理の構成は、基本的なPOST接続と同様です。
60行目:
dataTask() で受け取ったdataをJSONSerialization.jsonObject() で、JSONデータをパースした際にログイン成功時に発行されるTokenを取得します。ユーザー登録や、ログイン時の入力情報に不備があった際に受け取るエラーメッセージと同じ方法で、["key"] と指定します。
104行目:
バックグラウンド下での@Published 変数の変更を避けるために、DispatchQueue.main.async を使いTokenキーを代入します。
*アプリの再起動時等にもログイン済みの状態を把握するために、UserDefaultなどでデバイスにTokenキーの保持、削除する方法も利用されます。
ログアウト処理の構成も、基本的なPOST接続と同様です。
163 行目:
dataTask() で受け取ったdataを確認のためにコンソールに表示します。ログアウト処理が正常に行われると、ログイン処理時に作成されたTokenが削除されます。
次回以降のログインは、新しいTokenが発行されそちらを利用するようになります。
ここでは、単純にログアウトするだけの機能ですが、実際はStatusコード200番だったら、指定のビューへ遷移する等の処理を加えます。
rest-authとall-authのログイン方法の違い
Django-restauthをモバイルアプリ等で使う場合、共通のデータベースをWEBアプリでも使用する場面は多いと思います。
WEBアプリでall-authでユーザー登録、ログインする場合
モバイルアプリでrest-authでユーザー登録、ログインする場合では少々違いがあるので、まとめてみます。
WEBアプリ All-Auth | モバイルアプリ Rest-Auth | |
ユーザー登録登録 | Tokenは発行されない | Tokenが発行される |
ログイン成功後 | Tokenは発行されない | Tokenが発行される *モバイルアプリでユーザー登録した場合、 同じTokenが発行される *WEBアプリでユーザー登録した場合、 新規のTokenが発行される |
ログアウト | モバイルアプリログイン時発行された tokenは削除されない | Tokenが削除される (次回のログイン時は新規のTokenが発行される) |
WEBアプリで登録されたユーザーが、モバイルでログインする → Tokenが発行される
モバイルアプリで登録されたユーザーがWEBアプリでログインする → Tokenは発行されない(パスワード認証)
ひとつのユーザーが、WEB、モバイル同時にログイン → できる
モバイルの場合、各エンドポイントの権限はserializerクラスで設定するが、Token認証にする場合は、ログイン時に発行されるTokenを利用する。
SwiftUIアプリの実践的なログイン・ログアウト処理の実装
ログイン等の認証機能のあるアプリケーションでは、通常一度ログインを行うと、ログアウト処理をするまでは専用のビューが表示される方法がよく利用されます。
例えば、アプリを開いた際に、
- ログイン済みのユーザーは → 会員用ビューに自動で遷移 → ログアウト処理を行うとログイン画面に遷移
- ログインしていないユーザーは → ユーザー登録またはログイン画面に遷移
というような動作です。
これまでに紹介したコードの内容だと、一度アプリを再起動してしまうと、Tokenの値が消えてしまいますので、そのような環境に対応できる形で対応します。
動作の内容は以下のとおりです。
- アプリを開く際にすでにログイン処理をしており、Tokenを発行している場合はログイン後(HOME画面)に遷移される。
Tokenが発行されていない場合は、ログイン画面が表示される。 - ログイン処理を行い、成功すると発行されたTokenを@Published変数に格納する。
- ログアウト処理を行うと、Tokenの値を””とする
- ライフライクルがアクティブからバックグラウンドに映る際に、Published変数に格納されたTokenの値をUserDefaultsを利用して、デバイスに保持する
(ログイン済みの場合→”XXXXXXXXX12345XXXXXX”, ログインしている場合→””となっている) - ライフライクルがバックグラウンドからアクティブに際に、UserDefaultsを利用してデバイスに保持されているTokenの値をPublished変数に格納する
①に戻る
アクティブ⇔バックグラウンドの際にTokenの値をデバイスから出し入れするため、アプリの再起動にも値を保持した状態となります。
上記の動きにするため下記のようなコードに編集します。
AccountAPI.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 | class AccountAPI: ObservableObject { @Published var tokenKey:String = "" ...中略 func makeLogin(email theemail:String, password thepassword:String) { var key:String? ...中略 do { guard let receivedData = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any] else { print("Could not get JSON from responseData as dictionary") return } print("The request is: " + receivedData.description) //発行されたTokenの取得 if let receivedkey = receivedData["key"] { key = receivedkey as? String } catch { print("error parsing response from POST") return } ...中略 DispatchQueue.main.async { if statusCode == 200{ self.tokenKey = key! self.isMoveToHomeView = true }else{ //入力ミスのアラートを出すための処理 } } } task.resume() } ...中略 |
ポイントは以下のとおり
28行目:
ログインが成功した時は、@Published変数にのTokenの値を入れる
UserDefaultsを使用する際はここで処理します。
29行目:
ログインした後に指定のビューに遷移するように、対象のフラグをtrueにする
AccountAPI.swift / ログアウト処理の部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class AccountAPI: ObservableObject { @Published var tokenKey:String = "" ...中略 func makeLogout() { ...中略 DispatchQueue.main.async { if statusCode == 200{ self.tokenKey = "" self.isMoveToLoginView = true } } } task.resume() } ...中略 |
ポイントは以下のとおり
10行目:
ログアウトがされた時は、@Published変数のTokenの値を””とする
UserDefaultsを使用する際はここで処理します。
11行目:
ログアウトした後にログインビューに遷移するように、対象のフラグをtrueにする
以上、SwiftUIとDjango-REST-Framework(Django-AllAuth)でのログイン・ログアウト機能の実装方法を紹介しました。