Swift

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

はじめに

前回はこちらの記事でapiからデータ取得をしてその結果をtableViewに出力するところまで行いました。

あわせて読みたい
【swift入門】apiを叩いてTableViewに表示させるはじめに iOSアプリを開発する上で必須なスキルが、TableViewを使えるようにすることではないかなと思います。 前回こちらの記事...

ここから新しく機能を追加していきたいところですが、その前に前回までの構造ではいくつかの問題点があるのでそれを修正していこうと思います。

前回までの問題点

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

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

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

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

今回は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

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

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

こちらの記事でセルのレイアウトをいいかんじにしていきます。

あわせて読みたい
【swift3入門】Xibファイルのレイアウトを設定しようはじめに 前回Modelを作成しようでモデルを作成しapiから取得したデータからある程度の項目を抽出して使用できるようにしました。 今...

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