- 投稿日:2022-01-25T17:36:24+09:00
iOSアプリにApple Music機能を追加(音楽の検索と再生) (MusicKit)
これまでは、楽曲を検索するにはApple Music APIを使用する必要がありました。新たに発表されたiOSのMusicKitフレームワークによって、楽曲を検索したり、アルバム内の曲をすべてリストアップしたり、楽曲を再生したりするためのリクエストをより簡単に行えるようになりました。 このフレームワークは、Androidシステムでも利用可能です。この記事では、これについては触れません。 MusicKitの新たなフレームワークには、新しくなったSwiftの並行処理も採用されています。データのリクエストに対するレスポンスを待つには、awaitコマンドを使用することができます。 この記事では、Apple Musicのカタログを検索し、音楽を再生するアプリの構築について説明します。 使用事例 Appleによると、可能となる使用事例にはゲーム内のBGM、トレーニング中の音楽、ソーシャルメディアなどが含まれています(しかしながら、ご使用の際にはライセンスや著作権などに関して、常にご確認をいただくようお願いします)。 最初にEntitlementを追加する 最初に https://developer.apple.com/account/resources/identifiers/list に移動し、アプリケーションを開くことを選択します。クリックしてApp Servicesタブを開き、MusicKitのトグルをオンにします。 リスト内にアプリケーションが表示されない場合は、XcodeのSigning & Capabilitiesビューで任意の機能を追加できます。これによりXcodeが自動的でに署名プロファイルを生成します。 トークンを生成する必要はありません。それはMusicKitによって自動的に生成されます。 Apple Music ライブラリ へのアクセス申請 ユーザーの視聴履歴や個人ライブラリにアクセスしたい場合は、許可を申請する必要があります。 まず、Info.plistファイル内に使用説明文字列NSAppleMusicUsageDescriptionを追加します。 次に、ユーザーの認可を要求します Button("Request authorization") { Task { await MusicAuthorization.request() } } 次のコードで、ユーザーがアクセスを許可しているか確認します。 Button("Reload authorization status") { self.currentAuthStatus = MusicAuthorization.currentStatus } ユーザーのApple Musicサブスク契約状況を確認 画面が表示されたら、ユーザーのApple Musicサブスク契約状況の問い合わせができます。 まず、ユーザーのサブスク契約状況を格納する変数musicSubscriptionを作成します。表示開始時には、これはnilとなっています。 @State private var musicSubscription: MusicSubscription? ビューが表示されたら、forループでawaitを利用して読み込みを続け、ユーザーの登録ステータスにおける変化を待ちます。 .task { for await subscription in MusicSubscription.subscriptionUpdates { self.musicSubscription = subscription } } 登録に関する更新情報があれば、ユーザーの登録ステータスを読み込むことができます。 ユーザーがApple Musicに登録できるかどうかを確認 musicSubscription.canBecomeSubscriberを利用して、ユーザーがApple Musicの登録者になれるかどうかを確認します。 この値がtrueの場合、ユーザーはApple Musicにまだ登録しておらず、登録可能であることを意味します。 MusicSubscriptionOfferオブジェクトを利用すれば、ユーザーにApple Musicへの登録を勧めることができます。SwiftUI内では、musicSubscriptionOfferビューモディファイアが使用可能です。 @State private var showAppleMusicSubscriptionOfferView: Bool = false if musicSubscription.canBecomeSubscriber { Button("Promopt the user to subscribe to Apple Music") { self.showAppleMusicSubscriptionOfferView = true } .musicSubscriptionOffer(isPresented: $showAppleMusicSubscriptionOfferView) } 変数musicSubscription.canBecomeSubscriberがfalseの場合は以下のいずれかが当てはまります: ユーザーのデバイスが制限されているか、もしくはApple Musicにサブスクライブできません。 ユーザーが既にApple Musicにサブスクライブしています。どちらに当てはまるのか確認したい場合は、別の変数を使ってユーザーがApple Musicカタログにアクセスできるかチェックする必要があります。 ユーザーがApple Musicのカタログにアクセスできるかどうかの確認 変数musicSubscription.canPlayCatalogContentを使用します。この変数がtrueであれば、ユーザーはApple Music内のコンテンツを再生できることを意味し、ほとんどの場合、ユーザーはApple Musicに加入していることになります。 ユーザーのiCloudミュージックライブラリへのアクセス確認 変数musicSubscription.hasCloudLibraryEnabledを使って、ユーザーがクラウドミュージックライブラリを有効にしているかどうかを確認することができます。 音楽を検索する Apple Musicの音楽カタログを検索するにはMusicCatalogSearchRequestオブジェクトを使用します。 public struct MusicCatalogSearchRequest { public init(term: String, types: [MusicCatalogSearchable.Type]) } term変数には検索語を指定してください。types変数には検索するデータの種類を指定します(例:曲、アルバム、プレイリストなど)。検索する種類は複数定義することができます。 たとえば、次のコードを使用すると検索語がBGMの曲を検索することができます。 let request = MusicCatalogSearchRequest(term: "BGM", types: [Song.self]) 次に、let response = try await request.response()を利用して、検索リクエストを実行します。awaitを使用しているので、検索結果が返ってくるまでコードはこの行で停止します。 Button("Perform search") { Task { do { let request = MusicCatalogSearchRequest(term: searchTerm, types: [Song.self]) self.isPerformingSearch = true let response = try await request.response() self.isPerformingSearch = false self.searchResultSongs = response.songs } catch { fatalError("Error") } } } この例では、応答をリクエストする直前に、isPerformingSearchをtrueに設定しています。応答の準備が整うと、isPerformingSearchはfalseに、searchResultSongsは検索によって取得された曲に設定されます。 特定のアイテムをリクエストする アイテム(曲またはアルバム)をリクエストするためには、そのIDを使用します。 let request = MusicCatalogResourceRequest<Album>(matching: \.id, equalTo: "1594311281") let response = try await request.response() let items: MusicItemCollection<Album> = response.items オブジェクトのさらなる情報を読み込む MusicKitは、すべての情報で応答するわけではありません。例えば、アルバムを検索する場合、アルバム内の曲の情報は含まれません。 MusicKitから受け取ったオブジェクトは、with関数を呼び出すことで更新できます。 例えば、アルバムオブジェクトがあり、曲を含むアルバムを取得したい場合、関数album.with([.artists, .tracks, .relationAlbums])を呼び出します。 このSwiftUIビューの例では、検索結果からアルバム変数を入力し、曲情報でオブジェクトを更新します。 struct AlbumDetailsView: View { @State private var updatedAlbumObject: Album? var album: Album var body: some View { AnyView() .task { self.updatedAlbumObject = try? await album.with([.tracks]) } } } self.updatedAlbumObject.tracks変数を使用して、このアルバム内のすべての曲を取得します。 if let tracks = self.updatedAlbumObject?.tracks { ForEach(tracks) { track in switch track { case .song(let songItem): SongInfoView(songItem: songItem) case .musicVideo(_): EmptyView() @unknown default: EmptyView() } } } else { ProgressView("Loading tracks...") } 音楽アイテムのアートワークを入手 .artworkプロパティを呼び出すと、音楽アイテム(曲やアルバムなど)のアートワークを取得できます。 if let artwork = songItem.artwork { ArtworkImage(artwork, height: 100) } SwiftUI内では、ArtworkImage変数を直接使用して、アートワーク画像を表示できます。 音楽を再生 システムプレーヤーまたはアプリプレーヤーを使用して音楽を再生できます。1曲または1枚のアルバムを再生できます。 システムプレーヤーを使用すると、アプリを閉じても音楽が再生され続けます。 システムプレーヤーの使用 Button("Play using iOS system player") { Task { SystemMusicPlayer.shared.queue = .init(for: [album]) do { try await SystemMusicPlayer.shared.play() } catch { print(error.localizedDescription) } } } album変数は、MusicItemプロトコルに準拠した他のオブジェクトで変えることができます。 アプリ内プレーヤーの使用 アプリ内プレーヤーを使えば、より多くのカスタマイズを行うことができます。 Button("Play using in-app player") { Task { ApplicationMusicPlayer.shared.queue = .init(for: [album]) do { try await ApplicationMusicPlayer.shared.play() } catch { print(error.localizedDescription) } } } 他の種類のAPIリクエストの実行 その他のApple Music用APIの中には、MusicKit内では利用できないものがある場合があります。そのような場合は、MusicDataRequestを使ってリクエストを行ってください。レスポンスは手動で解析する必要があります。 let url = URL(string: "https://api.music.apple.com/v1/me/recent/played")! let request = MusicDataRequest(urlRequest: URLRequest(url: url)) let response = try await request.response() 例えば、最近再生した音楽を取得するには、このウェブページを参照してください: MusicKit - Access user’s recently … | Apple Developer Forums Twitter @MszPro カテゴリー別に整理された、自分の公開記事とニュースレターを表示します。iOSデバイスのカメラアプリを使用して、このコードをスキャンするだけです。 または、Webで表示
- 投稿日:2022-01-25T01:07:30+09:00
【Swift】知っておきたいKeyPathの基本と使い方
この投稿は何? SwiftにおけるKeyPathについて、基本から解説します。 実行環境 macOS 12.1 Xcode 13.2.1 Swift 5.5 KeyPathとは 要は、「あるデータ型に定義されたプロパティまでの参照(パス)」です。 Swift3時代のString KeyPath KeyPathは、Objective-Cの時代から利用されていました。 Swiftでもそれを受け継いでおり、クラスに`objcMembers'属性をマークすることで利用できました。 参照型データの場合 @objcMembers class Kid: NSObject { dynamic var nickname: String = "" dynamic var age: Double = 0.0 dynamic var bestFriend: Kid? = nil dynamic var friends: [Kid] = [] init(nickname: String, age: Double) { self.nickname = nickname self.age = age } } // ベン君 let ben = Kid(nickname: "Benji", age: 5.5) // Kid型のnicknameプロパティへのキーパスを作成 let kidsNameKeyPath = #keyPath(Kid.nickname) // String // キーパスを使って取得した「ベン君のニックネーム」はAny型になってしまう let name = ben.value(forKeyPath: kidsNameKeyPath) // value(forKeyPath: String) -> Any print(name as! String) // Benji この方法で取得したキーパスは、単純なString型として保持されます。 そして、String KeyPathを利用して取得したプロパティ値は、型情報が失われてしまう性質がありました。 Swiftに最適化されたKeyPath Swift4では、全く新しい方法でKeyPathを利用できるようになりました。 値型データの場合 struct BirthdayParty { let celebrant: Kid var theme: String var attending: [Kid] } var bensParty = BirthdayParty(celebrant: ben, theme: "Construction", attending: []) let birthdayKid = bensParty[keyPath: \BirthdayParty.celebrant] bensParty[keyPath: \BirthdayParty.theme] = "Ninja" let birthdayKidsAgeKeyPath = \BirthdayParty.celebrant.age // キーパスを作成 let birthdayboysAge = bensParty[keyPath: birthdayKidsAgeKeyPath] // 5.5 値型データのKeyPathでは型情報が維持され、よりシンプルに記述されます。 基点型が推論可能な場合、記述を省略できます。 benはKid型インスタンスなので、基点型はKidであることが推論可能です。 従って、Kidを省略して以下のように記述できます。 let age = ben[KeyPath: \.age] 同様に、bensPartyはBirthdayParty型インスタンスなので、以下のコードも略記できます。 bensParty[keyPath: \BirthdayParty.theme] = "Ninja" bensParty[keyPath: \.theme] = "Ninja" // 型推論による基点型の省略記法 この方法で取得したキーパスは、ReferenceWritableKeyPath型として保持されます。 そして、取得した値の型情報も維持されます。 クラスのキーパス クラスでも同じようにして、キーパスを利用できます。 class C { var p = "" } let c = C() let cpKeyPaty = \C.p // ReferenceWritableKeyPath<C, p> let cp = c[keyPath: \C.p] // String
- 投稿日:2022-01-25T00:09:14+09:00
Flutter モーダルボトムシート(iOS風)でWebView表示
はじめに FlutterでiOS風 モーダルボトムシート(モーダルが下から出て、後ろの画面が少し小さくなるあれ)でWebView表示を実装しました。 環境 Dart: 2.15.1 Flutter: 2.8.1 完成形 実装 WebViewの説明に関しては、以下の記事で説明しているのでご覧ください。 - Web画面表示(進捗バー付き) Package modal_bottom_sheetを使用します。(名前そのまま) pubspec.yaml dependencies: modal_bottom_sheet: ^2.0.0 webview_flutter: ^3.0.0 コード 今回の実装にあたって、主に3か所の追加修正が必要です。 ルート homeのままだと画面が小さくならないため、ルーティングの修正をします。 home: ~~~,となっていたところを、 initialRoute: ~~~, onGenerateRoute: (RouteSettings settings) {~~~} に変更します。 main.dart import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:webview_flutter/webview_flutter.dart'; void main() { runApp(const MyApp(),); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, initialRoute: HomeScreen.routeName, onGenerateRoute: (RouteSettings settings) { switch (settings.name) { case HomeScreen.routeName: return MaterialWithModalsPageRoute( builder: (context) => const HomeScreen(), settings: settings,); } }, ); } } 〜〜〜 続く 〜〜〜 モーダル パッケージが用意してくれた、showCupertinoModalBottomSheetでiOS風 モーダルを呼び出します。 class HomeScreen extends StatelessWidget { const HomeScreen({Key? key}) : super(key: key); static const routeName = '/home'; @override Widget build(BuildContext context) { void _showModalBottomSheet() { showCupertinoModalBottomSheet( expand: true, context: context, backgroundColor: Colors.transparent, builder: (context) => const WebViewScreen(),); } return Scaffold( appBar: AppBar( title: const Text('Home'), ), body: Center( child: ElevatedButton( child: const Text('Press'), onPressed: () { _showModalBottomSheet(); }, ), ), ); } } 〜〜〜 続く 〜〜〜 WebView そのまま表示しても、モーダルシート上だとスクロールが効かなかったため EagerGestureRecognizer()をWebViewのgestureRecognizersに渡します。 ただWebView上のリンクで遷移した後、左 → 右のスワイプで戻れないです。 方法があれば教えて欲しいです! class WebViewScreen extends StatefulWidget { const WebViewScreen({ Key? key, }) : super(key: key); @override _WebViewScreenState createState() => _WebViewScreenState(); } class _WebViewScreenState extends State<WebViewScreen> { final Set<Factory<OneSequenceGestureRecognizer>> _gestureRecognizers = {Factory(() => EagerGestureRecognizer())}; // タッチ操作用 final _key = UniqueKey(); final Completer<WebViewController> _controller = Completer<WebViewController>(); bool _isLoading = true; // ローディングの有無 String _title = ''; // Webタイトル double _downloadPercentage = 0.0; // 進捗バー用 @override void initState() { super.initState(); if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); } // ローディングの切り替え void toggleLoading() { setState(() { _isLoading = !_isLoading; }); } // 進捗計算 void calculateProgress(int progress) { setState(() { _downloadPercentage = (progress / 100); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_title),// Web画面のタイトルを表示 automaticallyImplyLeading: false, // バックボタンの非表示 actions: [ IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), ), ], ), body: Column( children: [ // 進捗バー _isLoading ? LinearProgressIndicator(value: _downloadPercentage, minHeight: 7.0,) : const SizedBox.shrink(), Expanded( child: Scrollbar( showTrackOnHover: true, child: WebView( gestureRecognizers: _gestureRecognizers, // タッチ操作の有効化 key: _key, initialUrl: 'https://qiita.com/', javascriptMode: JavascriptMode.unrestricted, onWebViewCreated: _controller.complete, onPageFinished: (String url) async { if (_isLoading) toggleLoading(); final controller = await _controller.future; final title = await controller.getTitle(); if (title != null) _title = title; }, onProgress: (progress) { calculateProgress(progress); } ), ), ), ], ), ); } } 以上で動くはずです。