Swift

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

はじめに

iOSアプリを開発する上で必須なスキルが、TableViewを使えるようにすることではないかなと思います。
前回こちらの記事でTableViewを作成して表示させる流れをまとめました。

【Swift入門】初心者でもわかる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となっています。

前回の記事で書いたコードの続きから書いていきます。

【Swift入門】初心者でもわかるTableViewを実装しようこんばんは。 今回はアプリ作成に欠かせないのに加えて、特有な設定が多くて初心者が挫折しやすいTableViewを作成する手順をまとめてい...

api通信のコード作成

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


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のオプショナル型となるので!で強制アンラップしています。

以下の記事では絶対に使わないようにしましょうと書きましたが、今回は開発段階でうまくいかない場合にわかりやすいようにあえてクラッシュさせるためにを使っています。

なので最終的にコードを整えるときに使わない書き方に直します。

【Swift入門】オプショナル型を理解しようswiftの大きな特徴といえばOptional型ですね。 ですが初心者にとっては、Optional型というと、 Optionalって...

実際のapi通信は、今回URLSessionを使用します。

api通信をするにあたって便利なライブラリがいくつもありますが、今回はライブラリを使用せずに実装していきます。
理由としては、今回はapi通信の流れを理解していくという目的があり、ライブラリを使用してしまうと便利がゆえにその部分を理解せずとも使えてしまうので、あえて使用しませんでした。

とはいえ、実際に開発していく上で、何かしらのライブラリを使うのが一般的ではあるので、今回の基本を理解した上で用途にあったものを使用するのがいいと思います。

ということで今回は、URLSessionを使用していきますが、大まかな使い方としては、
URLSession.shared.dataTaskURLSessionTaskを作成し、resume()で実行することができます。

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


{data, response, error in
    print("data: \(data)")
    print("response: \(response)")
    print("error: \(error)")
}

この部分がクロージャーです。

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

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

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


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通信を許可してないために起こってしまいます。
これを解決する方法を以下の記事で説明しているのでよかったら参考にしてください。

【swift】XcodeでiOSアプリのhttp通信を許可する方法XcodeでiOSアプリを開発する上で、iOS9以降ではデフォルトではhttps通信しか許可されていないのでhttp通信ができません。今回はApp Transport Securityという設定を変更して、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はレスポンスヘッダーが表示されています。

errornilなので特にエラーが無いことがわかります。

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の処理が行われます。

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


(
      {
      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形式で扱うことはできますが、ここで定義した定数jsonAny型となっています。
この段階ではjsonがどういった構造になっているかまでは変換してくれません。
なのでここからはこのjsonがどういった構造になっているかを具体的な型にキャストしていきます。キャストというのは型変換のことを表します。

キャストに関してはこちらの記事でまとめましたので、もしわからない人は参考にしてください。

【Swift入門 文法編】型キャスト(as, as!, as?)をマスターしようこの記事ではSwiftの基本ある、キャストについての基本事項を解説していきます。 プログラミングが初心者も、他の言語をやっていてSwi...

実際に始める前に、まず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については

前回にこちらの記事で書いたのでわからない人はこちらを読んでください。

【Swift入門】初心者でもわかるTableViewを実装しようこんばんは。 今回はアプリ作成に欠かせないのに加えて、特有な設定が多くて初心者が挫折しやすいTableViewを作成する手順をまとめてい...

次に、


let title = article["title"] as! String

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

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

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

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

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

1、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 letnil合体演算子については、こちらの記事にまとめてあるのでわからない人は確認してください。

【Swift入門】オプショナル型を理解しようswiftの大きな特徴といえばOptional型ですね。 ですが初心者にとっては、Optional型というと、 Optionalって...

let articles = json.flatMap { (article) -> [String: Any]? in
    return article as? [String: Any]
} // flatMapに書き換え

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


{ (article) -> [String: Any]? in
    return article as? [String: Any]
}

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

これでコードがある程度整いましたので、ここで完成にします。

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

具体的には、

  • Modelを作成して、articles[[String: Any]]型として扱っていたのを、[Article]型とする。
  • ViewModelを作成して、api通信の部分を切り出す。

というようなことが挙げられます。

ViewModelModelが出てきましたが、今回はMVVMを採用していきたいと思っています。

Modelに関してはこちらの記事で解説しています。

【swift入門】Modelを作成しようはじめに 前回はこちらの記事でapiからデータ取得をしてその結果をTableViewに出力するところまで行いました。 https:/...

ViewModelに関してはこちらの記事で解説しています。

【swift3入門】ViewModelを作成しようはじめに 前回まででTableViewのある程度の機能が実装できました。 ここで機能拡張はおいて設計について考えていきます。 アーキ...

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