Sponsor Link
作りたいアプリ仕様
以下のような仕様を作りたいですが、難しければ妥協するかも。
- プレイリストを選択できる(複雑な操作を行いたくない)
- 再生・停止の操作ができる
- プレイリスト単位をリピートする(固定)
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]? が取得できます。
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プロパティにアクセスすると曲情報を取得することができます。
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を初期化するときに確認しましょう。アクセスが認証されているならば、プレイリストを取得して内部に保持することにしましょう。コードとしては、以下のようになります。
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として使えるように設定します。
let appModel = AppModel() // ⬅️ 追加
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
.environmentObject(appModel) // ⬅️ 追加
プレイリストのリストを表示するビューをPlayListViewという名前で作ることにしましょう。
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())
}
}
ここで、一度実行してみましょう。
シミュレータで実行しているので、寂しいですが、プレイリスト名とそのプレイリスト中の曲数を表示しています。
次に、プレイリストを選択したら、曲を表示するようにして見ましょう。
選択されたプレイリストから曲データを取得してリスト表示
PlayListViewで選択されたプレイリストは、userSelection.playlistに保存されるようになっていますから、それを見て、曲をリスト表示するビューを作ります。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()
}
}
}
ここでもシミュレータでの実行なので、下側のリスト:曲のリストは空ですが、音楽の入っている実機で実行すると、実際の曲名が表示されます。
曲を再生/停止
曲の再生には、MPMusicPlayerApplicationController.applicationQueuePlayerからインスタンス化したものを使います。
Playするときに、一旦キューを空にしてから、プレイリストアイテムをキューに追加します。
あとは、play()やstop()でOKです。
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も作ります。
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 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”にチェックを入れます。
最終的なコード
//
// 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
// 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.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.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.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がよくできていることもあり簡単なことがわかりました。
説明は以上です。
Sponsor Link
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.