Edited at

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


はじめに

こんにちは!

皆さん、メトロポリタン美術館のAPIがあることはご存知でしょうか?

このAPIでは、メトロポリタン美術館の作品の写真や詳細を見ることができるようです!

これで、アプリを作ってみたいと思います。

ちなみにメトロポリタン美術館のGithubがあります

作品のデータをCSVとして公開していて、結構更新されています


メトロポリタン美術館のAPI

ということで、メトロポリタン美術館のAPIを触っていきます。

APIの使い方は、メトロポリタン美術館のGithubPagesで見れます。

https://metmuseum.github.io/

このAPIについて、一作品ごとにAPIを一回一回投げないといけないです!

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


アプリ完成品

Githubにコードを上げておきます!

リスト画面
詳細画面
画像画面




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で動的な実装を試してみたかったので、このサイトを参考にしました。

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


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
}

}



最後に

ということで、メトロポリタン美術館の作品を見れるアプリを作りました。

コード実装は簡単でしたが、レイアウトの部分がすこし大変でした...

あまり需要がないと思いますが、使いたい方はぜひ使ってみてください!