[Swift] iTunes APIを叩いて音楽視聴アプリを作ってみた

#iTunesAPI
iTunesAPIを利用することでiTunes内の本、映画、ポッドキャスト、音楽、ミュージックビデオ、オーディオブック、テレビ番組などの情報を取得できます。今回はその中でも音楽の視聴音源を用いて音楽視聴アプリを作ってみました。
## iTunesAPIのドキュメントに関して
- [iTunes APIのドキュメント](http://resources.organicfruitapps.com/documentation/itunes-store-web-service-search-api/)

##リクエスト例
例としてoneokrockの情報を取得してみます。
リクエストURLは以下のようになります。

```
https://itunes.apple.com/search?term=\oneokrock&entity=song&contry=jp
```
##レスポンス
上記のURLを入力すると以下のような.txtファイルがダウンロードされる
(少々長いのでresultCount2つまでを表示)
[JSON Pretty Linter](https://lab.syncer.jp/Tool/JSON-Viewer/)を用いて整形しました。

```json
{
resultCount:50
results:[
0:{
wrapperType:track
kind:song
artistId:252239625
collectionId:605688804
trackId:605689011
artistName:ONE OK ROCK
collectionName:Jinsei × Boku =
trackName:Clock Strikes
collectionCensoredName:Jinsei × Boku =
trackCensoredName:Clock Strikes
artistViewUrl:https://music.apple.com/us/artist/one-ok-rock/252239625?uo=4
collectionViewUrl:https://music.apple.com/us/album/clock-strikes/605688804?i=605689011&uo=4
trackViewUrl:https://music.apple.com/us/album/clock-strikes/605688804?i=605689011&uo=4
previewUrl:https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/af/6f/df/af6fdfd5-a32c-e794-ed20-e2e20c043981/mzaf_14716866606323246628.plus.aac.p.m4a
artworkUrl30:https://is4-ssl.mzstatic.com/image/thumb/Music125/v4/db/43/0a/db430aad-9319-2f7b-005c-8646e7f388a8/source/30x30bb.jpg
artworkUrl60:https://is4-ssl.mzstatic.com/image/thumb/Music125/v4/db/43/0a/db430aad-9319-2f7b-005c-8646e7f388a8/source/60x60bb.jpg
artworkUrl100:https://is4-ssl.mzstatic.com/image/thumb/Music125/v4/db/43/0a/db430aad-9319-2f7b-005c-8646e7f388a8/source/100x100bb.jpg
collectionPrice:13.99
trackPrice:1.29
releaseDate:2013-03-06T12:00:00Z
collectionExplicitness:notExplicit
trackExplicitness:notExplicit
discCount:1
discNumber:1
trackCount:13
trackNumber:5
trackTimeMillis:235347
country:USA
currency:USD
primaryGenreName:Rock
isStreamable:true
}
1:{
wrapperType:track
kind:song
artistId:252239625
collectionId:1176157333
trackId:1176157730
artistName:ONE OK ROCK
collectionName:Ambitions
trackName:Take What You Want (feat. 5 Seconds of Summer)
collectionCensoredName:Ambitions
trackCensoredName:Take What You Want (feat. 5 Seconds of Summer)
artistViewUrl:https://music.apple.com/us/artist/one-ok-rock/252239625?uo=4
collectionViewUrl:https://music.apple.com/us/album/take-what-you-want-feat-5-seconds-of-summer/1176157333?i=1176157730&uo=4
trackViewUrl:https://music.apple.com/us/album/take-what-you-want-feat-5-seconds-of-summer/1176157333?i=1176157730&uo=4
previewUrl:https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview115/v4/4e/cc/30/4ecc300e-a281-2d0c-1fb6-72886e3f9bc5/mzaf_6734311677989074101.plus.aac.p.m4a
artworkUrl30:https://is3-ssl.mzstatic.com/image/thumb/Music125/v4/ca/fc/a8/cafca8f8-f563-ab15-5873-974092810637/source/30x30bb.jpg
artworkUrl60:https://is3-ssl.mzstatic.com/image/thumb/Music125/v4/ca/fc/a8/cafca8f8-f563-ab15-5873-974092810637/source/60x60bb.jpg
artworkUrl100:https://is3-ssl.mzstatic.com/image/thumb/Music125/v4/ca/fc/a8/cafca8f8-f563-ab15-5873-974092810637/source/100x100bb.jpg
collectionPrice:9.99
trackPrice:1.29
releaseDate:2017-01-11T12:00:00Z
collectionExplicitness:notExplicit
trackExplicitness:notExplicit
discCount:1
discNumber:1
trackCount:14
trackNumber:14
trackTimeMillis:243274
country:USA
currency:USD
primaryGenreName:Rock
isStreamable:true
}
//以下省略
```
##今回アプリで用いる情報とライブラリ
###artistName
アーティスト名

###trackCensoredName
曲名

###previewUrl
視聴音源URL

###artworkUrl100
アルバムジャケット

###ライブラリの導入
Podfileに以下をインストール

```
pod 'SwiftyJSON',
pod 'Alamofire',
pod 'SDWebImage
```
##コーディング
役割ごとにModel,View,Controllerに分けてコーディングを行う

##View

![スクリーンショット 2021-09-26 16.06.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1908752/8c778a02-7542-2270-483a-e6984f1df48b.png)

##Model
```swift:model.swift
import Foundation
import SwiftyJSON
import Alamofire
protocol MusicProtocol {
    func catchData(count:Int)
}
class MusicModel{
    var artistNameArray = [String]()
    var trackCensoredNameArray = [String]()
    var previewUrlArray = [String]()
    var artworkUrl100Array = [String]()
    var musicDelegate:MusicProtocol?
    //JSON解析
    func setData(resultCount:Int,encodeUrlString:String){
        //Alamfireによる通信
        AF.request(encodeUrlString, method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON { (response) in
            //responseの値表示
            print("response:",response)
            //一旦配列に入っているものすべてを削除する(蓄積防止)
            self.artistNameArray.removeAll()
            self.trackCensoredNameArray.removeAll()
            self.previewUrlArray.removeAll()
            self.artworkUrl100Array.removeAll()
            switch response.result{
            case .success:
                do {
                    let json:JSON = try JSON(data: response.data!)
                    for i in 0...resultCount-1{
                        //もしartistNameがnilだったら
                        if json["results"][i]["artistName"].string == nil{
                            print("ヒットしませんでした")
                            return
                        }
                        self.artistNameArray.append(json["results"][i]["artistName"].string!)
                        self.trackCensoredNameArray.append(json["results"][i]["trackCensoredName"].string!)
                        self.previewUrlArray.append(json["results"][i]["previewUrl"].string!)
                        self.artworkUrl100Array.append(json["results"][i]["artworkUrl100"].string!)
                    }
                    //全てのデータを取得している状態
                    self.musicDelegate?.catchData(count:1)
                } catch  {
                }
                break
            case .failure(_):break
            }
        }
    }
}
```

##Controller
```swift:controller.swift
import UIKit
import SDWebImage
import AVFoundation
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource,UITextFieldDelegate,MusicProtocol {
    @IBOutlet weak var searchTextField: UITextField!
    
    @IBOutlet weak var musicTableView: UITableView!
    var musicModel = MusicModel()
    var player:AVAudioPlayer?

    override func viewDidLoad() {
        super.viewDidLoad()
        musicTableView.delegate = self
        musicTableView.dataSource = self
        searchTextField.delegate = self
    }
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    //tableViewDelegateによるデリゲートメソッド
    //returnカウントの数だけcellForRowAtが呼ばれる
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return musicModel.artistNameArray.count
    }
    //tableViewDelegateによるデリゲートメソッド
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = musicTableView.dequeueReusableCell(withIdentifier: "musicCell", for: indexPath)
        let artWorkImageView = cell.contentView.viewWithTag(1) as! UIImageView
        let musicNameLabel = cell.contentView.viewWithTag(2) as! UILabel
        let artistNameLabel = cell.contentView.viewWithTag(3) as! UILabel
        //写真、音楽名、アーティスト名の格納
        artWorkImageView.sd_setImage(with: URL(string: musicModel.artworkUrl100Array[indexPath.row]), completed: nil)
        musicNameLabel.text = musicModel.trackCensoredNameArray[indexPath.row]
        artistNameLabel.text = musicModel.artistNameArray[indexPath.row]
        //楽曲再生ボタンの作成
        let musicPlayButton = UIButton(frame: CGRect(x: 35, y: 21, width: 130, height: 130))
        musicPlayButton.setImage(UIImage(named:"play"), for: .normal)
        //ボタンを押したとき
        musicPlayButton.addTarget(self, action: #selector(playButtonTap(_:)), for:.touchUpInside)
        musicPlayButton.tag = indexPath.row
        //セルにmusicButtonを追加(StoryBoardにないから)
        cell.contentView.addSubview(musicPlayButton)
        return cell
    }
    //ボタンをタップした時
    @objc func playButtonTap(_ sender:UIButton){
        //音楽を止める
        if player?.isPlaying == true{
            player?.stop()
        }
        //sender.tagはindexPath.rowすなわちplayButton.tagとい一緒
        let url = URL(string: musicModel.previewUrlArray[sender.tag])
        downLoadMusicURL(url: url!)
    }
    //リターンが押されたときにキーボードを閉じる
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        //キーボードを閉じる
        refleshData()
        textField.resignFirstResponder()
        return true
    }
    //セルの高さ
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 170
    }
    //ダウンロードメソッド
    func downLoadMusicURL(url:URL){
      var downloadTask:URLSessionDownloadTask
      downloadTask = URLSession.shared.downloadTask(with: url, completionHandler: { (url, response, error) in
        self.play(url: url!)
      })
      downloadTask.resume()
    }
    //音楽再生メソッド
    func play(url:URL){
      do {
        player = try AVAudioPlayer(contentsOf: url)
        player?.prepareToPlay()
        player?.volume = 1.0
        player?.play()
      } catch let error as NSError {
        print(error.description)
      }
    }
    //MusicProtocolによるデリゲートメソッド
    func catchData(count: Int) {
        if count == 1{
            musicTableView.reloadData()
        }
    }
    func refleshData(){
        //テキストフィールドの中にアーティスト名が入ってたらアーティスト名を用いてitunesAPIを用いる
        if searchTextField.text?.isEmpty != nil{
            let urlString = "https://itunes.apple.com/search?term=\(String(describing:searchTextField.text!))&entity=song&contry=jp"
            //urlStringをエンコードする
            let encodeUrlString:String = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
            //委任
            musicModel.musicDelegate = self
            musicModel.setData(resultCount: 50, encodeUrlString: encodeUrlString)
            //キーボードを閉じる
            searchTextField.resignFirstResponder()
        }
    }
    
    @IBAction func searchAction(_ sender: Any) {
        refleshData()
    }
}
```

## デモ動画
アルバムジャケットの上に配置した再生ボタンを押せば音源が流れる仕様になっています
![demo2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1908752/91e56cf6-1909-1902-e1da-0b0a791eba5a.gif)


## 参考記事
- [【PHP】iTunes APIで音楽の試聴音源を取得する方法](https://haniwaman.com/itunes-api/)