【swift3入門】apiを叩いてTableViewに表示させる

はじめに

iOSアプリを開発する上で必須なのがTableViewを使えるようにすることではないかなと思います。
前回TableViewを作成しようでTableViewを作成して表示させる流れをまとめましたが、固定の結果を表示させるしかしませんでした。
今回はapiを叩いてデータを取得し、それを表示させるというところをやっていきましょう!

環境

  • Xcode 8.3.3
  • swift 3.1

apiについて

apiとは何かについて簡単にまとめると、普段webサイトを見るときはブラウザでは何をしてるかというとurlを叩いたらhtmlが返ってきます。htmlにはそのページの中身の情報とともにレイアウトをどうするか、クリックしたときにどのように動くかなどの情報が入っていて、それをブラウザが読み取ってhtmlにかかれているようにレイアウトを表示してくれて、普段見るようなきれいなページが表示されます。

一方、アプリではレイアウトの実装はアプリ側で実装されているので必要になるものは何を表示させるかのデータのみになります。

一旦例をあげてみます。

今回はqiitaのapiを使用することにします。
http://qiita.com/api/v2/items
このURLを叩いてみると、文字がひたすら並んでるだけのページが表示されると思います。

これはjsonと呼ばれるデータ形式で配列のようなものです。一般的なapiはこのjson形式でデータがわたってきます。
アプリではこれを受け取って、このデータを元にどのように表示させるかというのを実装していきます。

api通信をする

先程書いたように今回はhttp://qiita.com/api/v2/itemsこのapiを使用していきます。
これはqiitaの最新記事を取得するapiになっています。

前回、TableViewを作成しようで書いたコードの続きから書いていきます。

api通信のコード作成

ひとまずViewControllerのviewDidLoadに書いていくことにします。
まずこのように書きました。

最初にapiのURLをURL型に変換したのがurlです。URL型の初期化はString型のものを渡していてURL形式ではないものも渡せることが可能でURL型にならない場合もあるので返り値はURLのオプショナル型になっていますが、!で強制アンラップしています。オプショナル型を理解しようで!は絶対に使わないようにしましょうと書きましたが今回は開発段階でうまく行ってない場合にわかりやすいようにあえてクラッシュさせるために!を使っています。なので最終的にコードを整えるときに使わない書き方に書き換えます。

実際のapi通信は、今回URLSessionを使用します。
URLSession.shared.dataTaskでURLSessionTaskを作成し、resume()で実行することができます。

dataTaskでは、先程のurlを渡すのと、completionHandlerを引数に持ちます。ここで渡しているのはクロージャーと呼ばれるもので、関数を渡しています。

この部分がクロージャーです。
これは関数ですが、let taskの行を通ったときに実行されるわけではなくて、今回で言うと、api通信をしてレスポンスが返って来た段階でこの関数が実行されます。api通信は電波の状況やサーバーの状況によっていつ返ってくるか場合によって違うので、返ってきた段階で実行してほしい処理をクロージャーに定義して渡すようにしています。

今回はとりあえず通信して返ってきた値をちゃんと取得できてたかどうかを確認するためにとりあえずdata,response,errorをprintだけしています。

一回ビルドして実行してみます。
するとこのように出力されました。

これはhttp通信を許可してないために起こってしまいます。
http通信を許可する方法
これの通りに設定すれば大丈夫です。

設定した結果、以下のように出力されました

できてそうな雰囲気です!
dataに返ってきたデータ入っていますが、printしただけではOptional(444480 bytes)とだけしか表示されません。

dataを変換する

apiから値を取得することができましたがただ取得しただけでは型がData型になっているので、JSONに変換しなければいけません。

まずは返り値のJSONがどのような形になっているのかを把握します。
ブラウザで先程のURLを叩いたときにブラウザ上で確認することができますが、

このようなかんじで表示されてしまうので確認しにくいです。

僕はいつもJSONの形式を確認する場合にRestlet Clientというツールを使用しています。

これを使用するとこのように階層がわかりやすいように表示されるのと、日本語もエンコードされて表示されるので便利です。

ではコードに書いていきます。

まずこのように書いていきます。
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)
これはJSONに変換するためのメソッドです。このメソッドはtryを付けて、do-catch構文を用いて書かなければいけません。

do-catch構文はエラー処理をするためのもので基本的にはdoの中に書かれている処理を行いますが、その途中にエラーを投げる部分を通ったときにcatchの処理が行われます。

このjsonをprintした結果が以下のようになりました。

