Swift

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

はじめに

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

あわせて読みたい
【Swift入門】TableViewを作成しようこんばんは。 今回はアプリ作成に欠かせないTableViewを作成する手順をまとめていきます。 前回GitHubにiOSアプリの新規リ...

しかし、固定の結果を表示させるしかしませんでした。
なので今回は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になっています。

前回の

あわせて読みたい
【Swift入門】TableViewを作成しようこんばんは。 今回はアプリ作成に欠かせないTableViewを作成する手順をまとめていきます。 前回GitHubにiOSアプリの新規リ...

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

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

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

あわせて読みたい
【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)とだけしか表示されません。

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

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

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

以下のような形式です。

[
    {...}
    {...}
    {...}
]

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

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

以下のような形式です。

{
    "hoge": "hogehoge",
    "bb": 2,
    "hoge": 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つ下の階層を見ていきます。

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

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]["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を作成する手順をまとめていきます。 前回GitHubにiOSアプリの新規リ...

次に、

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をリロードさせるという処理にするのがよいです。

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

var articles: [[String: Any]] = [] {
    didSet {
        tableView.reloadData()
    }
}

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

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

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

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に記述しました。
これぐらいではそこまで気にならないですが新しくどんどん機能が追加された場合にコード量が多くなってわかりづらくなってきます。この際に役割ごとにファイルを切り分けるという作業が必要になってきます。

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

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