【SwiftUI】ログイン機能と、セッション切れたらログイン画面に戻る機能作ってみた

個人開発をちゃんとプロダクトとしてカタチにしていく上で、
ログイン機能で利用者を制限しつつ、利用者別に情報を制御するのが前提になりやすいので、
ログイン画面と、その後のセッションが有効期限切れになった際に
追い出す（ログイン画面に戻す）機能の実装方法を実際に作って確認してみました。。

## 開発環境
MacOS Monterey 12.2.1
Xcode Version 13.4.1
Swift Version 5.6.1

## その他ライブラリなど
CocoaPodsで以下を取り込んでいます。

・Alamofire5 -> HTTPリクエスト送信する(APIを呼び出す)ため
・PromiseKit -> API呼び出しを同期的に処理するため

## 画面イメージ
今回の画面イメージでは
ログイン画面でログイン（セッション取得）→メイン画面→サブ画面に遷移、
サブ画面のボタンからAPIを呼び出し、セッション有効であればメッセージ表示、
セッション無効になっていればメイン、サブ画面を閉じてログイン画面に戻す、
という挙動を確認します。

#### ログイン画面
<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126315/0dc2e3a7-f643-3318-64af-c25c0e6e1d37.png">

#### ログイン後のメイン画面
<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126315/1e165e34-11a4-3ec5-ddd5-0d067ad1bf0d.png">

#### ログイン後のサブ画面
（ボタン下のメッセージは「Call API」ボタン押下して、セッション有効でレスポンスが返却された際に表示）
<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126315/2864b320-2d88-d1f0-f242-84e45ffb0ffb.png">


## コード

前提として、今回はNavigationViewを使った画面遷移を実装しています。

コードの全文は以下を参照ください。
（プッシュ履歴がキレイじゃないですが、Githubに上げるようにしておくとこういうトコとで便利）

https://github.com/yuta-yama-1123/LoginTest

リポジトリの内容から、個人的にポイントになる部分を抜き出して転記します。

#### ログイン画面
```swift
struct ContentView: View {
  // API呼び出し
  var callApi = CallAPIModel()
  
  @State var email = "test@test.com"
  @State var password = "secret"
  @State var result = ""
  
  // ログイン成否
  @State var isAuthenticated: Bool = false
  
  var body: some View {
      NavigationView {
        VStack(spacing: 20) {
            ...
            Button(action: {
                login()
            }) {
                Text("ログイン")
            }
              ...
            Text(result)
            NavigationLink(
              destination: MainView(),
              isActive: $isAuthenticated) {
                EmptyView()
            }
        }
          .navigationBarBackButtonHidden(true)
          .navigationBarHidden(true)
      }
  }
  
  // ログイン処理
  func login() {
    // PromiseKitの記法(firstly~done~catch)
    firstly {
      // 認証API呼び出し
      callApi.postLogin(
        email: email,
        password: password
      )
    }.done { loggedIn in
      if (loggedIn) {
        self.isAuthenticated.toggle()
      }
    }.catch { error in
      print(error)
      result = "ログインに失敗しました"
    }
  }
}
```

流れとしてはログインボタン押下時に`login()`メソッド呼び出し
→サーバに入力内容をPOSTし、レスポンスで認証成否を判定
→ログイン成功の場合、画面遷移制御用のBool変数(`isAuthenticated`)をtoggle(→メイン画面に遷移)
→ログイン失敗の場合、ログインボタンの下に失敗メッセージを表示
となるようにしています。

#### メイン画面
```swift
struct MainView: View {
  @Environment(\.dismiss) private var dismiss

  @State private var isShowSubView = false

  // セッションのステータス管理(true: セッション有効期限切れ)
  @State private var sessionExpired = false

  var body: some View {
    NavigationView {
      VStack {
        Text("MainView")
          .padding()
        Button( action: { isShowSubView.toggle() }) {
            Text("Move to SubView")
        }
        NavigationLink(
          destination: SubView(
            sessionExpired: $sessionExpired),
          isActive: $isShowSubView) {
          EmptyView()
      }
        .onAppear{
          if (sessionExpired) {
            dismiss()
          }
        }
    }
      .navigationBarTitle("")
      .navigationBarHidden(true)
      .navigationBarBackButtonHidden(true)
  }
    .navigationBarTitle("")
    .navigationBarHidden(true)
    .navigationBarBackButtonHidden(true)
  }
}
```