長いので途中省略しましたが、Data型の場合と違い、ブラウザで叩いたときに返ってきたものと近いものになりました。

JSONをパースする

Data型からJSONに変換することができましたが、前回行ったのは全体をひとつの塊として認識しただけです。ここから、配列のより細かい構造がどうなっているかを記述していかなければなりません。この作業をパースといいます。

まずJSONの形式について説明します。

JSONの構造には大きく分けて2種類あって、それを組み合わせて何層にもわたるものになっていますが、一番上の階層から1つずつ見ていけば2種類のどちらかになるので1つずつ上から解析していけば大丈夫です。

以下のような形式です。

この形式は普通の配列に近いものでイメージしやすいのではないかなと思います。

この場合、型は[Any]として認識させます。

以下のような形式です。

この場合、型は[String: Any]として認識させます。

このように特定の型として認識させることをキャストするといいます。

実際にやってみる

上での2つを押さえた上で実際にパースしていきます。

まず、もう一度返り値の形式を確認していきます。

一番上の階層だけ表示させています。このような形式になっていて、これは①の形式になっています。

この場合、先程のコードはこのようになります

as! [Any] というのは[Any]型としてキャストするというものです。!がついた場合は[Any]型とキャストしようとしますが、できなかった場合はクラッシュします。

一方、as? [Any] という記法もあります。これは[Any]型としてキャストしようとしますが、できなかった場合はnilを返します。なので全体として返り値は[Any]?型になります。
本来はas!はクラッシュしてしまう危険性があるので使用しないようにしていますが、開発段階の場合は失敗した場合にわかりやすいようにあえて!を使うようにしています。最終的にコードを整えるときに書き直します。

ビルドしてみたら無事実行されました。print(json)したときに先ほど比べると

このように変化していて、一番上の括弧が()だったのが[]に変わっています。

これはわかりづらいですが、配列としてキャストされているということになります。
加えて、print("count: \(json.count)")というのも追加しましたが、配列として認識されているので.countが使えるようになっています。

次に1つ下の階層を見ていきます。

次のような構造になっています。

1つ分だけ広げて見ましたが、先程配列にした1つ1つの要素は同じ構造になっているので、全ての要素に対して同じ処理を行えばいいことになります。

次のように書き換えます。

このmapという関数は、jsonという配列のすべての要素に対して、この中身の処理を行い、それらを組み合わせた新しい配列を作成します。今回は[String: Any]型としてキャストさせる処理をしました。

試しに一番最初のtitleをプリントしてみました

print("title: \(articles[0]["title"])")

この書き方をまず説明すると、
①の形式の場合、指定の要素に対してのアクセスの仕方は配列の名前に[0]のように数字をいれた括弧を付けます。何番目か数えるときに0から数え始めるのに注意です。
②の形式の場合、Key-value形式といって、あるString型のキーに対して値が決まるのでアクセスしたい場合はキー名を指定します。なので今回、一番最初の記事のタイトルが取得したかったので、

articles[0]["title"]

というように書けば大丈夫でした。

json[0]["title"]

というのもコメントアウトで追記しておきましたが、これはコメントアウトを外した場合にエラーになってしまいます。
jsonは[Any]型としてキャストされているので、json[0]と書くことはできますが、json[0]["title"]と書くとエラーになってしまいます。

先程の

print("title: \(articles[0]["title"])")

の結果は下のようになりました。
title: Optional(Zabbixでのホスト自動登録)

このように出力されました。このようにOptional型となっています。

一方
print("hoge: \(articles[0]["hoge"])")
このようにprintしました。その結果は、

hoge: nil
となります。

これはhogeというキーの値は存在しないのでnilが返ってきます。このように存在しないキーを指定するかもしれない可能性があるので、存在するキーを指定して値が返ってきたとしても型はOptional型になっています。

これで、とりあえずapiから取得してきた記事一覧の記事ごとのタイトルを取り出せるようになりました。

TableViewに取得した記事を表示させる。

記事のデータを取得することができたのでここからは、取得したデータをTableViewに表示させていくことをやっていきましょう。

さっそくですが、このようなコードを書いていきます。ベースとなるものは前回のTableViewを作ろうのときと一緒です。

1つずつ説明していきます。

まず、このViewControllerに取得してきた、表示させたい記事の配列を保存できるようにarticlesというプロパティを作成します。

初期値は[]で空の配列を意味しています。

次に、この部分のself.articles = articlesで取得してきた記事を先程作成したarticlesプロパティに代入しています。

次にTableViewを表示させる部分です。
TableViewを表示させる流れで書き換える部分で最低限必要になるのは、

  • セルを何個表示させるか
  • セルの中身をどうするか

