技術記事については、Qiitaにも稀に投稿しています。

iTunes APIを使って、iOSの音楽試聴アプリを開発してみた話(Swift2対応)

iosApp

まずお伝えしたいのは、著者はiOSアプリ開発の初心者である、ということ。

つまり、これから開発するアプリは紛れも無く初めてのiOSアプリであり、動作こそするものの、エラー処理だのレイアウト調整だのは一切やっていない、ってことです。

まずは動かす、話はそれからだ。
[toc]

完成品の音楽視聴アプリ

完成品はこんな感じ。検索バーにアーティスト名を入れると、ヒットした楽曲がリストに表示され、タップすると遷移先でプレーヤーが再生をする、というもの。

iTunes APIについて

概要

公式サイトより引用。

The Search API allows you to place search fields in your website to search for content within the iTunes Store, App Store, iBooks Store and Mac App Store. You can search for a variety of content; including apps, iBooks, movies, podcasts, music, music videos, audiobooks, and TV shows.

要約すると、「iTunesとかApp Storeとかのコンテンツを検索してウェブサイトに載せられるような検索APIがあるよ。アプリとか書籍、映画、ポッドキャスト、音楽とかを探せるよ。」って感じです。

リクエスト例

http://itunes.apple.com/search?term=aiko&country=JP&lang=ja_jp&media=music&entity=song&attribute=artistTerm&limit=30
  • term: aiko
  • country: JP
  • lang: ja_jp
  • media: music
  • entity: song
  • attribute: artistTerm
  • limit: 30

パラメータには様々な組み合わせがあるので、それは公式サイトを読んでみてください。

レスポンス例

