20200824のSwiftに関する記事は9件です。

Xcodeの開発で、ファイルを保存するだけで動的に中身が書き換えられるようにしてみた

Xcodeの開発で、通常であれば、ファイルを保存し、ビルドし、プログラムを起動するという手順をとる。プログラムを起動した状態で、終了させずに、その中身を書き換えるということは、通常はできない。だが、injectionという仕組みを使うと、できるようになる。

1. 「InjectionIII」を使ってみる

John Holdsworth氏により、「InjectionIII」というアプリが公開されている。これを使うと、Injectionを簡単に実行できるようになっている。
https://github.com/johnno1962/InjectionIII
以下、使ってみる。

1.1. アプリをインストール

InjectionIIIアプリをインストールする。
https://apps.apple.com/jp/app/injectioniii/id1380446739?mt=12

インストールされたら、実行する→Status menuにInjectionIIIアプリのアイコンが表示されたことを確認する→「Help/README」を選ぶと、以下のページに接続される。
https://github.com/johnno1962/InjectionIII

1.2. サンプルをダウンロードする

以下、Injection IIIのホームページである。
http://johnholdsworth.com/injection.html

以下より、サンプルプログラムをダウンロードできる。
http://johnholdsworth.com/GettingStarted.zip
解凍し、「~/dev/GettingStarted/」となるようにする。

1.3. Xcodeからサンプルを開く

GettingStarted.xcodeprojをダブルクリックして起動。→Open

1.4. InjectionIIIアプリから、GettingStartedを接続する

Status menuのInjectionIIIから「Open project」を選択→「~/dev/GettingStarted/」を選択→「Select Project Directory」

1.5. 実行する

Cmd-Rで実行→シミュレーターに「Master」と表示される→「+」を押すと、現在時刻が表示される。それをクリックする→現在時刻と「CHANGEME」が表示される。

Xcodeに戻る→Cmd-1→DetailViewController.swiftを選択→" CHANGEME"の個所を、たとえば" CHANGED!"に変更する→Cmd-Sでセーブとすると、即座に画面上の「CHANGEME」だったところが、「CHANGED!」に変わる。これがインジェクションである。

2. 独自のプログラムからインジェクションしてみる

2.1. なにかアプリを作る

まずは、なにかシンプルなアプリを開発する。文字が表示されるものが良い。
Xcodeを起動→Create a new Xcode Project→iOS→「Single View App」→Next→Product Name:「InjectionTest」、User Interface: Storyboard→Next→「~/dev」を指定→Create
ViewController.swiftに、以下のようにshow()を追加。viewDidLoad()から呼ばれるようにする。

ViewController.swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        show()
    }
    func show() {
        let button = UIButton(frame: CGRect(x: 40, y: 100, width: 200, height: 100))
        button.backgroundColor = .cyan
        button.setTitle("Hello, world!", for: .normal)
        button.setTitleColor(.black, for: .normal)
        view.addSubview(button)
    }
}

まずはこの段階で実行してみる。Cmd-R→ビルドされ、実行される。Simulatorが起動して、「Hello, world!」が表示される。

ViewController.swiftに戻り、試しに"Hello, world!""Hello, Japan!"に修正してCmd-Sで保存しても、反映されない。当然である。

再度Cmd-Rすると、一旦シミュレーター上のアプリが終了し、再度立ち上げられ、今度は"Hello, Japan!"に修正されている。3秒程度で立ち上がる。通常は、このようにアプリを終了し、再読み込みするという手順で開発する。ビルドが早いので、3秒程度でこのサイクルは実行できる。実用上はあまり不満は持たれないかもしれない。

2.2. Linker Flagsを設定する

Xcodeに戻る。Cmd-1→InjectionTestのプロジェクトを選択→PROJECT: InjectionTest→Build Settings→Linking→Other Linker Flags→ここにカーソルを乗せると左に三角が表示される→クリックする→Debugの右の「+」をおす→Debug→Any Architecture | Any SDK:「-Xlinker -interposable」→リターンを押すと確定する

2.3. Bundleを追加

AppDelegate.swiftにBundleを追加する。

AppDelegate.swift
        #if DEBUG
        Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
        #endif

参考までに、AppDelegate.swiftの該当するメソッド全体である。

AppDelegate.swift
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        #if DEBUG
        Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
        #endif
        return true
    }

