A plugin to implement iOS SiriKit Media Intents support in your Flutter app.
Important
This plugin requires a minimum iOS deployment target of 14.0.
Note
This plugin requires several modifications to the iOS generated project.
To see the plugin in action just look at the example app, under example
folder.
In Xcode, select the "Runner" target and put 14.0
in the "iOS" form, under "Minimum Deployments" section.
Open ios/Flutter/AppframeworkInfo.plist
in your Flutter app and update the MinimumOSVersion
value to 14.0
.
Replace (or, edit) the AppDelegate.swift
file with the following content:
import Flutter
import Intents
import UIKit
import sirikit_media_intents
@main
@objc class AppDelegate: FlutterAppDelegate {
private var _flutterEngine = FlutterEngine(
name: "SharedFlutterEngine",
project: nil,
allowHeadlessExecution: true
)
var flutterEngine: FlutterEngine {
return _flutterEngine
}
private var _playMediaIntentHandler: PlayMediaIntentHandler?
var playMediaIntentHandler: PlayMediaIntentHandler? {
return _playMediaIntentHandler
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication
.LaunchOptionsKey: Any]?
) -> Bool {
flutterEngine.run()
GeneratedPluginRegistrant.register(with: flutterEngine)
guard flutterEngine.hasPlugin(SirikitMediaIntentsPlugin.typeName) else {
preconditionFailure("Plugin has not been registered")
}
guard let pluginInstance = SirikitMediaIntentsPlugin.instance else {
preconditionFailure("Plugin instance has not been created")
}
_playMediaIntentHandler = PlayMediaIntentHandler(
application: application,
plugin: pluginInstance
)
return super.application(
application, didFinishLaunchingWithOptions: launchOptions
)
}
override func application(
_ application: UIApplication, handlerFor intent: INIntent
) -> Any? {
switch intent {
case is INPlayMediaIntent:
return _playMediaIntentHandler
default:
return nil
}
}
}
This plugin adopts the in-app intent handling strategy (availble on iOS 14.0+), so no app extensions are created; on the other hand, the iOS app must opt-in to scenes and enable support for multiple scenes (read this). Please, read the Info.plist
section in the guide to complete the setup process for supporting multiple scenes.
Create the SceneDelegate.swift
(which will serve as the main scene delegate) with the following content:
import UIKit
import sirikit_media_intents
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene else {
return
}
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
preconditionFailure("unable to obtain AppDelegate")
}
let viewController = FlutterViewController(
engine: appDelegate.flutterEngine,
nibName: nil,
bundle: nil
)
window = UIWindow(windowScene: windowScene)
window?.rootViewController = viewController
window?.makeKeyAndVisible()
}
}
Add the "Siri" capability to the iOS app. In Xcode: "Runner" target > "Signing & Capabilities" > "+ Capability" (top right button in the tab list) > Search for "Siri" and add it.
This will make the Runner.entitlements
file look like the following example:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.siri</key>
<true/>
</dict>
</plist>
Add the INPlayMediaIntent
intent to the supported intents of your app.
In Xcode: "Runner" target > "General" > Scroll to the "Supported Intents" section > Click the "+" button and specify INPlayMediaIntent
in the "Class Name" column.
Then opt-in the media categories supported by your app (e.g. "Music", "General").
The aforementioned steps should have added the following entries to the Info.plist
file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- ... -->
<key>INIntentsSupported</key>
<array>
<string>INPlayMediaIntent</string>
</array>
<key>INSupportedMediaCategories</key>
<array>
<!-- These entries depend of which categories you've opted-in in the Xcode UI -->
<string>INMediaCategoryGeneral</string>
<string>INMediaCategoryMusic</string>
</array>
<!-- ... -->
</dict>
</plist>
As mentioned above, this plugin requires the iOS app to support multiple scenes; for this reason, you must add the UIApplicationSceneManifest
entry to the Info.plist
file:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- ... -->
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<!-- ... -->
</dict>
</plist>
After doing that, you should be able to find the "Application Scene Manifest" entry in the "Custom iOS Target Properties" in Xcode.
You need to create a class that implements/extends the MediaIntentsHandler
interface/abstract class.
The semantics of the resolveMediaItems
and playMediaItems
should appear pretty clear by the following example.
class ExampleMediaIntentsHandler extends MediaIntentsHandler {
@override
Future<List<MediaItem>> resolveMediaItems(MediaSearch mediaSearch) async {
// TODO: call backend APIs to find media items matching the search criteria
var mediaItems = [
MediaItem(
identifier: '<song-1-id>',
title: 'Cool song 1',
type: MediaItemType.song,
artwork: MediaItemImage(
url:
'https://images.pexels.com/photos/9851222/pexels-photo-9851222.jpeg?auto=compress&cs=tinysrgb&w=180',
width: 180.0,
height: 180.0,
),
artist: 'Cool Artist 1',
),
MediaItem(
identifier: '<song-2-id>',
title: 'Cool song 2',
type: MediaItemType.song,
artwork: MediaItemImage(
url:
'https://images.pexels.com/photos/8834489/pexels-photo-8834489.jpeg?auto=compress&cs=tinysrgb&w=180',
width: 180.0,
height: 180.0,
),
artist: 'Cool Artist 2',
)
];
return mediaItems;
}
@override
Future<void> playMediaItems(List<MediaItem> mediaItems) async {
log('Queuing ${mediaItems.length} songs');
// TODO: call App media services for queueing media items
log('Playing ${mediaItems.first.identifier} - ${mediaItems.first.title}');
// TODO: call App media services for playing a media item
}
}
In the code that initializes your app (the main
method is a good candidate) you should add the following lines for initializing the plugin, by telling which class is going to handle the iOS Siri media intent callbacks:
void main() {
final sirikitMediaIntentsPlugin = SirikitMediaIntents();
final mediaIntentsHandler = ExampleMediaIntentsHandler();
WidgetsFlutterBinding.ensureInitialized();
sirikitMediaIntentsPlugin.initialize(mediaIntentsHandler);
// ...
runApp(MyApp());
}