[SwiftUI] SwiftUIとMusicKit/MediaPlayerFrameworkを使った音楽再生アプリの作り方

ライブラリにある音楽再生機能を持つアプリを開発するための手順を説明します。

作りたいアプリ仕様

以下のような仕様を作りたいですが、難しければ妥協するかも。

  • プレイリストを選択できる(複雑な操作を行いたくない)
  • 再生・停止の操作ができる
  • プレイリスト単位をリピートする(固定)




MediaPlayerFrameworkのドキュメントを一読

MediaPlayerFramework Documentを一読しました。

ざっくりまとめ

  • 音楽再生そのものは、MPMusicPlayerApplicationControllerから生成したオブジェクトで行う。ビデオ再生したければ、AVFoundationのAVPlayerを使う必要あり。
  • バックグラウンド再生させたいならば、Capabilityをenableにする必要がある
  • 音楽のライブラリにアクセスするならば、info.plistに追記必要。
  • メディア情報は、MPMediaItemで管理される。音楽だけでなく、ビデオも同様に管理されている。
  • 音楽再生以外には、このフレームワークを使うべからず。

note: Apple Musicのケースにも言及してますが、今回はスコープ外として読んでません。

音楽ライブラリへのアクセス

ユーザーのライブラリにアクセスするならば、info.plistに、記載する必要あり。記載がないとアプリがクラッシュします。

キー値
Privacy - Media Library Usage Description
値:
アクセス理由を記載

プレイリストの取得

MPMediaQueryを使って、検索します。アルバムやアーティスト等でのQueryは、すでに用意されています。
プレイリストを取得するためのQuery作成は、MPMediaQuery.playlists()でした。

返り値のタイプが、MPMediaQueryなので、結果に対して、collectionsプロパティにアクセスすると[MPMediaItemCollection]? が取得できます。

Playlistを取得

let query = MPMediaQuery.playlists()
if let collections = query.collections {
    for item in collections {
        if let playlist = item as? MPMediaPlaylist {
            DispatchQueue.main.async {
                self.playlists.append(playlist)
            }
        }
    }
}

プレイリストから曲データの取得

Playlistは、MPMediaCollectionsなので、itemsプロパティにアクセスすると曲情報を取得することができます。

playlistから曲を取得してリスト表示

List {
    ForEach(userSelection.playlist!.items, id: \.self) {item in
        Text("\(item.title!)")
    }
}

前半まとめ

ここまでで、音楽ライブラリへアクセスするときの許諾の方法、プレイリストの取得方法、プレイリストから曲情報の取得と見てきたので、作りたいアプリの基本機能はカバーできたと思います。あとは、SwiftUIと組み合わせて簡単なUIを作っていきます。




アプリ用のモデル作成

SwiftUIのUIを更新するためのベースとなるモデルを作ります。

以下の、AppModelという”アプリ全体管理モデル”でプレイリスト全体を管理します。UserSelectionでは、ユーザーの選択したプレイリストを記憶して、再生・停止等の処理を行います。

アプリ全体管理モデル

class AppModel: ObservableObject {
    @Published var playlists:[MPMediaPlaylist] = []
    @Published var userSelection: UserSelection = UserSelection()
    @Published var playable: Bool = false
    
    init() {
		...
    }
}
プレイリスト選択管理モデル

class UserSelection: ObservableObject {
    @Published var playlist: MPMediaPlaylist?
}

音楽ライブラリへのアクセス許可

ライブラリへのアクセス許可は、アプリ全体を管理するAppModelを初期化するときに確認しましょう。アクセスが認証されているならば、プレイリストを取得して内部に保持することにしましょう。コードとしては、以下のようになります。

AppModel アクセスリクエスト

class AppModel: ObservableObject {
    @Published var playlists:[MPMediaPlaylist] = []
    @Published var userSelection: UserSelection = UserSelection()
    @Published var playable: Bool = false
    
    init() {
        if SKCloudServiceController.authorizationStatus() == .notDetermined {
            // ask for access
            SKCloudServiceController.requestAuthorization { (status) in
                switch status {
                case .denied, .restricted:
                    return // no way to play music
                case .authorized:
                    self.playable = true
                    self.updatePlaylist()
                    return
                default:
                    return
                }
            }
        } else {
            self.playable = true
            self.updatePlaylist()
        }
    }
    
