LoginSignup
11
9

More than 5 years have passed since last update.

QiitaのAPIを叩いて記事を取得して表示するサンプルアプリを書いてみた その4(Xcode9.3&Swift4対応、Codable対応、標準API切り替え対応)

Last updated at Posted at 2018-05-22

[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で実行できるか確認

  1. プロジェクトを git clone して取得する
  2. ターミナルでプロジェクトのディレクトリに移動して pod install を実行
  3. 作成された QiitaAPISample.xcworkspace を開き、シミュレータをデバイスとして指定してRunを実行
  4. シミュレータで起動することを確認

問題なく実行できた。

XcodeのRecommended settingsを適用する

  1. Xcodeの Issue Navigator に表示されている「Update to recommended settings」をクリック
  2. ダイアログが表示されるので、すべてにチェックを入れて、「Perform Changes」ボタンをクリック
  3. 「The working copy QiitaAPISample has uncommitted changes.」っていうアラートが表示されるので、「Countinue」ボタンをクリック
  4. シミュレータで起動することを確認

Swiftのバージョンを変更する

  1. プロジェクトの Build Settings>Swift Language Version を「Swift 4.1」に変更する
  2. シミュレータで起動することを確認

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)
    }
}
JSON→モデル
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じゃない場合は下記コードで変換できる。

JSON→モデル
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 キーの内容を一旦デコードし、デコードした中の iduserId とする処理を行っている。
それによって、ネスト構造をフラット構造に変換している。
先にネスト構造をネストを保ったまま変換するコードを書いたことによって見えてきたけど、いきなり書くのは難しいな、これ。

Alamofireから標準APIへの切り替え

「通信するなら通信ライブラリを使う」という盲目的な思考で導入したAlamofire。
でも、サンプルアプリではGETしか使わないし、導入している意味がほぼなかったので、標準APIに切り替えた。

完成形がこちら。

APIClient
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で適用するようにした。

ArticleListViewController
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通信する

11
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
9