2.4. injected()を追加

ViewController.swiftに、injectedというメソッドを追加する。

ViewController.swift
    @objc func injected() {
        show()
    }

参考までに、ViewController classの全体である。

ViewController.swift
class ViewController: UIViewController {
    @objc func injected() {
        show()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        show()
    }
    func show() {
        let button = UIButton(frame: CGRect(x: 40, y: 100, width: 200, height: 100))
        button.backgroundColor = .cyan
        button.setTitle("Hello, world!", for: .normal)
        button.setTitleColor(.black, for: .normal)
        view.addSubview(button)
    }
}

2.5. InjectionIIIにProjectを指定する

Status menuのInjectionIIIから「Open project」を選択→「~/dev/InjectionTest」を選択→「Select Project Directory」

2.6. 起動する

Cmd-R→Simulatorが起動して、「Hello, world!」が表示される。以下のように出力される。

? Injection connected ?
? Watching /Users/eto/dev/InjectionTest/**

2.7. 編集する

ViewController.swiftで、該当する行を以下のように編集してみる。

ViewController.swift
        button.setTitle("Hello, Japan!", for: .normal)

Cmd-Sで、ファイルをセーブする。そうすると、シミュレーター上の「Hello, world!」が、即座に(1秒以内くらいで)「Hello, Japan!」に変わる。以下のように出力される。

? *** Compiling /Users/eto/dev/InjectionTest/InjectionTest/ViewController.swift ***
? Loading .dylib ...
objc[31231]: Class _TtC13InjectionTest14ViewController is implemented in both /Users/eto/Library/Developer/CoreSimulator/Devices/97670822-70F9-46B8-87F7-5545DF54E516/data/Containers/Bundle/Application/82DDD3CB-9924-4E5C-BCCC-1AE2A8A9E3AD/InjectionTest.app/InjectionTest (0x107b53b40) and /var/folders/94/shwk5bk14l5fx43cggr_n04m0000gn/T/com.johnholdsworth.InjectionIII/eval106.dylib (0x110d9c280). One of the two will be used. Which one is undefined.
? Loaded .dylib - Ignore any duplicate class warning ^
? Injected 'ViewController'
? Replacing InjectionTest.ViewController.__allocating_init(coder: __C.NSCoder) -> Swift.Optional<InjectionTest.ViewController>
? Replacing InjectionTest.ViewController.__allocating_init(nibName: Swift.Optional<Swift.String>, bundle: Swift.Optional<__C.NSBundle>) -> InjectionTest.ViewController
? Replacing InjectionTest.ViewController.viewDidLoad() -> ()
? Replacing InjectionTest.ViewController.show() -> ()
? Replacing InjectionTest.ViewController.injected() -> ()
? Class ViewController has an @objc injected() method. Injection will attempt a "sweep" of all live instances to determine which objects to message. If this crashes, subscribe to the global notification "INJECTION_BUNDLE_NOTIFICATION" to detect injections instead.

内部で起こっていることを説明すると、ViewController.swiftを常にウォッチし、編集されたのを検知したら、即座にそれをCompileし、動的にロードし、メソッドを置き換えている。その後、injected()が呼ばれ、表示が切り替わる。このようにすると、プログラムを実行している間に、動的にメソッドを書き換えできるため、プログラム開発効率が高まると考えられる。

ここまでのファイルを、以下に置く。このまま実行できるはずである。
https://github.com/eto/InjectionTest

done!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリで音楽や動画のバックグラウンド再生の実装方法(他アプリ音声を停止しないやり方も)

AVPlayerやAVAudioPlayerなどで音声再生や動画再生の実装をする場合デフォルトのままだとアプリをバックグラウンドへ移行したりスリープをした時に再生が自動で停止します。
以下の実装を行うことで再生を継続させることができます。

簡単な手順でOKです。

手順1

  1. プロジェクトファイルを開く
  2. メニュー -> Editor -> Add Capability -> Background Modesを選択
  3. Audio, AirPlay, and Picture in PictureのチェックマークをONにする

スクリーンショット 2020-08-24 19.18.08.png

手順2

プロジェクト内の任意の場所に以下のコードを書きます

// アプリ初期化時等
do {
    try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
} catch _ {
    NSLog("audio session set category failure")
}

// 音声、動画再生時
do {
    try AVAudioSession.sharedInstance().setActive(true)
} catch _ {
    NSLog("audio session active failure")
}

注意点として、上記の実装をしているともし別のアプリで音声を再生している場合
上記のsetActiveが呼び出された時点で別アプリの音声再生が停止します。
なのでsetActiveは自分のアプリでの音声再生を開始する時点で呼ぶのが望ましいです。

他アプリの音声再生を停止せずに再生する方法

AVAudioSessionのsetCategory時にoptionを設定します

do {
    try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback,
                                                            options: [AVAudioSession.CategoryOptions.mixWithOthers])
} catch _ {
    NSLog("audio session set category failure")
}

上記はmixWithOthersを指定しましたが、duckOthersというのも存在します
違いは
- mixWithOthers → 他アプリの音声の音量を下げない
- duckOthers → 他アプリの音声の音量を下げる
場面に応じて使い分けましょう!

他のoption
- allowBluetooth → 音声再生時にはこのoption指定は無視される(bluetoothイヤホンを利用できるようになるとかそういうものではない)
- defaultToSpeaker → イヤホンやヘッドセットを利用しない場合にいわゆる電話の耳を当てる部分から音が出るのがデフォルトになっているが、それをスピーカーから音が出る状態にするoption

公式ドキュメント:
AVAudioSession

終わりです

Brewus,Inc.
株式会社ブリューアス
https://brewus.co.jp

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] textFieldのclearButtonMode

textFieldは中身を全部消すことができるclearButtonが実装できます。
画像の右端にあるバツマークです
スクリーンショット 2020-08-24 18.36.53.png

これの表示パターンはclearButtonModeの設定で変更することができます。

.always

.alwaysはtextFieldに文字列があれば常に出現します。

textField.clearButtonMode = .always

.never

.neverはボタンを表示しません

textField.clearButtonMode = .never

.unlessEditing

.unlessEditingはtextFieldに文字列があり、textFieldが選択されていない時に出現します

textField.clearButtonMode = .unlessEditing

.whileEditing

.whileEditingはtextFieldに文字列があり、textFieldが選択されている時に出現します

textField.clearButtonMode = .whileEditing

storyboard上での実装

textFieldのClearButtonの部分を変更すれば実装できます

スクリーンショット 2020-08-24 19.01.19.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS14】WidgetKitのテンプレートを読み解く

はじめに

9月リリースが噂されているiOS14から、Widgetが追加されます。
表現が固定されていたホーム画面での体験が大きく変わる機能であり、
多くの人が注目しているかと思います?✨

Apple_ios14-widgets-redesigned_06222020_inline.jpg.large_2x.jpg

公式ブログ

よし!自分のアプリにもWidgetを追加しよう!と手を動かそうとしている皆さんに向けて、
既存アプリへの追加手順の紹介と、テンプレートコードの読み解きをします。

本記事は、最低限の挙動と実装を確認したい人に向けての解説記事です。
WidgetKitの利用シーンやWidgetKitの詳しい解説はこちらの記事に丁寧にまとまっているので、割愛させていただきます。
https://qiita.com/shiz/items/309349d9cdb75084e74e

※本記事はXcode12 beta5時点の情報です、beta版の開発画面のスクリーンショットはNDA締結により掲載しておりません。

1.追加手順

Xcodeを開き

  1. File > New > Target を選択
  2. Application Extension group セクションから Widget Extension を選択
  3. Extension の名前をつける

リファレンスには
4. If the widget provides user-configurable properties, check the Include Configuration Intent checkbox.
とありますが、beta5では選択肢出てこず(自分だけ…?)、デフォでuser-configurableのテンプレートが生成されました。

公式リファレンス: https://developer.apple.com/documentation/widgetkit/creating-a-widget-extension

また、Application ExtensionごとにiOS Deployment Targetを設定できるので
iOS14未満をサポートしている場合も対応できそうです。
自分はiOS13対応アプリで正常挙動を確認しました。
(もちろんiOS13ではWidgetは利用できません、iOS12以下未検証)

CharacterWidgetと命名して追加すると
CharacterWidget.swift
CharacterWidget.intentdefinition
Assets.xcassets
Info.plist
のファイルが用意されます。
(Character画像をランダムに表示するWidgetを実装しようと思った命名)

2.テンプレートコードの読み解き

CharacterWidget.swiftのコード内に日本語コメントを含めながら
Widgetを構成する要素を説明します。

Widget

まずはWidgetを定義します。
テンプレートでは端末に基づいた時間が表示されるよう実装されていました。

@main
struct CharacterWidget: Widget {
    let kind: String = "CharacterWidget" // widgetの識別子、何をするwidgetか説明すべき

    var body: some WidgetConfiguration {
        // Configuration
        // StaticConfigration: ユーザが設定不可(ex.株価, ニュース)
        // IntentConfiguration: ユーザが設定可(ex. 場所ごとの天気, 荷物追跡): ← テンプレはこちらが実装されているが時間の表示だけであればStaticで良い
        IntentConfiguration(
            kind: kind,
            intent: ConfigurationIntent.self, // ユーザが設定可能なプロパティを定義するカスタムインテント、.intentdefinition からコード生成される
            provider: Provider() // Timeline セクションで解説
        ) { entry in // SwiftUIのViewを含むクロージャー、ProviderからTimelineEntryパラメーターが渡される
            CharacterWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("? Widget") // Widgetギャラリー(設定画面)にてユーザに表示するタイトル
        .description("This is ? widget.") // 同上メッセージ
        // サイズの設定も加えることができる、デフォルトは3つ全て .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

ConfigurationIntent
は、例えばユーザがアプリ内で選択したキャラクターのWidgetを表示したい場合などに実装します。
テンプレートではデフォルトで実装してあるが特に利用していないので、ユーザ設定が必要ない場合はStaticに書き換えてください。

        StaticConfiguration(
            kind: kind,
            provider: StaticProvider()
        ) { entry in
            CharacterWidgetEntryView(entry: entry)
        }

公式リファレンス: https://developer.apple.com/documentation/widgetkit/intentconfiguration

TimelineProvider

WidgetではTimelineという、いつどのViewを表示するか管理する仕組みを実装します。
まずはEntryを定義します。

struct SimpleEntry: TimelineEntry {
    let date: Date // 表示する時間
    let configuration: ConfigurationIntent
}

続いて、IntentTimelineProviderの実装です。
テンプレートでは1時間おきに5回、時間を更新して表示されるよう実装されていました。

struct Provider: IntentTimelineProvider {
    /// ユーザ設定可能の場合、初めて表示する時に表示されるプレースホルダー
    /// ウィジェットが何を表示するかについて一般的な考えをユーザに提供するべき
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent())
    }

    /// 表示するSnapshotを定義
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {

        let entry = SimpleEntry(date: Date(), configuration: configuration)
        // context.isPreview = true の時、widget galleryに表示されているので、サーバーからの取得がある場合はフラグを立ててローディング表示など実装するとよい
        // ex) if context.isPreview && !hasFetchedGameStatus { ... }
        completion(entry)
    }

    /// タイムラインを定義
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // 現在の時間から開始して、1時間おきに切り替わる5つのEntryで構成されるタイムラインを生成
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            // hourOffset時間後の時間を生成
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, configuration: configuration)
            entries.append(entry)
        }
        // 配列に含めてTimelineにする
        let timeline = Timeline(entries: entries, policy: .atEnd)// policy: .atEnd タイムライン終了後新しいタイムラインを要求するポリシー
        completion(timeline)
    }
}

Timelineのpolicyは.adEndの他に

  • never: 新しい要求がくるまで何もしない
  • after(_ date: Date): タイムラインをリクエストする日時を指定

を選択できます。

こちらもIntentを利用しない場合はStaticに書き換えてください。

struct StaticProvider: TimelineProvider {
    typealias Entry = SimpleEntry

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        var entries: [SimpleEntry] = []

        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .minute, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate)
            entries.append(entry)
        }
        let timeline = Timeline(entries: entries, policy: .never)
        completion(timeline)
    }
}

View

最後に、実際に表示するWidgetを実装します。
テンプレートではシンプルにTextでentry.dateを表示するだけだったので、
3つのWidget family(サイズ)ごとの実装について追記しました。

/// widgetのView、SwiftUIで実装する
struct CharacterWidgetEntryView : View {
    @Environment(\.widgetFamily) var family: WidgetFamily// Widget familyをサポートする
    var entry: Provider.Entry

    @ViewBuilder // viewにバリエーションがある時宣言
    var body: some View {
        // サイズごとにViewを指定する実装
        switch family { 
        case .systemSmall: Text(entry.date, style: .time); Text("small")
        case .systemMedium: Text(entry.date, style: .time); Text("medium")
        case .systemLarge: Text(entry.date, style: .time); Text("large")
        @unknown default: fatalError()
        }
    }
}

SwiftUIで実装するのでもちろんPreviewもできます。

struct CharacterWidget_Previews: PreviewProvider {
    static var previews: some View {
        CharacterWidgetEntryView(entry: SimpleEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

以上です。大まかな流れが掴めていると幸いです?‍♂️

3.intentdefinition

CharacterWidget.intentdefinitionからテンプレートで実装されているConfigurationIntent周りのコードが自動生成されます。
Siri Shortcutsを実装したことがある方なら馴染みがあるかもしれません。

テンプレでは用意されているだけで利用していないので割愛しますが、
需要があればこちらについても解説する投稿をしようかなと思います。
https://developer.apple.com/documentation/widgetkit/making-a-configurable-widget

おわりに

Widgetのサンプルコードも用意されているのでぜひぜひ一度動かしてみて、
ユーザにとって需要のある機能実装の模索をしてみましょう?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iPhoneX系の画面の下のバー(Home Indicator)を非表示にする方法

IMG_7320.PNG

画面の下にいつもいるバー
Home Indicatorという名前です。
iPhoneXが発売されて数年、私は知りませんでした。
全画面でコンテンツを表示したい時にこのバーだけ表示されてしまっていると見た目が悪いですよね。

但し常に非表示にしておくということはできませんが操作をしていない場合に非表示になってくれる実装方法があります。

非表示にしたいViewControllerに以下の一行を追加します。

ViewController.swift
override var prefersHomeIndicatorAutoHidden: Bool { true }

公式ドキュメント:
prefersHomeIndicatorAutoHidden

常に表示するという状態と、操作していない場合は非表示にしたい、という二つの状態を切り替えたいというときは以下のような実装をします。

ViewController.swift
private var indicatorAutoHidden:Bool = false {
    didSet {
        self.setNeedsUpdateOfHomeIndicatorAutoHidden()
    }
}

override var prefersHomeIndicatorAutoHidden: Bool { 
    return self.indicatorAutoHidden
}

setNeedsUpdateOfHomeIndicatorAutoHiddenを呼び出すことですぐさま表示、非表示状態の更新を切り替えることができます。

公式ドキュメント:
setNeedsUpdateOfHomeIndicatorAutoHidden

終わりです

Brewus,Inc.
株式会社ブリューアス
https://brewus.co.jp

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

APIテスト

本文

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Literal について

Image Literal

スクリーンショット 2020-08-24 16.30.27.png

Color Literal

スクリーンショット 2020-08-24 16.30.39.png

Literal

スクリーンショット 2020-08-24 16.30.06.png

あとは
・Image Literalをダブルクリックすれば、Assetフォルダ内に保存されている画像を選択できる。

・Color Literalをダブルクリックすれば、色を選択できますスクリーンショット 2020-08-24 16.31.22.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ViewとしてのSwiftUI

SwiftUIを今までのXibやStoryboardの代わりとして使うための方法をまとめました。
Playgroundで実行します。
ViewControllerを使わない、SwiftUIとModelだけのアーキテクチャにはまだ慣れないですが、コードレイアウトとしてのSwiftUIはメリットが大きいので、取り入れていきたいです。

登場人物と関係

クラス 説明
MyViewController SwiftUIで作ったViewを表示するためのViewController
ContentView SwiftUI
ContentViewDelegate ボタンをタップした時のdelegate
ViewModel タップ回数の変数を格納するObservableObject

それぞれの主な役割は以下の図です。

SwiftUI+ViewController.jpg

基本的なMVCです。

実装

import UIKit
import PlaygroundSupport
import SwiftUI

class ViewModel: ObservableObject {

    @Published var count: Int = 0

    func increment() {
        count += 1
    }

}

protocol ContentViewDelegate: AnyObject {
    func didTapAdd()
}

struct ContentView: View {

    @ObservedObject var viewModel: ViewModel = .init()

    weak var delegate: ContentViewDelegate?

    var body: some View {
        VStack(spacing: 16) {
            Text("count : \(self.viewModel.count)")
            Button.init("Add") {
                debugPrint("Add")
                self.delegate?.didTapAdd()
            }
        }
    }

}

class MyViewController : UIViewController {

    private var contentView = ContentView.init()

    override func loadView() {
        super.loadView()
        view.backgroundColor = .white

        contentView.delegate = self

        let hostingVC = UIHostingController.init(rootView: contentView)
        hostingVC.view.frame = view.bounds
        view.addSubview(hostingVC.view)

        debugPrint(view.subviews)

        self.view = view

    }
}

extension MyViewController: ContentViewDelegate {

    func didTapAdd() {
        debugPrint("didTapAdd")
        contentView.viewModel.increment()
    }

}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

ソース
https://gist.github.com/usk2000/394d70cba327e095f2b8002542721697

画面

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AppleMusic連携で曲の固有のIDからMPMediaItemを取得したり曲を再生する時の知見

Media Player のFrameworkを使ってAppleMusic連携のアプリを作りたい。
しかしこんなんドキュメント見ても分からんだろっていう落とし穴があったりするので、いろいろ試してわかった知見を書く。

曲固有のID

単一の曲を表すMPMediaItemにはいろいろとプロパティがあるが、曲を区別するためのIDは次の二つがある。似ているようで役割が違う。

persistentID

自分のライブラリ内の固有のID(と思われる)
このIDがあれば、MPMediaItemは取得できる。

次のように、MPMediaPropertyPredicate,MPMediaQueryを使ってpersistentIDからMPMediaItemを取得する。
MPMediaItemPropertyPersistentIDというプロパティを使って絞り込もう。

private func findMPMediaItem(persistentIDString: String) -> MPMediaItem? {
        let predicate = MPMediaPropertyPredicate(value: persistentIDString, forProperty: MPMediaItemPropertyPersistentID)
        let songQuery = MPMediaQuery()
        songQuery.addFilterPredicate(predicate)

        if let items = songQuery.items {
            return items.first
        } else {
            return nil
        }
}

playbackStoreID

こちらはApple MusicやiTunes全体で固有のID(と思われる)

iTuneSearchAPIを例にすると、このようにAPIを叩けば曲の情報が取得できる。

$ curl "https://itunes.apple.com/lookup?id=1513201190" | jq

また、Apple Music APIでも曲の情報を取得できるが、こちらはtokenが必要だったりするので省略する。

ただしこのIDからMPMediaItemは取得できない!!?

↑わたしが調べる限り2020年8月時点では不可能っぽい。使えるようになったら教えてほしい。?

MPMediaItemPropertyPlaybackStoreIDといういかにも使えそうなプロパティはあるのだが、使うと次のようなエラーで怒られるぞ。stackoverflowにある通り、現状は取得する手段がない。

"MPMediaPropertyPredicate cannot filter using the playbackStoreID property"

曲を再生したい場合は?

persistentIDを使う場合

曲を再生する時はMPMusicPlayerControllerのこれをつかう

 open func setQueue(with itemCollection: MPMediaItemCollection)

次のようにMPMediaItemからMPMediaItemCollectionを作って、セットすれば再生できる

//itemsは↑みたいな方法で取得しとく
let mediaItemCollection = MPMediaItemCollection(items: items)
let musicPlayer: MPMusicPlayerController = MPMusicPlayerController.systemMusicPlayer
musicPlayer.setQueue(with: mediaItemCollection)

playbackStoreIDを使う場合

こちらの場合、MPMediaItemは取得できない

しかし曲の再生はできる。
MPMusicPlayerControllerのこれをつかう

open func setQueue(with storeIDs: [String])

次のように書けばok

let musicPlayer: MPMusicPlayerController = MPMusicPlayerController.systemMusicPlayer
musicPlayer.setQueue(with: ["1513201190"])

まとめ

persistentIDは自分のライブラリ固有のIDで、MPMediaItemを取得できる。
playbackStoreIDはApple MusicやiTunes全体で固有のIDで、API利用して曲の情報を取得できる。

Apple Music APIを使ったアプリを作りたい時はplaybackStoreIDを使うと良さそう。
Swiftでネイティブアプリを作りたい時は、persistentIDplaybackStoreIDどう使うか考えると良さそう。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む