    func updatePlaylist() {
        // get playlist and save into playlists
        return
    }
}

プレイリストを取得してリスト表示

MPMediaQueryを使って取得したものを、playlistsに保存します。SwiftUIはこの変数を見てGUIを更新しますので、メインスレッドで変更します。

コード

class AppModel: ObservableObject {
    @Published var playlists:[MPMediaPlaylist] = []
    @Published var userSelection: UserSelection = UserSelection()
    @Published var playable: Bool = false
    
    init() {
        if SKCloudServiceController.authorizationStatus() == .notDetermined {
            // ask for access
            SKCloudServiceController.requestAuthorization { (status) in
                switch status {
                case .denied, .restricted:
                    return // no way to play music
                case .authorized:
                    self.playable = true
                    self.updatePlaylist()
                    return
                default:
                    return
                }
            }
        } else {
            self.playable = true
            self.updatePlaylist()
        }
    }
    
    func updatePlaylist() {
        let query = MPMediaQuery.playlists()
        if let collections = query.collections {
            for item in collections {
                if let playlist = item as? MPMediaPlaylist {
                    DispatchQueue.main.async {
                        self.playlists.append(playlist)
                    }
                }
            }
        }
        return
    }
}

メイン画面の作成

せっかくプレイリストが取得できたので、表示するようにします。

アプリモデルのenvironmentObject設定

まずは、AppModelをenvironmentObjectとして使えるように設定します。

scene(_ scene: UIScene, ...)を変更

let appModel = AppModel()  // ⬅️  追加
        
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
    .environmentObject(appModel)    // ⬅️  追加

プレイリストのリストを表示するビューをPlayListViewという名前で作ることにしましょう。

ContentView.swift

struct ContentView: View {
    @EnvironmentObject var appModel: AppModel
    var body: some View {
        VStack {
            PlayListView(userSelection: appModel.userSelection)
                .padding()
        }
        
    }
}

PlayListView

特に工夫もなく、リスト表示するビューです。

コード

struct PlayListView: View {
    @EnvironmentObject var appModel: AppModel
    @ObservedObject var userSelection: UserSelection

    var body: some View {
        List(selection: $userSelection.playlist, content: {
            ForEach(appModel.playlists, id: \.self) { item in
                Text("\(item.name!)  \(item.count)")
            }
        })
            .environment(\.editMode, .constant(.active))
    }
}

struct PlayListView_Previews: PreviewProvider {
    static var previews: some View {
        PlayListView(userSelection: UserSelection())
    }
}

ここで、一度実行してみましょう。

playlist 表示
シミュレータで実行しているので、寂しいですが、プレイリスト名とそのプレイリスト中の曲数を表示しています。

次に、プレイリストを選択したら、曲を表示するようにして見ましょう。

選択されたプレイリストから曲データを取得してリスト表示

PlayListViewで選択されたプレイリストは、userSelection.playlistに保存されるようになっていますから、それを見て、曲をリスト表示するビューを作ります。SongListViewとしましょう。

SongListView

struct SongListView: View {
    @ObservedObject var userSelection: UserSelection

    var body: some View {
        Group {
        if userSelection.playlist != nil {
            List {
                ForEach(userSelection.playlist!.items, id: \.self) {item in
                    Text("\(item.title!)")
                }
            }
        } else {
            EmptyView()
        }
        }
    }
}

ContentView.swiftの方も、SongListViewを追加します。

コード

struct ContentView: View {
    @EnvironmentObject var appModel: AppModel

    var body: some View {
        VStack {
            PlayListView(userSelection: appModel.userSelection)
                .padding()
            SongListView(userSelection: appModel.userSelection)
                .padding()
        }
    }
}

PlayListとSongList

ここでもシミュレータでの実行なので、下側のリスト:曲のリストは空ですが、音楽の入っている実機で実行すると、実際の曲名が表示されます。

曲を再生/停止

曲の再生には、MPMusicPlayerApplicationController.applicationQueuePlayerからインスタンス化したものを使います。
Playするときに、一旦キューを空にしてから、プレイリストアイテムをキューに追加します。
あとは、play()やstop()でOKです。

UserSelection