この2つです。

まず、セルの個数は、このようにarticlesの配列の要素数を返すようにしています。
こうすることで返ってきたデータの記事数分だけ勝手にセルの数が適用されます。

次にセルの中身をどうするかです。

このようにしました。
let article = articles[indexPath.row]

ここでは表示させたいindexPathの記事をarticlesから取得しています。

indexPathについては前回TableViewを作成しようで書いたのでわからない人はこの記事を読んで下さい。

次に、
let title = article["title"] as! String
でarticleのtitleキーに入ってる値を取得してString型でキャストしています。
それをbindDataに渡しています。
bindDataではセルのラベルに渡した文字列を表示させる役割をしているのでこれでタイトルが表示されるようになるはずです。

ここでビルドして実行してみます。

エラーは特にありませんでしたが、何も表示されません。

これはライフサイクルが原因です。

画面が表示されるまでの流れを整理します。

1、ViewDidLoad articles = []

2、task.resume() articles = []

3、TableViewが作成完了 articles = []

4、apiから返ってきて代入 articles = [article1, article2, …]

このようにapiから値が返ってくる頃にはTableViewが作成完了されてしまっているので何も表示されなくなってしまいます。

ということで、apiから値が返ってきてarticlesプロパティに代入する際にTableViewを再描画させる必要があります。

そのために呼ぶメソッドはtableView.reloadData()でしたね。

これを、self.articles = articlesの次の行に追加すればとりあえず動作はしますが、この書き方で書いてしまうと、もし他のメソッドでarticlesを代入した際にその部分にもtableView.reloadData()を書き加えないといけなくなってしまいます。
この場合、コード量が増えてしまうのと書き忘れるおそれがあるというデメリットがあります。

なのでどのようにするのがいいかというと、articlesプロパティの値が書き換わったときにTableViewをリロードさせるという処理にするのがよいです。

これはどのように書くかというと、

このように書くことができます。

このように書き換えてもう一度実行してみたら、このように表示されました。

しかし、最初真っ白な画面になって、時間がたったら先程のように表示されました。それに、

このように出力されました。

これはTableViewを表示するスレッドがmainスレッドではないのが原因です。

apiで値を取得してパースしてarticlesに代入するまでの流れはバックグラウンドのスレッドで実行されます。

スレッドというのは本来プログラミングをして実行するときには上から1行ずつ順番に実行されていくと思います。これがmainスレッドです。

このようにhttp通信が入る場合返ってくるタイミングが毎回ばらばらなので、返ってきたタイミングで処理が始まりますが。そのときにmainスレッドで行われていたものはそのまま実行しながら並行して処理が始まります。これがバックグラウンドスレッドになります。

Viewを作る処理はメインスレッドで行わないといけないというルールがあり、今回はそれに反しているのが原因でした。
今の実装だと、値が返ってきてからarticlesに代入してtableViewをリロードして表示されるまでの流れがバックグラウンドスレッドになってしまっていたので、tableViewをリロードする処理をメインスレッドに戻してあげれば大丈夫です。

このように書き換えました。

このように書き換えることで無事正常に動作するものが作ることができました!

一旦キリの良いところまでできましたので、コードを整えるのと、途中であえて使っていた!を直していきたいと思います。

以下のように変更しました。

fileprivateはそのファイル内でしか使わないようにするもので、公開する範囲を狭めることで予期せぬエラーを防いだり出来きたり、仕様変更の際にコードを書き換えやすいなどのメリットがあります。
guard letやnil合体演算子を使うのはオプショナル型を理解しように書いてあるので確認してください。

この部分ですが、mapをflatMapを使う書き方に変えました。

この部分ではarticleがキャストに失敗したときにnilが返ってきます。flatMapではnilを取り除いた配列を返してくれます。
なので、予期せぬ型のデータが入ったとしてもその値は省いてくれるので安全に扱うことができます。

これでコードがある程度整いました。
今回は記事データをapiから取得して表示させるまでの簡単な機能を実装しただけなのでコード量も少なく全てViewControllerに記述しました。
これぐらいではそこまで気にならないですが新しくどんどん機能が追加された場合にコード量が多くなってわかりづらくなってきます。この際に役割ごとにファイルを切り分けるという作業が必要になってきます。

次回以降では、機能追加と共にファイルの切り分け方もやっていこうと思います。

今回のコードはこちらでも確認できるのでよかったらご確認ください。

SNSでもご購読できます。

コメント

コメントを残す

*