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」をタップすると、ランチ情報を追加で読み込みます。
この記事が気に入ったらサポートをしてみませんか?