class UserSelection: ObservableObject {
    @Published var playlist: MPMediaPlaylist?
    let musicPlayer:MPMusicPlayerApplicationController = MPMusicPlayerApplicationController.applicationQueuePlayer

    init() {
        self.playlist = nil
    }
    
    init(playlist: MPMediaPlaylist) {
        self.playlist = playlist
    }
    
    public func play() -> Bool {
        guard let playlist = playlist else { return false }

        musicPlayer.repeatMode = .all
        
        musicPlayer.perform(queueTransaction: { (mutableQueue) in
            // make queue empty
            let oldItems = mutableQueue.items
            for item in oldItems {
                mutableQueue.remove(item)
            }
            
            let mediaItemQueDesc = MPMusicPlayerMediaItemQueueDescriptor(itemCollection: playlist)
            mutableQueue.insert(mediaItemQueDesc, after: nil)
        }) { (queue, error) in
            if queue.items.count > 0 {
                self.musicPlayer.play()
            }
            if (error != nil) {
                print("\(error)")
            }
        }
        return true
    }
    
    public func stop() -> Bool {
        self.musicPlayer.stop()
        return true
    }
}

Start/StopのUIも作ります。

PlayButtonView

struct PlayButtonView: View {
    @ObservedObject var userSelection: UserSelection

    var body: some View {
        VStack {
            Text("Playing: XXXX")
            HStack {
                Button(action: {
                    print("play")
                    _ = self.userSelection.play()
                }, label: {
                    Image(systemName: "play")
                })
                    .padding()
                Button(action: {
                    print("stop")
                    _ = self.userSelection.stop()
                }, label: {
                    Image(systemName: "stop")
                })
                .padding()
            }
        }
        .padding()
    }
}
最終的なContentView.switf

struct ContentView: View {
    @EnvironmentObject var appModel: AppModel

    var body: some View {
        VStack {
            PlayButtonView(userSelection: appModel.userSelection)
            PlayListView(userSelection: appModel.userSelection)
                .padding()
            SongListView(userSelection: appModel.userSelection)
                .padding()
        }
        
    }
}

バックグラウンド再生

アプリがバックグラウンドになっても再生を継続したいときには、アプリの"Signing & Capabilities"の"Background Modes"で"Audio, AirPlay, and Picture in Picture”にチェックを入れます。

background再生設定

最終的なコード

アプリデータモデル AppData.swift

//
//  AppModel.swift
//  AudioPlayer
//
//  Created by Tomoaki Yagishita on 2020/05/24.
//  Copyright © 2020 SmallDeskSoftware. All rights reserved.
//

import Foundation
import Combine
import MediaPlayer
import StoreKit

class AppModel: ObservableObject {
    @Published var playlists:[MPMediaPlaylist] = []
    @Published var userSelection: UserSelection = UserSelection()
    @Published var playable: Bool = false
    
    init() {
        if SKCloudServiceController.authorizationStatus() == .notDetermined {
            // ask for access
            SKCloudServiceController.requestAuthorization { (status) in
                switch status {
                case .denied, .restricted:
                    return // no way to play music
                case .authorized:
                    self.playable = true
                    self.updatePlaylist()
                    return
                default:
                    return
                }
            }
        } else {
            self.playable = true
            self.updatePlaylist()
        }
    }
    
    func updatePlaylist() {
        let query = MPMediaQuery.playlists()
        if let collections = query.collections {
            for item in collections {
                if let playlist = item as? MPMediaPlaylist {
                    DispatchQueue.main.async {
                        self.playlists.append(playlist)
                    }
                }
            }
        }
        return
    }
}

class UserSelection: ObservableObject {
    @Published var playlist: MPMediaPlaylist?
    let musicPlayer:MPMusicPlayerApplicationController = MPMusicPlayerApplicationController.applicationQueuePlayer

    init() {
        self.playlist = nil
    }
    
    init(playlist: MPMediaPlaylist) {
        self.playlist = playlist
    }
    
