iTunesSearchAPIをSwiftで使って、一覧表示させてみた

# 初めに
SwiftでiTunesSearchAPIを使って楽曲一覧アプリを作ってみたいと思います。
初心者にもわかりやすく、AutoLayoutの設定、デザインパターン、コードの可読性もしっかり守っているので、APIの入門記事としてはぴったりかなと。
まず完成形はこちら！

![Videotogif (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/33bb5e2a-0046-fb91-5e07-83e325aa5a3f.gif)

## UIの設計

このように配置していきます。


![スクリーンショット 2021-09-07 0.45.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/4295cb9d-a71b-cfbc-f4da-677674aa1d75.png)
![スクリーンショット 2021-09-07 0.44.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/58499886-28d2-8a93-fa4d-67c11138dd96.png)

制約をつけていきます。

![スクリーンショット 2021-09-07 0.45.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/3ac63a56-b9c9-98bf-7823-30a6f908b470.png)
![スクリーンショット 2021-09-07 0.44.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/56fcb484-65e6-e8e2-1ca6-333be194ac8f.png)

SearchViewController,ListTableViewCellを作り、IBOutlet,IBAction接続します。

```swift:SearchViewController.swift
import UIKit

class SearchViewController: UIViewController{
    
    @IBOutlet weak var searchBar: UISearchBar!{
        didSet{
            searchBar.delegate = self
        }
    }
    @IBOutlet weak var tableView: UITableView!{
        didSet{
            tableView.delegate = self
            tableView.dataSource = self
        }
    }
}
```

```swift:ListTableViewCell.swift
import UIKit

class ListTableViewCell: UITableViewCell {
    @IBOutlet weak var artistImageView: UIImageView!
    @IBOutlet private weak var songNameLabel: UILabel!
    @IBOutlet private weak var artistNameLabel: UILabel!
}
```

## 全体設計
UIができた後に、今回のアプリの設計を行なっていく。
![スクリーンショット 2021-09-07 11.23.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/80c95c7b-c309-4a44-2705-a5982272e344.png)
![スクリーンショット 2021-09-07 11.18.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/ff116ffa-738d-82b2-3ea1-58b7c1f47a5a.png)

## APIの取得
まず、APIの取得からやっていきたいと思います。
[iTunesAPI](http://resources.organicfruitapps.com/documentation/itunes-store-web-service-search-api/)を使います。

今回は,`https://itunes.apple.com/search?term=\(text)&entity=song&country=jp`を使っていきます。

こちらでこのように(`https://itunes.apple.com/search?term=西野カナ&entity=song&country=jp`)APIを叩くと、JSONデータを変換してくれます。
![スクリーンショット 2021-09-07 11.34.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/5bd9d1fe-f2a2-ce12-7ab8-1d408ae2bdd7.png)

これらのデータをうまく使い今回はアプリを作成していきます。

## iTunesAPI
今回のAPIにおいてのロジックを管理するiTunesAPIを書いていきます。

```swift:iTunesAPI.swift
import Foundation
import Reachability

class iTunesAPI {
    
    private static var task: URLSessionTask?
    
    enum SearchRepositoryError: Error {
        case wrong
        case network
        case parse
    }
    
    static func searchRepository(text: String, completionHandler: @escaping (Result<[Song], SearchRepositoryError>) -> Void) {
        if !text.isEmpty {
            
            let reachability = try! Reachability()
            if reachability.connection == .unavailable {
                completionHandler(.failure(SearchRepositoryError.network))
                return
            }
            let urlString = "https://itunes.apple.com/search?term=\(text)&entity=song&country=jp".addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!
            
            guard let url = URL(string: urlString) else {
                completionHandler(.failure(SearchRepositoryError.wrong))
                return
            }
            
            let task = URLSession.shared.dataTask(with: url) { (data, res, err) in
                if err != nil {
                    completionHandler(.failure(SearchRepositoryError.network))
                    return
                }
                
                guard let date = data else {return}
                
                if let result = try? jsonStrategyDecoder.decode(Songs.self, from: date) {
                    completionHandler(.success(result.results))
                } else {
                    completionHandler(.failure(SearchRepositoryError.parse))
                }
            }
            task.resume()
        }
    }
    
    static private var jsonStrategyDecoder: JSONDecoder {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        return decoder
    }
    
    static func taskCancel() {
        task?.cancel()
    }
}
```

## Songs
レスポンスしたデータをデコードするためSongsを作ります。

```swift:Songs.swift
import Foundation

struct Songs :Codable{
    let results:[Song]
}

struct Song :Codable{
    //アーティスト名
    let artistName :String?   
    //楽曲名
    let trackCensoredName:String?
　  //音源URL
    let previewUrl:String?
    //ジャケ写URL
    let artworkUrl100:String
    
    var artworkImageUrl100:URL?{
        return URL(string: artworkUrl100)
    }
}
```

## SearchViewController
取得したデータをViewに反映させる、またTableView,SearchBarの操作のためにSearchViewControllerを作っていきます。

```swift:SearchViewController.swift
import UIKit
import JGProgressHUD
import AVFoundation

class SearchViewController: UIViewController{
    
    @IBOutlet weak var searchBar: UISearchBar!{
        didSet{
            searchBar.delegate = self
        }
    }
    @IBOutlet weak var tableView: UITableView!{
        didSet{
            tableView.delegate = self
            tableView.dataSource = self
        }
    }
 
    private var songs: [Song] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()

        let nib = UINib(nibName: ListTableViewCell.cellIdentifier, bundle: nil)
        tableView.register(nib, forCellReuseIdentifier: ListTableViewCell.cellIdentifier)
        
        tableView.rowHeight = UITableView.automaticDimension
    }
}

extension SearchViewController: UITableViewDelegate, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return songs.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: ListTableViewCell.cellIdentifier, for: indexPath) as! ListTableViewCell

        let song = songs[indexPath.row]
        cell.setup(song:song)
        return cell
    }
}

extension SearchViewController: UISearchBarDelegate{
    func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
        return true
    }
    
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        iTunesAPI.taskCancel()
    }
    
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        guard !(searchBar.text?.isEmpty ?? true) else { return }
        searchBar.resignFirstResponder()
        let word = searchBar.text!
        
        let progressHUD = JGProgressHUD()
        progressHUD.show(in: self.view)
        
        iTunesAPI.searchRepository(text: word) { result in
            DispatchQueue.main.async {
                progressHUD.dismiss()
            }
            
            switch result {
            case .success(let results):
                self.songs = results
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            case .failure(let error):
                DispatchQueue.main.async {
                    switch error {
                    case .wrong :
                        let alert = ErrorAlert.wrongWordError()
                        self.present(alert, animated: true, completion: nil)
                        return
                    case .network:
                        let alert = ErrorAlert.networkError()
                        self.present(alert, animated: true, completion: nil)
                        return
                    case .parse:
                        let alert = ErrorAlert.parseError()
                        self.present(alert, animated: true, completion: nil)
                        return
                    }
                }
            }
        }
        return
    }
}
```

## ListTableViewCell
tableViewCellの配置を行うListTableViewCellを作っていきます。
その前に画像のキャッシュのために便利な[SDWebImage](https://github.com/SDWebImage/SDWebImage)というライブラリを使いたいと思います。
SDWebImageの詳しい説明、導入の仕方などは[これら](https://qiita.com/hcrane/items/422811dfc18ae919f8a4)の記事を見るとわかると思います。

```swift:ListTableViewCell.swift
import UIKit
import SDWebImage

class ListTableViewCell: UITableViewCell {
    @IBOutlet private weak var artistImageView: UIImageView!
    @IBOutlet private weak var songNameLabel: UILabel!
    @IBOutlet private weak var artistNameLabel: UILabel!
    
    static let cellIdentifier = String(describing: ListTableViewCell.self)
    
    func setup(song: Song) {
        
        songNameLabel.text = song.trackCensoredName ?? ""
        artistNameLabel.text = song.artistName ?? ""
        
        if let url = song.artworkImageUrl100 {
            artistImageView.sd_setImage(with: url, completed: nil)
        } else {
            artistImageView.image = nil
        }
    }
}
```
## ErrorAlert
最後に、様々なエラー処理のためのクラスを作っていきたいと思います。

```swift:ErrorAlert.swift
import UIKit

class ErrorAlert {
    
    private static func errorAlert(title: String, message: String = "") -> UIAlertController {
        let alert: UIAlertController = UIAlertController(title: title, message : message, preferredStyle: UIAlertController.Style.alert)
        let defaultAction = UIAlertAction(title: "OK", style: UIAlertAction.Style.default)
        alert.addAction(defaultAction)
        return alert
    }
    
    static func wrongWordError() -> UIAlertController {
        return errorAlert(title: "不正なワードが入力されました", message: "検索ワードを確認してください")
    }
    
    static func networkError() -> UIAlertController {
        return errorAlert(title: "インターネットに接続されていません", message: "接続状況を確認してください")
    }
    
    static func parseError() -> UIAlertController {
        return errorAlert(title: "データの表示に失敗しました")
    }
}
```

# 終わりに
以上でこのようなアプリが作成できました。
![Videotogif (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325764/33bb5e2a-0046-fb91-5e07-83e325aa5a3f.gif)