はじめに
前回はこちらの記事でapiからデータ取得をしてその結果を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
のように作成するのが本来よいのかもしれませんが、今回はこのように必要そうなものだけをピックアップしたのでこのような構造にしました。
せっかくいろいろ追加してみたので現状セルにタイトルしか表示させていないのでその記事を書いたユーザー名とそのユーザーのサムネ画像が出るようにしてみようかなと思いますが、今回はここまでにして次回に回そうかなと思います。
こちらの記事でセルのレイアウトをいいかんじにしていきます。
今回のコードはこちらでも確認できるのでよかったらご確認ください。