[Swift]QiitaのAPIを叩いて記事を取得して表示するサンプルアプリを書いてみたシリーズの続き。
概要
[Swift]QiitaのAPIを叩いて記事を取得して表示するサンプルアプリを書いてみた その3の続き。
上記記事の内容がSwift3の頃のものなので、更新することにした。
また、考えなしにライブラリを導入していたので、これを機会にiOSの標準APIに切り替えてみることにした。
改修対象のソースコード
前回記事公開時点のものを改修する。
https://github.com/macneko-ayu/QiitaAPISample/releases/tag/1.0.0
改修後は下記からどうぞ。
https://github.com/macneko-ayu/QiitaAPISample/releases/tag/1.0.1
やったこと
- Xcode9.3&Swift4対応
- ObjectMapperからCodableへの切り替え
- Alamofireから標準APIへの切り替え
- Viewの改修
Xcode9.3&Swift4対応
Xcode9.3で実行できるか確認
- プロジェクトを
git clone
して取得する - ターミナルでプロジェクトのディレクトリに移動して
pod install
を実行 - 作成された
QiitaAPISample.xcworkspace
を開き、シミュレータをデバイスとして指定してRunを実行 - シミュレータで起動することを確認
問題なく実行できた。
XcodeのRecommended settingsを適用する
- Xcodeの
Issue Navigator
に表示されている「Update to recommended settings」をクリック - ダイアログが表示されるので、すべてにチェックを入れて、「Perform Changes」ボタンをクリック
- 「The working copy QiitaAPISample has uncommitted changes.」っていうアラートが表示されるので、「Countinue」ボタンをクリック
- シミュレータで起動することを確認
Swiftのバージョンを変更する
- プロジェクトの
Build Settings>Swift Language Version
を「Swift 4.1」に変更する - シミュレータで起動することを確認
ObjectMapperからCodableへの切り替え
ObjectMapper の利用をやめ、Swift4から採用されたCodableを使うことにした。
完成形がこちら。
モデルは Item
という名称がふさわしくなかったので、改修を機に Article
にリネームした。
struct Article: Codable {
var title: String?
var userId: String?
private enum UserKeys: String, CodingKey {
case userId = "id"
}
private enum ArticleKeys: String, CodingKey {
case title
case user
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: ArticleKeys.self)
self.title = try values.decode(String.self, forKey: .title)
let user = try values.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
self.userId = try user.decode(String.self, forKey: .userId)
}
}
let data = """
[
{
"title": "仮のタイトル",
"user": {
"id": "macneko_ayu"
}
},
{
"title": "仮のタイトル2",
"user": {
"id": "macneko_ayu"
}
}
]
""".data(using: .utf8)!
let articles = try! JSONDecoder().decode([Article].self, from: data)
切り替えるにあたって、つまづいたところが二つあって、結構悩んだ。
RootがArrayの場合に、JSONをモデルに変換する方法がわからない
RootがArrayじゃない場合は下記コードで変換できる。
let data = """
{
"title": "仮のタイトル",
"user": {
"id": "macneko_ayu"
}
}
""".data(using: .utf8)!
let articles = try! JSONDecoder().decode(Article.self, from: data)
そしてRootがArrayの場合は、Article.self
の部分を、[Article].self
とすれば良いだけだった。
ネストした構造の場合に、JSONをフラットなモデルに変換する方法がわからない
モデルは改修前のフラット構造をそのまま使いたかった。
struct Article: Codable {
var title: String?
var userId: String?
}
この構造のモデルに変換するには、ネスト構造をフラット構造にする必要があるんだけど、その方法がわからなかったので、手始めにネスト構造のまま変換することにした。
Article
モデル内に、JSONの user
キーに相当する User
モデルを持つようにする。
struct Article: Codable {
var title: String?
var user: User
struct User: Codable {
var id: String?
}
}
サンプルアプリではこの構造でもいいんだけど、実際の案件ではフラット構造にしたいことも出てくるだろう。
そこでCodingKeyを用いて、フラット構造に変換できるようにした。
struct Article: Codable {
var title: String?
var userId: String?
private enum UserKeys: String, CodingKey {
case userId = "id"
}
private enum ArticleKeys: String, CodingKey {
case title
case user
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: ArticleKeys.self)
self.title = try values.decode(String.self, forKey: .title)
let user = try values.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
self.userId = try user.decode(String.self, forKey: .userId)
}
}
init(from decoder: Decoder) throws
メソッドで user
キーの内容を一旦デコードし、デコードした中の id
を userId
とする処理を行っている。
それによって、ネスト構造をフラット構造に変換している。
先にネスト構造をネストを保ったまま変換するコードを書いたことによって見えてきたけど、いきなり書くのは難しいな、これ。
Alamofireから標準APIへの切り替え
「通信するなら通信ライブラリを使う」という盲目的な思考で導入したAlamofire。
でも、サンプルアプリではGETしか使わないし、導入している意味がほぼなかったので、標準APIに切り替えた。
完成形がこちら。
struct APIClient {
static func fetchArticle(_ completion: @escaping ([Article]) -> Void) {
let components = URLComponents(string: "https://qiita.com/api/v2/items")
guard let url = components?.url else {
return
}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
let decorder = JSONDecoder()
do {
let articles = try decorder.decode([Article].self, from: data)
print(articles)
completion(articles)
} catch {
print(error.localizedDescription)
}
} else {
print(error ?? "Error")
}
}
task.resume()
}
}
Viewの改修
モデル名を変更したのと、メインキューで APIから取得したデータをUITableViewにセットするようにした。
また、デリゲートをExtensionで適用するようにした。
class ArticleListViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
var articles: [Article] = []
override func viewDidLoad() {
super.viewDidLoad()
title = "新着記事"
tableView.dataSource = self
APIClient.fetchArticle { (articles) in
self.articles = articles
DispatchQueue.main.sync {
self.tableView.reloadData()
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
extension ArticleListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return articles.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.subtitle, reuseIdentifier: "Cell")
let article = articles[indexPath.row]
if let title = article.title {
cell.textLabel?.text = title
}
if let userId = article.userId {
cell.detailTextLabel?.text = userId
}
return cell
}
}
感想
すぐできると思って手をつけたけど、意外とてこずった。
参考
Codable
Encoding and Decoding Custom Types
“SwiftでJSON作成、読込みする方法( Swift4 Codableを利用)” by digitalnauts
ルートが配列のJSONをCoadableでカスタムモデルにマッピングする
Ultimate Guide to JSON Parsing with Swift 4
イケてない JSON を Swift の Decodable で扱いやすいモデルにデコードする - star__hoshi's diary
iOS開発: 再入門 apiを叩いてtableViewに表示する (Qiita編)
通信
Swift の HTTP ライブラリで苦しまないための自作 API クライアント設計
iOSでライブラリに頼らず、URLSessionを使ってHTTP通信する