メトロポリタン美術館のAPIからアプリを作った

# はじめに
こんにちは！
皆さん、メトロポリタン美術館のAPIがあることはご存知でしょうか？
このAPIでは、メトロポリタン美術館の作品の写真や詳細を見ることができるようです！
これで、アプリを作ってみたいと思います。
ちなみにメトロポリタン美術館の[Github](https://github.com/metmuseum)があります
作品のデータをCSVとして公開していて、結構更新されています

## メトロポリタン美術館のAPI
ということで、メトロポリタン美術館のAPIを触っていきます。
APIの使い方は、メトロポリタン美術館のGithubPagesで見れます。
[https://metmuseum.github.io/](https://metmuseum.github.io/)

このAPIについて、一作品ごとにAPIを一回一回投げないといけないです！
あと、作者や写真などのデータが抜けているときがあるので、今回は、事前に何回もAPIを投げて、写真や作者のデータがきちんとあるものだけをクライアント側で集めています。

## アプリ完成品
Githubに[コード](https://github.com/azuma317/MetropolitanApp)を上げておきます！

|リスト画面|詳細画面|画像画面|
| --- | --- | --- |
|![image.png](https://qiita-image-store.s3.amazonaws.com/0/211071/00738ce2-29c6-b1ec-1004-99631852adaf.png)|![image.png](https://qiita-image-store.s3.amazonaws.com/0/211071/44f62257-1bbe-fbec-4853-45f031fa4576.png)|![image.png](https://qiita-image-store.s3.amazonaws.com/0/211071/bc1863be-c9fe-5943-da5e-7ff784826eac.png)|

## Library
* Kingfisher

# 実装
## モデル

``` Work.swift
struct Work: Decodable {
    var id: Int?
    var isPublic: Bool?
    var imagePath: String?
    var artistName: String?
    var title: String?
    var classification: String?
    var date: String?
    var medium: String?
    var url: String?

    private enum CodingKeys: String, CodingKey {
        case id = "objectID"
        case isPublic = "isPublicDomain"
        case imagePath = "primaryImage"
        case artistName = "artistDisplayName"
        case title
        case classification
        case date = "objectDate"
        case medium
        case url = "objectURL"
    }

    func isTrue() -> Bool {
        return id != nil && isPublic ?? false && !(imagePath?.isEmpty ?? true) && !(artistName?.isEmpty ?? true) && !(title?.isEmpty ?? true) && !(classification?.isEmpty ?? true) && !(date?.isEmpty ?? true)
    }
}
```

## リスト画面
今回は、UITableViewControllerを使います。
ここでAPIを叩きますが、先程述べたように、レスポンスに写真や作者のデータが無いときがあります。
なので、ここでは、事前に使えるIDだけを羅列したファイルを作りました。

``` 1.txt
34
37
38
40
41
108
109
110
111
112
113
114
115
116
117
119
122
```
リスト画面の全体コード

``` WorkListController.swift
class WorkListController: UITableViewController, UITextFieldDelegate, UIPickerViewDelegate, UIPickerViewDataSource {

    var works: [Work] = []
    var tempWorks: [Work] = []

    let pages: [Int] = Array(1...30)
    let pickerView = UIPickerView()
    var selectedPage = 1
    var workIDs: [Int] = []

    @IBOutlet weak var pageTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        pickerView.delegate = self
        pickerView.dataSource = self

        let toolBar = UIToolbar()
        toolBar.sizeToFit()
        let toolbarButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done))
        toolBar.items = [toolbarButton]
        pageTextField.inputAccessoryView = toolBar

        loadWork()
    }

    @objc func done() {
        loadWork()
        pageTextField.resignFirstResponder()
    }

    // ファイルからIDを読み取って配列に格納
    func loadWork() {
        guard let path = Bundle.main.path(forResource: "\(selectedPage)", ofType: "txt") else { return }
        guard let workID = try? String(contentsOfFile: path, encoding: .utf8) else { return }
        self.workIDs = []
        // 1行ずつ読み取って配列に格納する
        workID.enumerateLines(invoking: { (workID, _) in
            guard let id = Int(workID) else { return }
            self.workIDs.append(id)
        })
        tasks()
    }

    // 再帰を使って、APIを叩く
    func tasks() {
        tempWorks = []
        for id in workIDs {
            let url = URL(string: "https://collectionapi.metmuseum.org/public/collection/v1/objects/\(id)")
            URLSession.shared.dataTask(with: url!) { (data, _, _) in
                guard let data = data else { return }
                do {
                    let work = try JSONDecoder().decode(Work.self, from: data)
                    if work.isTrue() && !(self.tempWorks.last?.artistName == work.artistName && self.tempWorks.last?.title == work.title) {
                        self.tempWorks.append(work)
                        DispatchQueue.main.async {
                            // 25個のデータを取得するごとに表示
                            if self.tempWorks.count % 25 == 0 {
                                self.works = self.tempWorks
                                self.tableView.reloadData()
                            }
                        }
                    }
                } catch {
                    print(error)
                }
                }.resume()
        }
    }

    // Page機能
    @IBAction func textFieldEditing(_ sender: UITextField) {
        sender.inputView = pickerView
    }

    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return pages.count
    }

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return "\(pages[row])Page"
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        selectedPage = pages[row]
        pageTextField.text = "\(pages[row])Page"
    }

    // TableView DataSource
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return works.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "WorkCell", for: indexPath) as! WorkCell

        cell.selectionStyle = .none
        cell.configure(work: works[indexPath.item])

        return cell
    }

    // TableView Delegate
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 240
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        performSegue(withIdentifier: "toDetail", sender: works[indexPath.item])
    }

    // 画面遷移
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        guard let work = sender as? Work else { return }
        guard let vc = segue.destination as? DetailController else { return }
        vc.imagePath = work.imagePath
        vc.workName = work.title
        vc.artistName = work.artistName
        vc.date = work.date
        vc.classification = work.classification
    }

}
```

## 詳細画面
レイアウトに関しては、AutoLayoutで動的な実装を試してみたかったので、この[サイト](https://blog.kishikawakatsumi.com/entry/2018/11/05/044228)を参考にしました。

この画面では、主にデータを表示する処理しかしていません。

``` DetailController.swift
class DetailController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var workNameLabel: UILabel!
    @IBOutlet weak var artistNameLabel: UILabel!
    @IBOutlet weak var dateLabel: UILabel!
    @IBOutlet weak var classificationLabel: UILabel!

    var imagePath: String?
    var workName: String?
    var artistName: String?
    var date: String?
    var classification: String?

    override func viewDidLoad() {
        super.viewDidLoad()

        guard let imagePath = imagePath else { return }
        imageView.kf.setImage(with: URL(string:imagePath))
        workNameLabel.text = workName
        artistNameLabel.text = artistName
        dateLabel.text = date
        classificationLabel.text = classification
    }
    
    @IBAction func showImage(_ sender: UIButton) {
        performSegue(withIdentifier: "toPhoto", sender: nil)
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let vc = segue.destination as! PhotoController
        vc.imagePath = imagePath
    }

}
```

## 画像画面
画像の表示画面については、画像の拡大をしたかったのでScrollViewにImageViewをおいています。

``` PhotoController.swift
class PhotoController: UIViewController, UIScrollViewDelegate {

    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var scrollView: UIScrollView!

    var imagePath: String?

    override var prefersStatusBarHidden: Bool {
        return navigationController?.isNavigationBarHidden ?? false
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        scrollView.delegate = self
        scrollView.maximumZoomScale = 8.0
        scrollView.minimumZoomScale = 1.0
        guard let imagePath = imagePath else { return }
        imageView.kf.setImage(with: URL(string: imagePath))
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        navigationController?.hidesBarsOnTap = true
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        navigationController?.hidesBarsOnTap = false
    }

    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageView
    }

}
```

# 最後に
ということで、メトロポリタン美術館の作品を見れるアプリを作りました。
コード実装は簡単でしたが、レイアウトの部分がすこし大変でした...
あまり需要がないと思いますが、使いたい方はぜひ使ってみてください！