{
    "resultCount": 30, 
    "results": [
        {
            "artistId": 635154255, 
            "artistName": "aiko", 
            "artistViewUrl": "http://itunes.apple.com/jp/artist/aiko/id635154255?uo=4", 
            "artworkUrl100": "http://is5.mzstatic.com/image/thumb/Music2/v4/78/3b/4c/783b4c75-8f55-c585-ec28-fbb086c56840/source/100x100bb.jpg", 
            "artworkUrl30": "http://is5.mzstatic.com/image/thumb/Music2/v4/78/3b/4c/783b4c75-8f55-c585-ec28-fbb086c56840/source/30x30bb.jpg", 
            "artworkUrl60": "http://is5.mzstatic.com/image/thumb/Music2/v4/78/3b/4c/783b4c75-8f55-c585-ec28-fbb086c56840/source/60x60bb.jpg", 
            "collectionCensoredName": "4月の雨 - Single", 
            "collectionExplicitness": "notExplicit", 
            "collectionId": 634853356, 
            "collectionName": "4月の雨 - Single", 
            "collectionPrice": 250.0, 
            "collectionViewUrl": "http://itunes.apple.com/jp/album/4yueno-yu/id634853356?i=634853563&uo=4", 
            "country": "JPN", 
            "currency": "JPY", 
            "discCount": 1, 
            "discNumber": 1, 
            "isStreamable": false, 
            "kind": "song", 
            "previewUrl": "http://a1939.phobos.apple.com/us/r20/Music2/v4/c5/22/db/c522dbb4-f263-d714-1626-f965a354ae6d/mzaf_8138279883609072773.aac.m4a", 
            "primaryGenreName": "J-Pop", 
            "releaseDate": "2013-04-17T07:00:00Z", 
            "trackCensoredName": "4月の雨", 
            "trackCount": 1, 
            "trackExplicitness": "notExplicit", 
            "trackId": 634853563, 
            "trackName": "4月の雨", 
            "trackNumber": 1, 
            "trackPrice": 250.0, 
            "trackTimeMillis": 335799, 
            "trackViewUrl": "http://itunes.apple.com/jp/album/4yueno-yu/id634853356?i=634853563&uo=4", 
            "wrapperType": "track"
        }, 
以下略

今回は、曲名にあたる「collectionName」と、試聴URLにあたる「previewUrl」を使ってアプリを開発していきます。

プロジェクトの作成

Xcodeの起動

起動メニューから「Create a new Xcode project」を選択します。

1_startXcode

左メニューの「iOS > Application」から、「Single View Application」を選択します。ここまではお決まりの手順。

2_singleViewApp

アプリに関する情報の入力

  • Product Name(アプリ名)
  • Organization Name(作者名とか)
  • Organization Identifier(独自ドメインがあるなら逆順で入力、ないなら適当に)
  • Language(Swift確定)
  • Devices(今回はiPhone)

その他の項目は、アプリデータベースを使うか等の設定なので今回はFALSEでOKです。

3_productName

Projectが作成されると以下のような初期画面が開きます。本稿ではXcodeの各画面やウィンドウの呼称については割愛します。

4_startProject

開発スタート

左メニューから「Main.stroryborad」を開くと、ViewControllerがひとつ配置された状態になっています。iPhoneにしてはちょっと横広な気がするのでそれは後で直します。

5_viewController

Navigation Controllerの準備

今回は初期状態のView Controllerではなく、Navigation Controllerというものを使いたいので、右メニューから「Navigation Controller」を探して(上のほうにある。なければ検索を。)storyborad上にドラッグします。

6_navigationController

Entry Pointの変更

Navigation Controllerをstoryboradに配置すると、以下のように初期状態のViewにだけ「→」が接続されているのがわかります。これがEntry Pointとなります。

起動画面は別で用意できるので、起動画面直後のホームってところでしょうか。

7_placeNavigationController

今回は追加したNavigation Controllerをホーム画面としたいのでこの「→」を移動します。Navigation Controllerを選択し右メニューから「Is Initial View Controller」にチェックを入れます。

すると、以下のように「→」が移動します。

8_initialViewToNavigation

storyboard上をすっきりさせるためにも初期のView Controllerは消してしまいます。

9_onlyNavigationController

sizeの変更

厳密にはサイズというか、シミュレータ時のサイズの変更です。(iPhoneの様々なサイズやiPadにまで対応させるにはもっと複雑な設定をしていくことになるみたいなので)。

右メニューのSimulated MetricsからSizeのプルダウンメニューを開き、今回は「iPhone 4-inch」とします。

10_changeSizeTo4inch

Navigation Controllerを選択しているので、全体が縦長のiPhoneらしいサイズ感になります。もちろん、これで4-inchのiPhoneのみに対応するアプリになる、というわけではありません。

11_changedSize

検索バーの準備

Search Barを設置

iOSアプリ開発は画面レイアウトのパーツを配置して、そこに挙動・制御となるコードを紐付けていくので、まずはユーザからの入力を受ける検索バーを配置します。

12_searchBarSearch

Navigation Controllerと同じように、右メニューからSearch Barを探します。そして、それをNavigation Controllerの右のViewに配置します。

13_dropSearchBar

14_resultDropSearchBar

結果表示テーブルのidentifierを設定

ここで、検索した結果を表示するテーブルに識別子となるidentifierを設定しておきます。検索バー下の「Prototype Cells」を選択し、右メニューのTable View CellからIdentifierに「cell」と入力します。

15_settingIdentifier

Navigation Controller用のSwiftファイルを追加

と、その前にデフォルトのView Controller.swiftを削除しておきます。これをそのまま使っても(がんばれば)できなくはなかったですが、UITableViewControllerの継承がうまくできなかったので。。

16_deleteViewController

メニューの「File > New > File...」を選択し、表示された以下のダイアログで「Cocoa Touch Class」を選択します。

17_selectCocoaTouch

Class名は「RootViewController」とし、UITableViewControllerを継承します。他はデフォルトのままで、他のファイルと同階層に保存します。

18_CreateRootViewController

Navigation Controllerと紐付ける

Main.storyboard上で、Navigation Controllerを選択し、右メニューのCustom Classプルダウンから「RootViewController」を選択します。(作成時にUITableViewControllerを継承していないと出現しないようです)

19_settingRootViewController

Search Barの実装

Search Barのoutletを作成

いよいよ、コーディングに入っていくのですが、その前にstoryboardに配置した検索バーをコード内で扱うため、outletを作成します。

検索バーを選択し、controlキーを押しながらソース上にドラッグします。

  • Name: searchBar
  • Type: UISearchBar
  • Storage: Weak

20_outletSearchBarToRootView

以下のような感じになっていればOKです。

class RootViewController: UITableViewController {

    @IBOutlet weak var searchBar: UISearchBar!

    override func viewDidLoad() {
        super.viewDidLoad()

また、outletが作成されているかの確認として、コードの左にある黒い丸をクリックすると、storyboard側の検索バーが青くハイライトされます。

22_checkSearchBar

json.swiftの作成

Search Barの動きをコーディングする前に忘れてはいけないのは、今回はiTunes APIを叩いたレスポンスを使うということです。

APIのレスポンスがJSON形式なので、それをSwiftで扱いやすくするためのライブラリを追加します。いろいろとライブラリは存在しているのですが、今回は「swift-json」を使います。

23_createSwiftFile

ファイルの作成で、Swiftファイルを選択し、ファイル名はjson.swiftとします。作成した後、swift-jsonのGithubから落としたソースをペタッと貼り付けます。

落としたソースを先にディレクトリに置いて、Xcodeから読み込んでもたぶん同じです。

24_checkJsonSwift

Search Barの検索ボタンが押された時の挙動

ようやく本格的なコードを書きます。ユーザが何かキーワードを入力し、検索を行った時の制御です。

  • trackNames: 曲名を格納する可変配列
  • previewUrls: 試聴用URLを格納する可変配列

searchWordには日本語が入力されることも想定して、エンコーディングも忘れずに。

class RootViewController: UITableViewController {

    @IBOutlet weak var searchBar: UISearchBar!

    var trackNames = NSMutableArray()
    var previewUrls = NSMutableArray()

    func searchBarSearchButtonClicked(searchBar: UISearchBar) {
        searchBar.resignFirstResponder()

        let searchWord: String? = searchBar.text?.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())

        if let searchWord = searchWord {
            let urlString:String = "http://itunes.apple.com/search?term=\(searchWord)&country=JP&lang=ja_jp&media=music&entity=song&attribute=artistTerm&limit=30"
            let url:NSURL! = NSURL(string:urlString)

            let task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: {data, response, error in
                let json = JSON(data: data!)
                for var i = 0; i < json["results"].length; i++ {
                    self.trackNames[i] = "\(json["results"][i]["trackName"])"
                    self.previewUrls[i] = "\(json["results"][i]["previewUrl"])"
                }
            })
            task.resume()
        }

        self.tableView.reloadData()
    }

Search Barのdelegateを設定

delegate、の意味は未だに理解しきれていないのだけど、英語で「代表、派遣」といった意味みたいです。一応、その部品の挙動や制御をコードに一任できるようにする、みたいな理解でいます。

設定は簡単で、検索バーからcontrolを押しながらドラッグして、下図のようにRoot View Controller自身にoutletを繋ぎます。すると、delegateという選択肢が出るのでこれを設定します。

26_searchBarDelegate

ちなみに、storyboardを開いた状態で、左にあるDocument Outlineから以下のように繋げることもできます。結果は同じです。

27_searchBarDelegateAnother

実行する

Xcodeの左上にある「実行」のボタンを押します。以下では、iPhone6sのシミュレータが起動します。

29_execute

試しに検索してみる

例として、上部の検索バーに「aiko」と入力し、検索を行います。

  • 30_initialScreen
  • 31_firstTap

ヒットした曲名がずらっと並ぶ

APIのパラメータにlimit=30を入れているので最大で30曲表示されます。また、右のように日本語での検索もできます。

  • 32_secondTap
  • 34_oneTapLoadAndJapanese

isLoadNowフラグで状態管理

画面キャプチャでは伝わりづらいのですが、ここでひとつ問題が発生します。実は、検索バーにキーワードを入力して検索を走らせても一発では検索結果は得られないのです。

この時点では、二回のEnterで検索が始まります。これはiTunes APIからの返り値を待たずにテーブルの更新が終わってしまうために起こっている現象だと思われます。よって、上記のコードを以下のように修正します。

class RootViewController: UITableViewController {

    @IBOutlet weak var searchBar: UISearchBar!

    var trackNames = NSMutableArray()
    var previewUrls = NSMutableArray()
    var isLoadNow = false

    func searchBarSearchButtonClicked(searchBar: UISearchBar) {
        searchBar.resignFirstResponder()

        let searchWord: String? = searchBar.text?.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())

        if let searchWord = searchWord {
            self.isLoadNow = true
            let urlString:String = "http://itunes.apple.com/search?term=\(searchWord)&country=JP&lang=ja_jp&media=music&entity=song&attribute=artistTerm&limit=30"
            let url:NSURL! = NSURL(string:urlString)

            let task = NSURLSession.sharedSession().dataTaskWithURL(url, completionHandler: {data, response, error in
                let json = JSON(data: data!)
                for var i = 0; i < json["results"].length; i++ {
                    self.trackNames[i] = "\(json["results"][i]["trackName"])"
                    self.previewUrls[i] = "\(json["results"][i]["previewUrl"])"
                }
                self.isLoadNow = false
            })
            task.resume()
            while isLoadNow {
                usleep(5)
            }
        }

        self.tableView.reloadData()
    }

曲名をタップしてみる

テーブル上のセルがタップされた時の制御を何も実装していないので、ただグレーになるだけで何も起きません。次はここを実装していきます。

35_tapNoWorking

AVKit Player View Controllerの設置

音楽を再生するためのViewとして、AVKit Player Controllerを使います。これまでと同じように右メニューから検索し、storyboard上に配置します。

36_avKitPlayer

配置したてでは以下のようにバランスが悪いので、これも同じようにSimulated MetricsからSizeのプルダウンメニューを開き、「iPhone 4-inch」とします。

37_placeAvKitPlayer

画面の遷移(segue)を作成

View間の画面遷移は、segueと呼ばれるもので繋ぐことで実現できます。今回はテーブルのセルをタップすることで遷移させたいので、以下のようにPrototype Cellsからcontrolを押しながら繋ぎます。

segueにはいろいろと種類があるようなのですが、とりあえず「Show」としておきます。

38_segueToAvKitPlayer

作成されたsegueをクリックし、以下のようにセルが青くハイライトされればOKです。

39_checkSegue

音楽を再生するための実装

AVKit Player View Controllerを制御するコードとして、DetailViewController.swiftを追加します。RootViewController.swiftと同様ですが、継承するのはAVPlayerViewControllerです。

40_CreateDetailViewController

継承できない?エラーが発生

ここであれ?となるのですが、おそらくはAVPlayerViewControllerを継承するための何かが足りないのではないかと。その辺りは詳しく調べてみないとわからないのだけど。

41_errorInAvKitPlayer

とりあえず、以下のようにAVFoundationとAVKitのimport文を記述することで解決しました。

import UIKit
import AVFoundation
import AVKit

class DetailViewController: AVPlayerViewController {

}

これをstoryboard上の、DetailViewControllerに紐付けるため、RootViewControllerの時と同じように右メニューのCustom Classからプルダウンで選択します。

43_selectDetailViewController

DetailViewControllerのコード

次に、DetailViewControllerの中身を書いていきます。やりたいことは、前の画面から再生用のURL(previewUrl)を受け取って再生する、という動きです。

class DetailViewController: AVPlayerViewController {

    var previewUrl: String?

    override func viewDidLoad() {
        super.viewDidLoad()

        if let previewUrl = previewUrl {
            player = AVPlayer(URL: NSURL(string: previewUrl)!)
            player?.play()
        }
    }

}

RootViewControllerにコード追加

受け取る側の実装ができたので、送る側にもコードを追加します。segueで画面遷移する際、タップされたセル(indexPath.row)のpreviewUrlを送ります。

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if let vc = segue.destinationViewController as? DetailViewController {
            if let indexPath = tableView.indexPathForSelectedRow {
                vc.previewUrl = previewUrls[indexPath.row] as? String
            }
        }
    }

再生の確認

キャプチャではわかりませんが、タップした楽曲が十数秒程度流れます。

45_startMusic

Githubに完成品を置いています

恥ずかしながらせっかくここまでブログとして備忘録を残したので、プロジェクトごとGithubに置くことにしました。記事執筆時点では動作しています。


参考: iTunes APIを使ったiOSアプリハンズオン(Swift)