はじめに
iOSアプリを開発する上で必須なスキルが、TableView
を使えるようにすることではないかなと思います。
前回こちらの記事でTableView
を作成して表示させる流れをまとめました。
しかし、この記事では固定の結果を表示させるしかしませんでした。
なので今回はapiを叩いてデータを取得し、それを表示させるというところをやっていきましょう!
環境
- Xcode 8.3.3
- swift 3.1
Xcode9 swift4でも問題なく動作するかと思います。
apiについて
まずapi
とは何かについて簡単にまとめると、普段webサイトを見るときはブラウザでは何をしてるかというとurlを叩いたらhtmlが返ってきます。
htmlにはそのページの中身の情報とともに、さらにhtml内に書かれているcss、js、画像ファイルなどをさらにダウンロードしてきて、ブラウザが読み取ってhtmlにかかれているようにレイアウトを表示してくれて、普段見るようなきれいなページが表示されます。
それに対して、アプリではレイアウトの実装はアプリ側で実装されているので必要になるものは何を表示させるかのデータのみになります。
実際にどのようなものか見ていきましょう。
今回はqiitaのapiを使用することにします。
http://qiita.com/api/v2/items
このURLを叩いてみると、文字がひたすら並んでるだけのページが表示されると思います。
これはjson
と呼ばれるデータ形式で配列のようなものです。一般的なapiはこのjson形式
でデータがわたってきます。
昔はxml
というhtmlに似たような形式が主流でしたが、最近ではjson形式
が一般的です。
アプリではこのようなjson
を受け取って、このデータを元にどのように表示させるかというのを実装していきます。
api通信をする
先程書いたように、今回はhttp://qiita.com/api/v2/itemsこちらのapiを使用していきます。
これはqiitaの最新記事を取得するapiとなっています。
前回の記事で書いたコードの続きから書いていきます。
api通信のコード作成
ひとまずViewController
のviewDidLoad
に書いていくことにします。
まずこのように書きました。
override func viewDidLoad() {
super.viewDidLoad()
let url: URL = URL(string: "http://qiita.com/api/v2/items")!
let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {data, response, error in
print("data: \(data)")
print("response: \(response)")
print("error: \(error)")
})
task.resume() //実行する
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell") //前回書いたコード
}
最初にapi
のURLをURL型
に変換したのがurl
です。
URL型
の初期化は文字列を渡しますが、URL形式ではない文字列を渡した場合にnil
が帰ってくるのでURLのオプショナル型となるので!
で強制アンラップしています。
以下の記事で!
は絶対に使わないようにしましょうと書きましたが、今回は開発段階でうまくいかない場合にわかりやすいようにあえてクラッシュさせるために!
を使っています。
なので最終的にコードを整えるときに使わない書き方に直します。
実際のapi通信は、今回URLSession
を使用します。
api通信をするにあたって便利なライブラリがいくつもありますが、今回はライブラリを使用せずに実装していきます。
理由としては、今回はapi通信の流れを理解していくという目的があり、ライブラリを使用してしまうと便利がゆえにその部分を理解せずとも使えてしまうので、あえて使用しませんでした。
とはいえ、実際に開発していく上で、何かしらのライブラリを使うのが一般的ではあるので、今回の基本を理解した上で用途にあったものを使用するのがいいと思います。
ということで今回は、URLSession
を使用していきますが、大まかな使い方としては、
URLSession.shared.dataTask
でURLSessionTask
を作成し、resume()
で実行することができます。
dataTask
では、先程のurl
を渡すのと、completionHandler
を引数に持ちます。ここで渡しているのはクロージャーと呼ばれるもので、関数を渡しています。
{data, response, error in
print("data: \(data)")
print("response: \(response)")
print("error: \(error)")
}
この部分がクロージャーです。
これは関数ですが、let task
の行を通ったときに実行されるわけではなくて、今回で言うと、api通信をしてレスポンスが返って来た段階でこの関数が実行されます。api通信は電波の状況やサーバーの状況によっていつ返ってくるか場合によって違うので、返ってきた段階で実行してほしい処理をクロージャーに定義して渡すようにしています。
今回はとりあえず通信して返ってきた値をちゃんと取得できてたかどうかを確認するために、とりあえずdata
,response
,error
をprint
だけしています。
一回ビルドして実行してみます。
するとこのように出力されました。早速エラーです。
2017-09-13 01:57:43.020 SampleApp[2441:11283755] App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.
data: nil
response: nil
error: Optional(Error Domain=NSURLErrorDomain Code=-1022 "The resource could not be loaded because the App Transport Security policy requires the use of a secure connection." UserInfo={NSUnderlyingError=0x618000057c70 {Error Domain=kCFErrorDomainCFNetwork Code=-1022 "(null)"}, NSErrorFailingURLStringKey=http://qiita.com/api/v2/items, NSErrorFailingURLKey=http://qiita.com/api/v2/items, NSLocalizedDescription=The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.})
これはhttp通信を許可してないために起こってしまいます。
これを解決する方法を以下の記事で説明しているのでよかったら参考にしてください。
設定した結果、以下のように出力されました。
data: Optional(444480 bytes)
response: Optional( { URL: http://qiita.com/api/v2/items } { status code: 200, headers {
"Cache-Control" = "max-age=0, private, must-revalidate";
Connection = "keep-alive";
"Content-Type" = "application/json; charset=utf-8";
Date = "Tue, 12 Sep 2017 16:59:51 GMT";
Etag = "W/\"f964a20580dc254076f9598a85652d24\"";
Link = "<http://qiita.com/api/v2/items?page=1>; rel=\"first\", <http://qiita.com/api/v2/items?page=2>; rel=\"next\", <http://qiita.com/api/v2/items?page=12042>; rel=\"last\"";
"Rate-Limit" = 60;
"Rate-Remaining" = 57;
"Rate-Reset" = 1505238912;
Server = nginx;
"Total-Count" = 240833;
"Transfer-Encoding" = Identity;
Vary = Origin;
"X-Request-Id" = "efdbc538-085c-449f-a7dc-28bf185675fa";
"X-Runtime" = "0.413863";
} })
error: nil
できてそうな雰囲気です!
data
に返ってきたデータ入っていますが、print
しただけではOptional(444480 bytes)
とだけしか表示されません。
response
はレスポンスヘッダーが表示されています。
error
はnil
なので特にエラーが無いことがわかります。
dataを変換する
api
から値を取得することができましたがただ取得しただけでは型がData型
になっているので、JSON
に変換しなければいけません。
まずは返り値のJSON
がどのような形になっているのかを把握します。
ブラウザで先程のURLを叩いたときにブラウザ上で確認することができますが、
[{"rendered_body":"\u003cp\u003eRake 12.1.0で導入された did you mean 対応を試します。\u003c/p\u003e\n\n\u003cul\u003e\n\u003cli\u003eRakefile\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cdiv class=\"code-frame\" data-lang=\"ruby\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003cspan\u003e\u003c/span\u003e\u003cspan class=\"n\"\u003edesc\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"hoge\"\u003c/span\u003e\n\u003cspan class=\"n\"\u003etask\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"hoge\"\u003c/span\u003e \u003cspan class=\"k\"\u003edo\u003c/span\u003e\n \u003cspan class=\"nb\"\u003eputs\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"hoge\"\u003c/span\u003e\n\u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\n\u003cspan class=\"n\"\u003edesc\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"hige\"\u003c/span\u003e\n\u003cspan class=\"n\"\u003etask\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"hige\"\u003c/span\u003e \u003cspan class=\"k\"\u003edo\u003c/span\u003e\n \u003cspan class=\"nb\"\u003eputs\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"hige\"\u003c/span\u003e\n\u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\n\u003cspan class=\"n\"\u003edesc\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"foo\"\u003c/span\u003e\n\u003cspan class=\"n\"\u003etask\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"foo\"\u003c/span\u003e \u003cspan class=\"k\"\u003edo\u003c/span\u003e\n \u003cspan class=\"nb\"\u003eputs\u003c/span\u003e \u003cspan class=\"s2\"\u003e\"foo\"\u003c/span\u003e\n\u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\n\u003cul\u003e\n\u003cli\u003e実行結果\u003c/li\u003e\n\u003c/ul\u003e\n\n\u003cdiv class=\"code-frame\" data-lang=\"sh\"\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre\u003e\u003cspan\u003e\u003c/span\u003e$ rake -T\nrake foo \u003cspan class=\"c1\"\u003e# foo\u003c/span\u003e\nrake hige \u003cspan class=\"c1\"\u003e# hige\u003c/span\u003e\nrake hoge \u003cspan class=\"c1\"\u003e# hoge\u003c/span\u003e\n$ rake foo\nfoo\n$ rake fooa\nrake aborted!\nDon\u003cspan class=\"s1\"\u003e't know how to build task '\u003c/span\u003efooa\u003cspan class=\"s1\"\u003e' (see --tasks)\u003c/span\u003e\n\u003cspan class=\"s1\"\u003eDid you mean? foo\u003c/span\u003e\n\n\u003cspan class=\"s1\"\u003e(See full trace by running task with --trace)\u003c/span\u003e\n\u003cspan class=\"s1\"\u003e$ rake hege\u003c/span\u003e\n\u003cspan class=\"s1\"\u003erake aborted!\u003c/span\u003e\n\u003cspan class=\"s1\"\u003eDon'\u003c/span\u003et know how to build task \u003cspan class=\"s1\"\u003e'hege'\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003esee --tasks\u003cspan class=\"o\"\u003e)\u003c/span\u003e\nDid you mean? hige\n hoge\n\n\u003cspan class=\"o\"\u003e(\u003c/span\u003eSee full trace by running task with --trace\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/pre\u003e\u003c/div\u003e\u003c/div\u003e\n\n\u003ch2\u003e\n\u003cspan id=\"雑感\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E9%9B%91%E6%84%9F\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e雑感\u003c/h2\u003e\n\n\u003cp\u003eべんり\u003c/p\u003e\n\n\u003ch2\u003e\n\u003cspan id=\"外部資料\" class=\"fragment\"\u003e\u003c/span\u003e\u003ca href=\"#%E5%A4%96%E9%83%A8%E8%B3%87%E6%96%99\"\u003e\u003ci class=\"fa fa-link\"\u003e\u003c/i\u003e\u003c/a\u003e外部資料\u003c/h2\u003e\n\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://www.hsbt.org/diary/20170912.html#p01\" rel=\"nofollow noopener\" target=\"_blank\"\u003eRake 12.1.0 をリリースした - HsbtDiary(2017-09-12)\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://github.com/ruby/rake/pull/221\" rel=\"nofollow noopener\" target=\"_blank\"\u003eAdded did you mean to rake by xtina-starr · Pull Request #221 · ruby/rake\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"http://qiita.com/yuki24/items/feb72ee1f280434d5a5f\" id=\"reference-39fccbf030ee353bb112\"\u003eスペルミスでエラーが出たら、正しい名前を教えてくれる gem を作った - Qiita\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"http://qiita.com/tbpgr/items/835b3ac168304cba9b7b#_reference-6860fadc9851c9849ade\" id=\"reference-3736db89ece1740e74a6\"\u003eRuby | did_you_mean gem でスペルミスを検出+ did_you_mean gem のコードリーディング - Qiita\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n","body":"Rake 12.1.0で導入された did you mean 対応を試します。\n\n* Rakefile\n\nruby\ndesc \"hoge\"\ntask \"hoge\" do\n puts
このようなに改行がなかったり、エンコードされてたりしているので、確認しにくいです。
僕はいつもJSON
の形式を確認する場合にRestlet Clientというツールを使用しています。
これを使用すると、このように階層がわかりやすいように表示されるのと、日本語もデコードされて表示されるので便利です。
ではコードに書いていきます。
override func viewDidLoad() {
super.viewDidLoad()
let url: URL = URL(string: "http://qiita.com/api/v2/items")!
let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {data, response, error in
do {
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)
print(json)
}
catch {
print(error)
}
})
task.resume()
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
}
まずこのように書いていきます。
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)
これはJSON
に変換するためのメソッドです。このメソッドはtry
を付けて、do-catch構文
を用いて書かなければいけません。
do-catch構文
はエラー処理をするためのもので基本的にはdo
の中に書かれている処理を行いますが、その途中にエラーを投げる部分を通ったときにcatch
の処理が行われます。
このjson
をprint
した結果が以下のようになりました。
(
{
body = "\n\U30e1\U30fc\U30eb\U30b5\U30fc\U30d0\U306b\U30e1\U30fc\U30eb\U304c\U305f\U307e\U3063\U3066\U305d\U3046\U306a\U6642(inode\U304c\U3069\U3069\U3063\U3068\U5897\U3048\U305f\U308a)\U306b\U4fbf\U5229\U3001grep\U3057\U305f\U308a\U3057\U306a\U304f\U3066\U6e08\U3080\U3002\n\nCentOS 6 \U3067\U306f\U4ee5\U4e0b\U3067\U30d1\U30c3\U30b1\U30fc\U30b8\U3092\U30a4\U30f3\U30b9\U30c8\U30fc\U30eb\U3067\U304d\U308b\U3002base repo\U306a\U306e\U3067\U7279\U306b\U30ea\U30dd\U30b8\U30c8\U30ea\U8ffd\U52a0\U3068\U3044\U3046\U4e0d\U8981\U3002\n\nshell-session\n$ yum install postfix-perl-scripts\n \n\n![qshape.PNG](https://qiita-image-store.s3.amazonaws.com/0/54494/ece997a5-c904-bd59-9772-07617969c08d.png)\n\n\U8a73\U3057\U304f\U306f [Postfix \U30dc\U30c8\U30eb\U30cd\U30c3\U30af\U5206\U6790](http://www.postfix-jp.info/trans-2.1/jhtml/QSHAPE_README.html#deferred_queue)\n\n---\n\n### \U5bfe\U51e6\n\n**MailDir**\U5f62\U5f0f \U3067 \U30b9\U30d1\U30e0\U304c\U5927\U91cf\U306b\U5c4a\U3044\U305f\U3068\U304b\U3001\U9001\U4fe1\U3067\U304d\U307e\U305b\U3093\U3067\U3057\U305f\U30e1\U30fc\U30eb\U304c\U5927\U91cf\U306b\U305f\U307e\U3063\U3066\U3044\U305f\U3089\n\U3082\U3057\U304f\U306f\U3001\U30e1\U30fc\U30eb\U4ee5\U5916\U3067\U3082\U5927\U91cf\U306e\U30d5\U30a1\U30a4\U30eb\U6d88\U3059\U3068\U304d\U3068\U304b\n\n shell-session\n$ /bin/rm -rf /<\U30e1\U30fc\U30eb\U30c7\U30a3\U30ec\U30af\U30c8\U30ea/ new/*\n
\n\U3060\U3068\U30d5\U30a1\U30a4\U30eb\U6570\U306b\U3088\U3063\U3066\U306f\U6b7b\U306c\U306e\U3067\Uff08\U5f15\U6570\U5927\U6749\U554f\U984c\Uff1a /bin/rm: Argument list too long \Uff09\n\n shell-session\n$ find /<\U30e1\U30fc\U30eb\U30c7\U30a3\U30ec\U30af\U30c8\U30ea\Uff1e/ >
︙
長いので途中省略しましたが、Data型
の場合と違い、ブラウザで叩いたときに返ってきたものと近いものになりました。
JSONをパースする
Data型
からJSON
に変換することができましたが、前回行ったのは全体をひとつの塊として認識しただけです。ここから、配列のより細かい構造がどうなっているか把握して、必要な部分を取り出すという処理を記述していかなければなりません。この処理をパースといいます。
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments)
先程のこの処理をしただけでは、JSON形式
で扱うことはできますが、ここで定義した定数json
はAny型
となっています。
この段階ではjson
がどういった構造になっているかまでは変換してくれません。
なのでここからはこのjson
がどういった構造になっているかを具体的な型にキャストしていきます。キャストというのは型変換のことを表します。
キャストに関してはこちらの記事でまとめましたので、もしわからない人は参考にしてください。
実際に始める前に、まずJSON
の形式について説明します。
JSON
の構造には大きく分けて2種類あって、それを組み合わせて何層にもわたるものになっています。1階層ごとに見ていけば2種類のどちらかになるので、1階層ごとに上から解析していけば大丈夫です。
①
以下のような形式です。
[
{...}
{...}
{...}
]
この形式は普通の配列に近いものでイメージしやすいのではないかなと思います。
この場合、型は[Any]
としてキャストします。
②
以下のような形式です。
{
"hoge": "hogehoge",
"bb": 2,
"hogehoge": null
}
この場合、型は[String: Any]
としてキャストします。
実際にやってみる
上での2つを押さえた上で実際にパースしていきます。
まず、もう一度返り値の形式を確認していきます。
一番上の階層だけ表示させています。このような形式になっていて、これは①の形式になっています。
この場合、先程のコードはこのようになります
override func viewDidLoad() {
super.viewDidLoad()
let url: URL = URL(string: "http://qiita.com/api/v2/items")!
let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {data, response, error in
do {
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! [Any] //この部分を書き換える
print(json)
print("count: \(json.count)") //追加
}
catch {
print(error)
}
})
task.resume()
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
}
as! [Any]
というのは[Any]型
としてキャストするというものです。!
がついた場合は[Any]型
とキャストしようとしますが、できなかった場合はクラッシュします。
一方、as? [Any]
という記法もあります。これは[Any]型
としてキャストしようとしますが、できなかった場合はnil
を返します。なので全体としての返り値は[Any]?型
になります。
本来はas!
はクラッシュしてしまう危険性があるので使用しないようにしていますが、開発段階の場合は失敗した場合にわかりやすいようにあえて!
を使うようにしています。最終的にコードを整えるときに書き直します。
ビルドしてみたら無事実行されました。print(json)
したときに先ほど比べると
//Before
(
{・・・}
{・・・}
{・・・}
︙
)
//After
[
{・・・}
{・・・}
{・・・}
︙
]
このように変化していて、一番上の括弧が()
だったのが[]
に変わっています。
これはわかりづらいですが、配列としてキャストされているということになります。
加えて、
print("count: \(json.count)")
というのも追加しましたが、配列として認識されているので.count
が使えるようになっています。
次に1つ下の階層を見ていきます。
次のような構造になっています。これはQiitaの1記事分の記事のデータにあたります。
1つ分だけ広げて見ましたが、先程配列にした1つ1つの要素はすべて記事のデータで構造は同じになっているので、全ての要素に対して同じ処理を行えばいいことになります。
次のように書き換えます。
override func viewDidLoad() {
super.viewDidLoad()
let url: URL = URL(string: "http://qiita.com/api/v2/items")!
let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {data, response, error in
do {
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! [Any]
let articles = json.map { (article) -> [String: Any] in
return article as! [String: Any]
}
print("title: \(articles[0]["title"])")
print("hoge: \(articles[0]["hoge"])")
// json[0]["title"] エラーになる
}
catch {
print(error)
}
})
task.resume()
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
}
このmap
という関数は、json
という配列のすべての要素に対して、引数に渡すクロージャーの処理を行い、それらを組み合わせた新しい配列を返します。
今回は要素全てに対して、[String: Any]型
としてキャストしたものを返すようにしました。
let articles = json.map { (article) -> [String: Any] in
return article as! [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]
はAny型
なのでjson[0]["title"]
と書くとエラーになってしまいます。
先程の
print("title: \(articles[0]["title"])")
の結果は下のようになりました。
title: Optional
このように出力されました。このようにOptional型
となっています。
一方
print("hoge: \(articles[0]["hoge"])")
このようにprint
しました。その結果は、
hoge: nil
となります。
これはhoge
というキーは存在しないのでnil
が返ってきます。このように存在しないキーを指定する可能性があるので、存在するキーを指定して値が返ってきたとしても型はOptional型
になっています。
これで、とりあえずapiから取得してきた記事一覧の記事ごとのタイトルを取り出せるようになりました。
TableViewに取得した記事を表示させる。
記事のデータを取得することができたのでここからは、取得したデータをTableView
に表示させていくことをやっていきましょう。
さっそくですが、このようなコードを書いていきます。ベースとなるものは前回のTableView
を作ろうのときと一緒です。
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
var articles: [[String: Any]] = [] //追加
override func viewDidLoad() {
super.viewDidLoad()
let url: URL = URL(string: "http://qiita.com/api/v2/items")!
let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {data, response, error in
do {
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! [Any]
let articles = json.map { (article) -> [String: Any] in
return article as! [String: Any]
}
self.articles = articles //追加
}
catch {
print(error)
}
})
task.resume()
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return articles.count //変更
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TableViewCell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell") as! TableViewCell
let article = articles[indexPath.row] //追加
let title = article["title"] as! String //追加
cell.bindData(text: "title: \(title)") //変更
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("section: \(indexPath.section) index: \(indexPath.row)")
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
return
}
}
1つずつ説明していきます。
まず、このViewController
に取得してきた、表示させたい記事の配列を保存できるようにarticles
というプロパティを作成します。
初期値は[]
で空の配列を意味しています。
次に、この部分の
self.articles = articles
で取得してきた記事を先程作成したarticles
プロパティに代入しています。
let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {data, response, error in
do {
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! [Any]
let articles = json.map { (article) -> [String: Any] in
return article as! [String: Any]
}
self.articles = articles //追加
}
catch {
print(error)
}
})
次にTableView
を表示させる部分です。
TableView
を表示させる流れで書き換える部分で最低限必要になるのは、
- セルを何個表示させるか
- セルの中身をどうするか
この2つです。
まず、セルの個数は、このようにarticles
の配列の要素数を返すようにしています。
こうすることで返ってきたデータの記事数分だけ勝手にセルの数が適用されます。
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return articles.count //変更
}
次にセルの中身をどうするかです。
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TableViewCell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell") as! TableViewCell
let article = articles[indexPath.row] //追加
let title = article["title"] as! String //追加
cell.bindData(text: "title: \(title)") //変更
return cell
}
このようにしました。
let article = articles[indexPath.row]
ここでは表示させたいindexPath
の記事をarticles
から取得しています。
indexPath
については
前回にこちらの記事で書いたのでわからない人はこちらを読んでください。
次に、
let title = article["title"] as! String
でarticle
のtitleキー
に入ってる値を取得してString型
でキャストしています。
それをbindData
に渡しています。
bindData
ではセルのラベルに渡した文字列を表示させる役割をしているのでこれでタイトルが表示されるようになるはずです。
ここでビルドして実行してみます。
エラーは特にありませんでしたが、何も表示されません。
これはライフサイクルが原因です。
画面が表示されるまでの流れを整理します。
ViewDidLoad articles = []
2、task.resume() articles = []
3、TableView
が作成完了 articles = []
4、apiから返ってきて代入 articles = [article1, article2, ...]
このようにapiから値が返ってくる頃にはTableView
が作成完了されてしまっているので何も表示されなくなってしまいます。
ということで、apiから値が返ってきてarticles
プロパティに代入する際にTableView
を再描画させる必要があります。
TableView
を再描画するために呼ぶメソッドは
tableView.reloadData()
です。
これを、
self.articles = articles
の次の行に追加すればとりあえず動作はしますが、この書き方で書いてしまうと、もし他のメソッドでarticles
を代入した際にその部分にもtableView.reloadData()
を書き加えないといけなくなってしまいます。
この場合、コード量が増えてしまい、管理がしにくくなりバグを生みやすいというデメリットがあります。
なのでどのようにするのがいいかというと、articles
プロパティの値が書き換わったときにTableView
をリロードさせるという処理にするのがよいです。
これはどのように書くかというと、
var articles: [[String: Any]] = [] {
didSet {
tableView.reloadData()
}
}
このように書くことができます。
こうすることで、articles
に値がset
されるたびにdidSet
の中が呼ばれます。
このように書き換えてもう一度実行してみたら、このように表示されました。
しかし、最初真っ白な画面になって、時間がたったら先程のように表示されるまでにわりと時間がかかりました。
それに加えて、
2017-09-14 17:22:09.783 SampleApp[70763:12094241] This application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.
Stack:(
0 CoreFoundation 0x00000001106bcb0b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x000000010d962141 objc_exception_throw + 48
2 CoreFoundation 0x0000000110725625 +[NSException raise:format:] + 197
3 Foundation 0x000000010d65b17b _AssertAutolayoutOnAllowedThreadsOnly + 105
4 Foundation 0x000000010d65af0f -[NSISEngine _optimizeWithoutRebuilding] + 61
5 Foundation 0x000000010d48a966 -[NSISEngine optimize] + 108
6 Foundation 0x000000010d658ef4 -[NSISEngine performPendingChangeNotifications] + 84
7 UIKit 0x000000010def7762 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1787
8 QuartzCore 0x0000000113486904 -[CALayer layoutSublayers] + 146
9 QuartzCore 0x000000011347a526 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 370
10 QuartzCore 0x000000011347a3a0 _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 24
11 QuartzCore 0x0000000113409e92 _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 294
12 QuartzCore 0x0000000113436130 _ZN2CA11Transaction6commitEv + 468
13 QuartzCore 0x000000011343642c _ZN2CA11Transaction14release_threadEPv + 214
14 libsystem_pthread.dylib 0x00000001119a950f _pthread_tsd_cleanup + 544
15 libsystem_pthread.dylib 0x00000001119a9249 _pthread_exit + 152
16 libsystem_pthread.dylib 0x00000001119a77cd pthread_attr_getschedpolicy + 0
17 libsystem_pthread.dylib 0x00000001119a71ed start_wqthread + 13
)
このように出力されました。
これはTableView
を表示する処理のスレッドがmainスレッド
ではないのが原因です。
apiで値を取得してパースしてarticles
に代入するまでの流れはバックグラウンドのスレッドで実行されます。
スレッドというのは本来プログラミングをして実行するときには上から1行ずつ順番に実行されていくと思います。これがmainスレッド
です。
このようにhttp通信
が入る場合、返ってくるタイミングが毎回ばらばらなので、結果が返ってきたタイミングで処理が始まりますが、そのときにmainスレッド
で行われていたものはそのまま実行しながら並行して処理が始まります。これがバックグラウンドスレッドになります。
View
を作る処理はメインスレッドで行わないといけないというルールがあり、今回はそれに反しているのが原因でした。
今の実装だと、値が返ってきてからarticles
に代入してtableView
をリロードして表示されるまでの流れがバックグラウンドスレッドになってしまっていたので、tableView
をリロードする処理をメインスレッドに戻してあげれば大丈夫です。
このように書き換えました。
let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {data, response, error in
do {
let json = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as! [Any]
let articles = json.map { (article) -> [String: Any] in
return article as! [String: Any]
}
DispatchQueue.main.async() { () -> Void in
self.articles = articles
}
}
catch {
print(error)
}
})
task.resume()
このように書き換えることで無事正常に動作するものが作ることができました!
一旦キリの良いところまでできましたので、コードを整えるのと、途中であえて使っていた!
を直していきたいと思います。
以下のように変更しました。
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
fileprivate var articles: [[String: Any]] = [] { // fileprivateを追加
didSet {
tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
fetchArticles() // メソッドに切り出し
initTableView() // メソッドに切り出し
}
private func fetchArticles() {
guard let url: URL = URL(string: "http://qiita.com/api/v2/items") else { return }
let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {data, response, error in
do {
guard let data = data else { return } // guard letに書き換え
guard let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? [Any] else { return } // guard letに書き換え
let articles = json.flatMap { (article) -> [String: Any]? in
return article as? [String: Any]
} // flatMapに書き換え
DispatchQueue.main.async() { () -> Void in
self.articles = articles
}
}
catch {
print(error)
}
})
task.resume()
}
private func initTableView() {
tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return articles.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TableViewCell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell") as! TableViewCell
let article = articles[indexPath.row]
let title = article["title"] as? String ?? "" // nil合体演算子を使用
cell.bindData(text: "title: \(title)")
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("section: \(indexPath.section) index: \(indexPath.row)")
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
return
}
}
fileprivate
はそのファイル内でしか使わないようにするもので、公開する範囲を狭めることで予期せぬエラーを防いだり出来きたり、仕様変更の際にコードを書き換えやすいなどのメリットがあります。
guard let
やnil合体演算子
については、こちらの記事にまとめてあるのでわからない人は確認してください。
let articles = json.flatMap { (article) -> [String: Any]? in
return article as? [String: Any]
} // flatMapに書き換え
この部分ですが、map
をflatMap
を使う書き方に変えました。
{ (article) -> [String: Any]? in
return article as? [String: Any]
}
この部分ではarticle
がキャストに失敗したときにnil
が返ってきます。flatMap
ではnil
を取り除いた配列を返してくれます。
なので、予期せぬ型のデータが入ったとしてもその値は省いてくれるので安全に扱うことができます。
これでコードがある程度整いましたので、ここで完成にします。
今回は記事データをapiから取得して表示させるまでの簡単な機能を実装しただけなのでコード量も少なく全てViewController
に記述しました。
これぐらいではそこまで気にならないですが新しくどんどん機能が追加された場合にコード量が多くなってわかりづらくなってきます。この際に役割ごとにファイルを切り分けるという作業が必要になってきます。
具体的には、
Model
を作成して、articles
を[[String: Any]]型
として扱っていたのを、[Article]型
とする。ViewModel
を作成して、api通信の部分を切り出す。
というようなことが挙げられます。
ViewModel
とModel
が出てきましたが、今回はMVVM
を採用していきたいと思っています。
Model
に関してはこちらの記事で解説しています。
ViewModel
に関してはこちらの記事で解説しています。
今回のコードはこちらでも確認できるのでよかったらご確認ください。