OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

React NativeアプリでSiri Media Intentを実装した話

初めまして、アプリ開発を担当している エモト と申します。手元にSHIROBAKOのムビチケがあるのに、昨今の事情で映画館に行けず、モヤモヤしております。昨年秋に入社して、いつかはブログを書こうと思って気付いたら今日となりました。よろしくお願いします。

さて昨年末、弊社のiOSアプリは Siri Media Intents に対応しました。

「Hey Siri, {作品名}をオーディオブックで再生して」

と呼びかけると、Siri経由でアプリのライブラリ内にあるオーディオブックが再生されます。ぜひお試しください。

弊アプリは、React Native(以降、RN)でアプリ開発を行っています。クロスプラットフォーム開発のRNでSiriが実装できるの!?と思う方もいるかと思います。RNでのSiri開発を少し話したいと思います。

なお、以降文中のネイティブ開発は、Xcodeを使ったSwiftやObjective-Cでのアプリ開発を指します。

Siri Media Intents と React Native

一般的なネイティブ開発での Siri Media Intents の実装は、Siriが命令を受けると、Intent側の handle(intent:completion:) からアプリ側のメソッド application(_:handle:completionHandler:) が呼ばれます。そのメソッド内でアプリ側の処理(今回はオーディオブックの再生)を行い、Siriへ完了コールバックを送ります。ここで、弊社アプリはRNを採用しているため、RN側で処理を行う必要があります。

RNには、SwiftやObjective-Cで書かれたネイティブモジュールを用いて、ネイティブ 側とやりとりできる機能があります。それを利用して、Siriから受けた情報をRNに送り、RNで再生制御を行いました。

f:id:mitsuharu_e:20200401134736p:plain

最も苦労した点の1つは、RN側の再生処理の結果を application(_:handle:completionHandler:) の中で受け取らなといけないことです。ネイティブからRNへにイベントを送る処理は情報を送るだけで、再生完了コールバックのデリゲートやクロージャを設定することはできません。あまりスマートではないですが、RN側で処理が完了したらネイティブ側に完了フラグを通知する、ネイティブ側はそのフラグが立つまで待つというアプローチを取りました。

// React-Nativeからの完了を受け取る
var isSucceededFromRN: Bool?

@objc( SiriCtrl )
class SiriCtrl: RCTEventEmitter {
}

extension SiriCtrl {

  // React-NativeからNative methodとして、Intentの完了メソッドを呼ぶ
  @objc(completeSiriHandler:)
  func completeSiriHandler(isSucceeded: Bool) {
    DispatchQueue.global().async {
      isSucceededFromRN = isSucceeded
    }
  }
  
   /**
   AppDelegate の application(_:handle:completionHandler:) 
   の中で用いるメソッド
   */
  func application(_ application: UIApplication,
                   handle intent: INIntent,
                   completionHandler: @escaping (INIntentResponse) -> Void) {
    if #available(iOS 13.0, *) {
      guard
        let playMediaIntent = intent as? INPlayMediaIntent,
        let mediaItem = playMediaIntent.mediaItems?.first,
        let title = mediaItem.title
        else {
          completionHandler(INPlayMediaIntentResponse(code: .failure,
                                                      userActivity: nil))
          return
      }

      // 完了判定フラグを初期化する
      isSucceededFromRN = nil

      // React Nativeに再生イベントを投げる
      self.sendMediaInfoToReactNative(title: title)

      // RNから完了判定が帰ってくるまで待つ
      self.waitAsyncCallback({
        isSucceededFromRN == nil
      }) {
        var code: INPlayMediaIntentResponseCode = .failureRestrictedContent
        if let result = isSucceededFromRN, result == true {
          code = .success
        }
        completionHandler(INPlayMediaIntentResponse(code: code,
                                                    userActivity: nil))
      }
    }
  }
}

extension NSObject {

  /**
   処理完了を待ってから、後続処理を行う
   */
  func waitAsyncCallback(_ waitContinuation: @escaping (() -> Bool),
                         completion: @escaping (() -> Void)) {
    var wait = waitContinuation()
    let semaphore = DispatchSemaphore(value: 0)
    DispatchQueue.global().async {
      while wait {
        DispatchQueue.main.async {
          wait = waitContinuation()
          semaphore.signal()
        }
        semaphore.wait()
        Thread.sleep(forTimeInterval: 0.01)
      }
      DispatchQueue.main.async {
        completion()
      }
    }
  }
}

非同期処理の待ち処理は、こちらの記事 【Swift 3】処理の完了を待ってから後続処理を行う - Qiita を参考にしました。ありがとうございます。

まとめ

React Nativeと聞くと、SwiftやKotlinなどのネイティブコードは不要のイメージがあるかもしれません(私も最初はそうでした)。しかしながら、今回のような固有機能の実装時には適宜ネイティブコードを使用しています。

最後に、オトバンクではエンジニアを募集中です。もし、日頃はSwiftやKotlinを書かれてる方でも、オーディオブックや、React Native開発に少しでも興味があれば、是非どうぞ。

お待ちしております。