    public func play() -> Bool {
        guard let playlist = playlist else { return false }

        musicPlayer.repeatMode = .all
        
        musicPlayer.perform(queueTransaction: { (mutableQueue) in
            // make queue empty
            let oldItems = mutableQueue.items
            for item in oldItems {
                mutableQueue.remove(item)
            }
            
            let mediaItemQueDesc = MPMusicPlayerMediaItemQueueDescriptor(itemCollection: playlist)

            mutableQueue.insert(mediaItemQueDesc, after: nil)
        }) { (queue, error) in
            if queue.items.count > 0 {
                self.musicPlayer.play()
            }
            if (error != nil) {
                print("\(error)")
            }
        }
        return true
    }
    
    public func stop() -> Bool {
        self.musicPlayer.stop()
        return true
    }
}
ContentView.swift

//
//  ContentView.swift
//  AudioPlayer
//
//  Created by Tomoaki Yagishita on 2020/05/24.
//  Copyright © 2020 SmallDeskSoftware. All rights reserved.
//

import SwiftUI
import MediaPlayer

struct ContentView: View {
    @EnvironmentObject var appModel: AppModel

    var body: some View {
        VStack {
            PlayButtonView(userSelection: appModel.userSelection)
            PlayListView(userSelection: appModel.userSelection)
                .padding()
            SongListView(userSelection: appModel.userSelection)
                .padding()
        }
        
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
PlayListView

//
//  PlayListView.swift
//  AudioPlayer
//
//  Created by Tomoaki Yagishita on 2020/05/24.
//  Copyright © 2020 SmallDeskSoftware. All rights reserved.
//

import SwiftUI
import MediaPlayer

struct PlayListView: View {
    @EnvironmentObject var appModel: AppModel
    @ObservedObject var userSelection: UserSelection

    var body: some View {
        List(selection: $userSelection.playlist, content: {
            ForEach(appModel.playlists, id: \.self) { item in
                Text("\(item.name!)  \(item.count)")
            }
        })
            .environment(\.editMode, .constant(.active))
    }
}

struct PlayListView_Previews: PreviewProvider {
    static var previews: some View {
        PlayListView(userSelection: UserSelection())
    }
}
SongListView

//
//  SongListView.swift
//  AudioPlayer
//
//  Created by Tomoaki Yagishita on 2020/05/24.
//  Copyright © 2020 SmallDeskSoftware. All rights reserved.
//

import SwiftUI
import MediaPlayer

struct SongListView: View {
    @ObservedObject var userSelection: UserSelection

    var body: some View {
        Group {
        if userSelection.playlist != nil {
            List {
                ForEach(userSelection.playlist!.items, id: \.self) {item in
                    Text("\(item.title!)")
                }
            }
        } else {
            EmptyView()
        }
        }
    }
}

struct SongListView_Previews: PreviewProvider {
    static var previews: some View {
        SongListView(userSelection: UserSelection())
    }
}
PlayButtonView

//
//  PlayButtonView.swift
//  AudioPlayer
//
//  Created by Tomoaki Yagishita on 2020/05/24.
//  Copyright © 2020 SmallDeskSoftware. All rights reserved.
//

import SwiftUI

struct PlayButtonView: View {
    @ObservedObject var userSelection: UserSelection

    var body: some View {
        VStack {
            Text("Playing: XXXX")
            HStack {
                Button(action: {
                    print("play")
                    _ = self.userSelection.play()
                }, label: {
                    Image(systemName: "play")
                })
                    .padding()
                Button(action: {
                    print("stop")
                    _ = self.userSelection.stop()
                }, label: {
                    Image(systemName: "stop")
                })
                .padding()
            }
        }
        .padding()
    }
}

struct PlayButtonView_Previews: PreviewProvider {
    static var previews: some View {
        PlayButtonView(userSelection: UserSelection())
    }
}

Note: 再生中の曲名表示用にTextを入れていますが、実装してません。




まとめ

以下の機能を持つアプリを作りました。

  • プレイリストをリスト表示
  • 選択されたプレイリストに含まれる曲をリスト表示
  • 選択されたプレイリストを再生・停止

ドキュメントを斜め読みして、SwiftUI と MediaPlayer を組み合わせて作ったアプリですが、AirPlay 等にも対応するので、既存のアプリに音楽再生機能を追加することはFrameworkがよくできていることもあり簡単なことがわかりました。

説明は以上です。




1 COMMENT

Bryan

Very helpful! I am trying to build an app using MusicKit and SwiftUI, so I learned a lot here that I will use in the future.

返信する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です