見出し画像

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

前回作成したUIは、リストを下にスクロールしても、約20個のランチャー情報しか含まれていません。これは、ランチャー情報にページ番号が付けられており、最初のページしか取得していないためです。

今回は、「カーソルベース」の読み込みシステムを使用して、ランチャー情報のリスト全体を読み込みます。

1. マスタービューの編集

(1) MasterViewController.swift を以下のように編集。

・MasterViewController.swift

import UIKit
import SDWebImage
import Apollo

class MasterViewController: UITableViewController {
    // ランチ情報
    var launches = [LaunchListQuery.Data.Launch.Launch]()

    // セクション種別
    enum ListSection: Int, CaseIterable {
        case launches
        case loading
    }

    // UI
    var detailViewController: DetailViewController? = nil

    // リスト読み込み
    private var lastConnection: LaunchListQuery.Data.Launch? // ランチコネクション
    private var activeRequest: Cancellable? // リクエスト中

    // ビューロード時に呼ばれる
    override func viewDidLoad() {
        super.viewDidLoad()
        self.loadMoreLaunchesIfTheyExist()
    }
  
    // ビュー表示時に呼ばれる
    override func viewWillAppear(_ animated: Bool) {
        clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
        super.viewWillAppear(animated)
    }

    // 画面遷移先にデータを渡す
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // セクションインデックスの取得
        guard let selectedIndexPath = self.tableView.indexPathForSelectedRow else {
            return
        }
       
        // セクション種別の取得
        guard let listSection = ListSection(rawValue: selectedIndexPath.section) else {
            assertionFailure("Invalid section")
            return
        }
       
        switch listSection {
        // ランチ
        case .launches:
            guard
                let destination = segue.destination as? UINavigationController,
                let detail = destination.topViewController as? DetailViewController else {
                    assertionFailure("Wrong kind of destination")
                    return
            }
        
            // データを渡す
            let launch = self.launches[selectedIndexPath.row]
            detail.launchID = launch.id // ランチID
            self.detailViewController = detail
        case .loading:
            assertionFailure("Shouldn't have gotten here!")
        }
    }
   
    // セグエを実行するかどうかを決定
    override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
        // 選択インデックスパスの取得
        guard let selectedIndexPath = self.tableView.indexPathForSelectedRow else {
            return false
        }
             
        // セクション種別の取得
        guard let listSection = ListSection(rawValue: selectedIndexPath.section) else {
            assertionFailure("Invalid section")
            return false
        }
           
        switch listSection {
        // ランチ
        case .launches:
            return true
        // ローディング
        case .loading:
            self.tableView.deselectRow(at: selectedIndexPath, animated: true)
 
            if self.activeRequest == nil {
                self.loadMoreLaunchesIfTheyExist()
            }
            self.tableView.reloadRows(at: [selectedIndexPath], with: .automatic)
        
            // セグエ実行なし
            return false
        }
    }

    // セクション数の取得時に呼ばれる
    override func numberOfSections(in tableView: UITableView) -> Int {
        return ListSection.allCases.count
    }

    // セクションの行数の取得時に呼ばれる
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // セクション種別の取得
        guard let listSection = ListSection(rawValue: section) else {
            assertionFailure("Invalid section")
            return 0
        }
          
        switch listSection {
        // ランチ
        case .launches:
            return self.launches.count 
        // ローディング
        case .loading: 
            if self.lastConnection?.hasMore == false {
                return 0
            } else {
                return 1
            }
        }
    }

    // セルの取得時に呼ばれる
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // セルの取得
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        // セルの初期化
        cell.imageView?.image = nil
        cell.textLabel?.text = nil
        cell.detailTextLabel?.text = nil

        // セクション種別の取得
        guard let listSection = ListSection(rawValue: indexPath.section) else {
            assertionFailure("Invalid section")
            return cell
        }
      
        switch listSection {
        // ランチ
        case .launches:
            let launch = self.launches[indexPath.row]
            cell.textLabel?.text = launch.mission?.name // テキスト
            cell.detailTextLabel?.text = launch.site // 詳細テキスト

            // イメージ
            let placeholder = UIImage(named: "placeholder")!
            if let missionPatch = launch.mission?.missionPatch {
                cell.imageView?.sd_setImage(
                with: URL(string: missionPatch)!, placeholderImage: placeholder)
            } else {
                cell.imageView?.image = placeholder
            }
        // ローディング           
        case .loading:
            // テキスト
            if self.activeRequest == nil {
                cell.textLabel?.text = "Tap to load more"
            } else {
                cell.textLabel?.text = "Loading..."
            }
        }
        return cell
    }
  
    // エラーアラートの表示
    private func showErrorAlert(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)
    }
    
    // ランチ情報の読み込み
    private func loadMoreLaunches(from cursor: String?) {
        self.activeRequest = Network.shared.apollo.fetch(
            query: LaunchListQuery(cursor: cursor)) { [weak self] result in
            // selfの準備
            guard let self = self else {
                return
            }
            self.activeRequest = nil
   
            // 後処理
            defer {
                self.tableView.reloadData()
            }
   
            switch result {
            // 成功時
            case .success(let graphQLResult):
                // ランチコネクションとランチ情報の取得
                if let launchConnection = graphQLResult.data?.launches {
                    self.lastConnection = launchConnection
                    self.launches.append(contentsOf:
                        launchConnection.launches.compactMap { $0 })
                }
    
                // エラー表示
                if let errors = graphQLResult.errors {
                    let message = errors
                        .map { $0.localizedDescription }
                        .joined(separator: "\n")
                    self.showErrorAlert(title: "GraphQL Error(s)",
                        message: message)
                }
            // エラー時
            case .failure(let error):
                // エラー表示
                self.showErrorAlert(title: "Network Error",
                    message: error.localizedDescription)
            }
        }
    }
   
    // 情報存在時にランチ情報の読み込み
    private func loadMoreLaunchesIfTheyExist() {
        guard let connection = self.lastConnection else {
            // ランチコネクションがないので最初から読み込む
            self.loadMoreLaunches(from: nil)
            return
        }
       
        guard connection.hasMore else {
            // ランチ情報はもうない
            return
        }       
        self.loadMoreLaunches(from: connection.cursor)
    }
}

2. LaunchList.graphql の編集

変数をGraphQLクエリに渡すには、「$名前:型」を使用して変数を定義する必要があります。

・(GraphiQL)

query LaunchList($cursor:String) {
  launches(after:$cursor) {

3. 実行

ビルドしてアプリを実行すると、次のようにランチ情報が表示されます。「Tap to load more」をタップすると、ランチ情報を追加で読み込みます。

画像1


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