見出し画像

Apollo iOS チュートリアル (6)

今回は、ミューテーションから返された認証トークンを利用して、旅行の予約およびキャンセルを行います。

1. ApolloClient

これまで、ApolloClient はURL指定のみで利用してきました。

・Network.swift

import Apollo
import Foundation

class Network {
    static let shared = Network()
  
    private(set) lazy var apollo = ApolloClient(
        url: URL(string: "http://localhost:8080/graphql")!)
}

ApolloClient は、内部で NetworkTransport を使用します。デフォルトでは、HTTPNetworkTransport を作成して、サーバとのHTTP通信を処理します。リクエストが送信される前に何か処理する必要がある場合、
HTTPNetworkTransportPreflightDelegate を使います。

2. AppClientへの認証トークンの付加

AppClientに認証トークンを付加するには、Network.swiftを次のように編集します。

・Network.swift

import Apollo
import Foundation
import KeychainSwift

class Network {
    static let shared = Network()
  
    private(set) lazy var apollo: ApolloClient = {
        let httpNetworkTransport = HTTPNetworkTransport(
            url: URL(string: "https://apollo-fullstack-tutorial.herokuapp.com/")!)
        httpNetworkTransport.delegate = self // デリゲートの付加
        return ApolloClient(networkTransport: httpNetworkTransport)
    }()
}

extension Network: HTTPNetworkTransportPreflightDelegate {
    // 送信すべきかどうかの取得
    func networkTransport(_ networkTransport: HTTPNetworkTransport,
        shouldSend request: URLRequest) -> Bool {
        return true
    }
   
    // 送信前に呼ばれる
    func networkTransport(_ networkTransport: HTTPNetworkTransport,
        willSend request: inout URLRequest) {
        // 認証トークンの付加
        let keychain = KeychainSwift()
        if let token = keychain.get(LoginViewController.loginKeychainKey) {
            request.addValue(token, forHTTPHeaderField: "Authorization")
        }
    }
}

3. Alertヘルパーメソッドの追加

(1) Xcodeのメニュー「File → New → File → Swift File」で、新規Swiftファイルを作成し、UIViewController+Alert.swiftという名前を指定。

(2) UIViewController+Alert.swift を以下のように編集。

・UIViewController+Alert.swift

import UIKit
import Apollo

extension UIViewController {
    // アラートの表示
    func showAlert(title: String, message: String) {
        let alert = UIAlertController(title: title,
            message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        self.present(alert, animated: true)
    }
   
    // エラーアラートの表示
    func showAlertForErrors(_ errors: [GraphQLError]) {
        let message = errors
            .map { $0.localizedDescription }
            .joined(separator: "\n")
        self.showAlert(title: "GraphQL Error(s)", message: message)
    }
}

4. GraphiQLでのBookTripミューテーションの確認

(1) GraphiQLで、[DOCS]タブを開き、bookTripsミューテーションを確認。

画像1

このミューテーションを使用して、一度に複数の旅行を予約して元に戻すことができます。

・ success: 予約が成功したかどうか。
・ message: ユーザーに表示する文字列。
・ launches: 現在ユーザーが予約しているランチのリスト。

(2) GraphiQLで、以下のミューテーションを入力。

・(GraphiQL)

mutation BookTrip($id:[ID]!) {
  bookTrips(launchIds:$id) {
    success
    message
  }
}

(3) 「QUERY VARIABLES」で、IDの配列を追加。

・(GraphiQL)

{"id": ["25"]}

(4) 「HTTP HEADERS」で、認証ヘッダを追加して、ログイン時に受け取ったトークンを通過させる。

・(GraphiQL)

{"Authorization": "YOUR_TOKEN"}

(5) 再生ボタンをクリック。
予約したばかりの旅行に関する情報が返されます。

【注意】
「Cannot read property 'id' of null」というエラーが表示される場合は、パススルーしたトークンに基づいてユーザーが見つからなかったことを意味します。 認証ヘッダが適切にフォーマットされていることと、実際にログインしていることを確認してください。

画像2


このミューテーションを使用すると、同時に必要な旅行をいくつでも予約できます。 ただし今回のアプリでは、一度に1つの旅行しか予約できません。単一のオブジェクトのみを取得できるように、変更します。

(1) GraphiQLで、以下のミューテーションを入力。

・(GraphiQL)

mutation BookTrip($id:ID!) {
  bookTrips(launchIds:[$id]) {
    success
    message
  }
}

(2) 「QUERY VARIABLES」で、IDを追加。

・(GraphiQL)

{"id": "25"}

(3) 再生ボタンをクリック。
先ほどと同じ結果が表示されます。

5. XcodeでのBookTripミューテーションの実装

(1) Xcodeの LaunchList.graphql と同じフォルダに新規ファイル BookTrip.graphql を生成し、先程のミューテーションを貼り付ける。

(2) アプリをビルドし、API.swift に新規ミューテーションが追加されていることを確認。

(3) DetailViewControll.swift に予約とキャンセルのメソッドを追加。
キャンセルは、まだprint()のみになります。

・DetailViewController.swift

