Swift

【swift3入門】Modelを作成しよう

はじめに

前回apiを叩いてTableViewに表示させるでapiからデータ取得をしてその結果をapiに出力するところまで行いました。
ここから新しく機能を追加していきたいところですが、その前にいくつかこの構造での問題点があるのでそこを修正していこうと思います。

前回までの問題点

問題点はどこにあるかというと、前回での取得した記事のデータであるarticles[[String: Any]]という型で扱っていたことです。

前回は記事のタイトルのみを表示させましたが今後の流れとして他の項目を表示させたりといった流れになってくると思います。
apiの返り値の構造から見てわかるようにすべての記事のデータ形式はほとんどおなじになっていてパターン化されています。

現状の[[String: Any]]型だと型的に前回実装したtitleでさえ確実に持っている保証はなく、毎回パースしてオプショナル型なのでアンラップしてという流れになってしまい、項目が増えれば増えるほどめんどくさい実装になってしまいます。

そこで今回は記事のデータはどういう構造なのかというのをしっかりと定義するということをしていきます。これがモデルです。

今回はStructを用いてArticleモデルを作成していきます。

Article.swiftの作成

まずは新しくArticle.swiftという名前でSwiftファイルを作成します。

とりあえず前回タイトルだけ表示させたので、titleのみを持つ、Articleモデルを作成して適用させていきます。

まず作成したArticle.swiftの中身はこのように書きました。

struct Article {
      let title: String

      init(json: [String: Any]) {
          title = json["title"] as? String ?? ""
      }

      init() {
          title = ""
      }
  }

今回structを用いて書きました。実装の手順を順番に説明していきます。

プロパティの定義

Articleモデルは何をプロパティをして持っているのかを定義していきます。

struct Article {
      let title: String
  }

このようになります。

イニシャライザの定義

プロパティが定義できたので、次は実際にどのような値が入るのかを定義していきます。
イニシャライザというのは初期化するための関数で、普段つかうメソッドは呼び出したら実行されますが、イニシャライザは初期化する際に自動的に呼ばれます。
今回でいうとプロパティを先程定義しましたが、titleはletで値が書き換わらないけど値は定義されてないのに型はString型なのでnilは入りません。なので矛盾してるようになりますが、初期化する際にそれらの値を入れた上でArticleがインスタンス化されるというようになっています。

struct Article {
      let title: String

      init(json: [String: Any]) {
          title = json["title"] as? String ?? ""
      }

      init() {
          title = ""
      }
  }

このようになりました。initというのがイニシャライザです。
ですが、今回2つ書いています。2つの違いというと引数があるかないかが異なってきます。

init(json: [String: Any]) {
      title = json["title"] as? String ?? ""
  }

こっちのほうjson: [String: Any]という引数を渡しています。
渡ってきたjsonに対してtitleにどのように値を入れるかを定義しています。
なので、入れてほしいデータを渡してそれに対してどのように当てはめるかをイニシャライザには書いています。

一方こちらは引数がありません。

init() {
      title = ""
  }

中身というとtitleに関しては空文字を代入しています。
データが渡ってきていないので空にして初期化しています。

ViewControllerを書き換える

これで一旦Articleモデルの作成は完了です。

次にViewControllerを書き換えていきましょう。

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

import UIKit

  class ViewController: UIViewController {

      @IBOutlet weak var tableView: UITableView!
      fileprivate var articles: [Article] = [] { // articlesの型を[[String: Any]]から変更
          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 jsonArray = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? [Any] else { return }
                  let articlesJson = jsonArray.flatMap { (json: Any) -> [String: Any]? in
                      return json as? [String: Any]
                  } // わかりやすいように一部命名を変更
                  let articles = articlesJson.map { (articleJson: [String: Any]) -> Article in
                      return Article(json: articleJson)
                  } // jsonからArticleに変換
                  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 // before: 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
      }
  }

articlesの型が[[String: Any]]型から[Article]型に書き換えました。

let articles = articlesJson.map { (articleJson: [String: Any]) -> Article in
      return Article(json: articleJson)
  } // JsonをArticleに変換

ここではarticlesJsonのそれぞれの要素の型は[String: Any]だったのでそれを先ほど作成したArticleに変換しています。
Article(json: articleJson)と書くことでarticleJsonを先程の引数ありのイニシャライザを使用して初期化することができます。

このようにArticleに変換することで

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 // before: let title = article["title"] as? String ?? ""
      cell.bindData(text: "title: \(title)")
      return cell
}

この部分がスッキリかけるようになりました。
Articleはtitleというプロパティを持っていて型はString型と保証されているのでアンラップもしなくてよくなります。

ここで一回実行してみましょう。
前回と同じように表示されるようになると思います。

Articleのプロパティを追加

前回簡単なモデルを作成することができました。
現状ではtitleしか入ってないので他のプロパティも追加していきたいと思います。

に使いそうなものをピックアップしてArticleモデルに追加して、以下のようにしてみました。

struct Article {
      let title: String
      let body: String
      let renderedBody: String
      let urlString: String
      let userId: String
      let profileImageUrlString: String

      init(json: [String: Any]) {
          title = json["title"] as? String ?? ""
          body = json["body"] as? String ?? ""
          renderedBody = json["rendered_body"] as? String ?? ""
          urlString = json["url"] as? String ?? ""
          if let user = json["user"] as? [String: Any] {
              userId = user["id"] as? String ?? ""
              profileImageUrlString = user["profile_image_url"] as? String ?? ""
          } else {
              userId = ""
              profileImageUrlString = ""
          }
      }

      init() {
          title = ""
          body = ""
          renderedBody = ""
          urlString = ""
          userId = ""
          profileImageUrlString = ""
      }
  }

userがさらに配列を持っていたので、新たにUserモデルを作成して、Articleのプロパティに

let user: User

のように作成するのが本来よいのかもしれませんが、今回はこのように必要そうなものだけをピックアップしたのでこのような構造にしました。

せっかくいろいろ追加してみたので現状セルにタイトルしか表示させていないのでその記事を書いたユーザー名とそのユーザーのサムネ画像が出るようにしてみようかなと思いますが、今回はここまでにして次回に回そうかなと思います。

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