メイン画面自体ではAPI呼び出しを行っていないですが、
セッション無効時に画面を閉じるための判定用フラグ`sessionExpired`を設定し、
画面表示時`onAppear`にこのフラグがtrueとなっている場合に画面を閉じる処理を入れています。
また、上記のフラグをサブ画面にバインドし、サブ画面でのAPI呼び出しで
セッション無効となった場合にメイン画面の制御にも伝播できるようにしています。

#### サブ画面
```swift
struct SubView: View {
  @Environment(\.dismiss) private var dismiss

  // メイン画面にセッション状態を伝播するためのバインドパラメータ
  @Binding var sessionExpired: Bool

  var callApi = CallAPIModel()

  @State var message = ""

  var body: some View {
      VStack {
        Text("SubView")
          .padding()
        Button( action: { touch() }) {
            Text("Call API")
        }
        Text(message)
      }
    .navigationBarTitle("")
    .navigationBarHidden(true)
    .navigationBarBackButtonHidden(true)
  }
  
  func touch() {
    firstly {
      callApi.touch()
    }.done { touched in
      if (touched) {
        callApi.printCookies()
        message = "セッションは有効です"
      } else {
        callApi.printCookies()
        // 画面を閉じ、親画面にセッション有効期限切れを伝える
        sessionExpired = true
        dismiss()
      }
    }.catch { error in
      print(error)
      callApi.printCookies()
      // 画面を閉じ、親画面にセッション有効期限切れを伝える
      sessionExpired = true
      dismiss()
    }
  }
```

ポイントとしてはAPI呼び出し時にセッションが有効であれば、
ボタンの下にその旨のメッセージを表示するだけですが、
想定外のレスポンス（セッション有効期限切れ）となった場合に、
メイン画面でも触れたセッション状態の判定用フラグをセットし、
自画面を閉じる処理を行なっています。
サブ画面が閉じる→メイン画面に戻る→メイン画面のonAppearでフラグ判定
→セッション無効のためメイン画面も閉じる→ログイン画面に戻る
という流れが実現します。

（ここでのtouchで呼び出しているAPIはただサーバにアクセスして
レスポンスを受け取って戻ってくるだけのもので、
リクエスト・レスポンスもログイン画面での書き方としては大差ありません。）


![LoginTest.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126315/9a1e8b7c-742e-5888-e324-e7d8dce5ea21.gif)
Gifで動作イメージ貼ってみました。
個人的に、サブ画面閉じて〜メイン画面に戻って、閉じて〜みたいな流れが
もっさりした動きになるのはヤダなぁと思っていたんですが、実際作ってみるとそんなことはなく
SubViewからログイン画面にスパッと戻ってる感じが伝わればよいかな、と。


## ハマりポイントなど
#### NavigationBarが画面遷移のたびに積み重なっていく
画面遷移の動きよりも、NavigationBarの表示制御で困りました。
とりあえずNavigationBarが表示されないよう、随所に下記を記載しています。

```swift
    .navigationBarTitle("")
    .navigationBarHidden(true)
    .navigationBarBackButtonHidden(true)
```

#### セッション(Cookie)管理のロジックは書かなくてよかった
ログインの際に取得したセッションをCookieに詰めて、
そのあとAPI呼び出しのたびにCookieから読み出して、、、
ということを書かないといけないだろうなーと思っていたんですが、
Alamofireを使っていれば、特に意図せずHttpCookieObjectを使って
いい感じに保存・取得してくれていました。ラクでいいですねー


今回の記事ではAPI呼び出し(Alamofire利用部分)については
特に詳細に触れるべきポイントもなさそうなので割愛していますが、
上に貼ったリポジトリのリンクにその部分も上げてますのでご興味あれば参照下さい。

今回は以上です。


<font color="lightgray">
この辺ができるようになって、
いよいよ個人開発で具体的な完成形に向かって何か作れそうな気がしてきた、
っていう感覚を得られたのが大きい。
</font>