    // 旅行の予約
    private func bookTrip(with id: GraphQLID) {
        Network.shared.apollo.perform(mutation: BookTripMutation(id: id)) {
            [weak self] result in
           
            // selfの取得
            guard let self = self else {
                return
            }
           
            switch result {
            // 成功時
            case .success(let graphQLResult):
                if let bookingResult = graphQLResult.data?.bookTrips {
                    if bookingResult.success {
                        self.showAlert(title: "Success!",
                            message: bookingResult.message ?? "Trip booked successfully")
                    } else {
                        self.showAlert(title: "Could not book trip",
                            message: bookingResult.message ?? "Unknown failure.")
                    }
                }

                // エラー表示
                if let errors = graphQLResult.errors {
                   self.showAlertForErrors(errors)
                }
            // エラー時
            case .failure(let error):
                // エラー表示
                self.showAlert(title: "Network Error",
                    message: error.localizedDescription)
            }
        }
    }
   
    // 旅行のキャンセル
    private func cancelTrip(with id: GraphQLID) {
        print("Cancel trip \(id)")
    }

(4) bookOrCancelTapped の今までprint()のみだった部分を、予約とキャンセルのメソッドに置き換える。

・DetailViewController.swift

        // 旅行の予約およびキャンセル
        if launch.isBooked {
            self.cancelTrip(with: launch.id)
        } else {
            self.bookTrip(with: launch.id)
        }

(5) 実行。
これで、旅行を予約するためのコードが実装できました。

画像3

6. GraphiQLでのCancelTripミューテーションの確認

(1) GraphiQLで、[DOCS]タブを開き、cancelTripsミューテーションを確認。

・ launchId: 予約ID。

(2) GraphiQLで、以下のミューテーションを入力。

・(GraphiQL)

mutation CancelTrip($id:ID!) {
  cancelTrip(launchId:$id) {
    success
    message
  }
}

(3) 「QUERY VARIABLES」で、IDの配列を追加。

・(GraphiQL)

{"id": "25"}

(4) 「HTTP HEADERS」で、認証ヘッダを追加して、ログイン時に受け取ったトークンを通過させる。

・(GraphiQL)

{"Authorization": "YOUR_TOKEN"}

(5) 再生ボタンをクリック。

7. XcodeでのCancelTripミューテーションの実装

(1) Xcodeの LaunchList.graphql と同じフォルダに新規ファイル CancelTrip.graphql を生成し、先程のミューテーションを貼り付ける。

(2) アプリをビルドし、API.swift に新規ミューテーションが追加されていることを確認。

(3) DetailViewControll.swift のcancelTrip()を次のように編集。

・DetailViewController.swift // 旅行のキャンセル

    private func cancelTrip(with id: GraphQLID) {
        Network.shared.apollo.perform(mutation: CancelTripMutation(id: id)) {
            [weak self] result in
           
            // selfの取得
            guard let self = self else {
                return
            }
           
            switch result {
            // 成功時
            case .success(let graphQLResult):
                // キャンセル結果の取得
                if let cancelResult = graphQLResult.data?.cancelTrip {
                    if cancelResult.success {
                        if cancelResult.success {
                            self.showAlert(title: "Trip cancelled",
                                message: cancelResult.message ?? "Your trip has been officially cancelled.")
                        } else {
                            self.showAlert(title: "Could not cancel trip",
                                message: cancelResult.message ?? "Unknown failure.")
                        }
                    }
                }

                // エラー表示
                if let errors = graphQLResult.errors {
                    self.showAlertForErrors(errors)
                }
            // エラー時
            case .failure(let error):
                // エラー表示
                self.showAlert(title: "Network Error",
                    message: error.localizedDescription)
            }
        }
    }

(4) 実行。
これで、旅行をキャンセルするためのコードが実装できました。

画像4

8. キャッシュポリシー

ApolloClientのfetch()は、パラメータの多くはデフォルトが提供されているため、自分で指定する必要があるのはクエリのみです。ただし、パラメータ cachePolicy は注意が必要です。

cachePolicy のデフォルト値は、returnCacheDataElseFetch です。これは、はじめに現在のキャッシュ(デフォルトではメモリ)を検索し、データが存在しない場合はネットワークから取得します。データが存在する場合、デフォルトの動作はローカルコピーを返し、不要な通信を防止します。これは望ましい動作ではない場合があります(特にミューテーションの実行後)。

使用できる cachePolicy はいくつかありますが、ネットワーク更新を強制する最も簡単な方法は、fetchIgnoringCacheData を使用することです。

loadLaunchDetails()での使用例は、次のとおりです。

private func loadLaunchDetails(forceReload: Bool = false) {
    guard
        let launchID = self.launchID,
        (forceReload || launchID != self.launch?.id) else {
        return
    }

    let cachePolicy: CachePolicy
    if forceReload {
        cachePolicy = .fetchIgnoringCacheCompletely
    } else {
        cachePolicy = .returnCacheDataElseFetch
    }
    Network.shared.apollo.fetch(query: LaunchDetailsQuery(id: launchID), cachePolicy: cachePolicy) {
        [weak self] result in

        <<省略>>
    }
}


この記事が気に入ったらサポートをしてみませんか?