🙄

FlutterのShareExtensionがわかる!実に!わかるぞ!最高に「ハイ!」ってやつだアアアアアアハハハハハハハハハーッ

に公開

わかる!実に!わかるぞ!最高に「ハイ!」ってやつだアアアアアアハハハハハハハハハハーッ[1]

というわけで、お久しぶりです、わたなおです。今回はですね、FlutterなのにiOS側のみでShareExtensionを使用したいという狂気じみたことをやったので備忘録を書こうと思います。

あれ?前回の記事含め、Flutterである意味って、、、いや、気にしないことにしましょう。

お目汚しいたします、愚痴です

ShareExtensionとかiOSでしかできない機能の資料、少なすぎません??

タスクが終わったらタイトルの状態ですよ、、、

ShareExtensionとは、、、

ShareExtensionとは、⬇下の画像の黄色で囲まれた部分を押すことで表示されるUIで呼び出される処理とそのUIのことです。

ShareExtensionとは

よくエアドロで使うあの機能で写真アプリなどの列に自分のアプリを表示し、データを登録することが目標です。

自作したくない自分が色々動いた結果

Flutterにライブラリはあるようなのですが、説明が不十分だったのかiOSの変更などに追従できていなかったことで機能が実装できなかったので自作することにしました。

機能の流れ

  1. 共有データ(画像や音声)を選択し、共有用画面を開く
  2. アプリのアイコンを選択

ここからあとの実装はShare Extensionがどういう機能であるのかに依存します。

実装準備

脳内でキューピー3分クッキング👶の曲を流しながら、以下の手順でShareExtensionを準備しましょう。(慣れれば30分以内でできます。10回ループするだけですね。)

  1. Flutterで作成された、自作プロジェクトのios/Runner.xcworkspaceを起動します。名前が似ているのがありますが、白いアイコンの方を選んでください。
  2. Xcodeのナビゲーターエリアの一番上にあるRunner(ディレクトリではないRunnerを選んでください)をクリックします。
  3. 右の方にあるProjectTargetsの下にある+をクリックし、下のようなウィンドウが出てきます。そのフィルターにShare Extensionと入力し、先ほどのお馴染みのアイコンを選択します。
    add_extension
  4. Swiftのプロジェクト作成のようなウィンドウが見えると思いますので、ShareExtensionと入力してください。以降の内容を揃えたいので同名でお願いします。

以上を実施後、自作プロジェクトのios配下にShareExtension、Xcodeのナビゲーターエリアに青色ディレクトリのアイコンでShareExtensionとあるはずです。これで、表示自体はされるはずなので試してみましょう。

added

見えましたね。これで実装の準備は完了です。

できること・できないこと

自分が調査した範囲でできることとできないことをまとめようと思います。不十分な可能性もありますが、その場合はコメントしていただけると助かります。

できること

  • ShareExtensionを起動した要因を取得する(画像や音声など)
  • UserDefaultなどのiOS依存の機能を使用すること
  • デフォルトのUIの見た目を変更すること
  • 独自の見た目を実装すること(ShareViewControllerが継承するクラスをUIViewControllerに変えます)
  • 元アプリとデータなどを共有すること
  • PODでライブラリを追加する

できないこと

  • 元となったアプリを起動すること(これ、やりたかった、、、 😭)

外部ライブラリの追加方法

今回、Flutterを使用しているため、Podfileに下記のように記載します。今回はAPI呼び出し時にTokenが欲しいのでFirebase/CoreFirebase/Authを使用できるようにします。

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
  target 'RunnerTests' do
    inherit! :search_paths
  end
end

target 'Share_Extension' do
  use_frameworks!
  pod 'Firebase/Core'
  pod 'Firebase/Auth'
  pod 'GoogleUtilities/AppDelegateSwizzler'
  pod 'GoogleUtilities/MethodSwizzler'
  pod 'GoogleUtilities/Network'
  pod 'GoogleUtilities/NSData+zlib'
  pod 'GoogleUtilities/Environment'
  pod 'GoogleUtilities/UserDefaults'
  pod 'GoogleUtilities/Reachability'
  pod 'GTMSessionFetcher'
end

今回はFlutterなのでPodfileですが、SPMでのやり方は考えたくないので書かないことにします。(後日できたら…)

詳細な技術の話

準備

元からあるUIKit + Storyboardを使用します。UIViewControllerRepresentableを使えばSwiftUIも使えるかもしれないですが、そこまでする義理もないのでこれで行きます。

class ShareViewController: UIViewController {
  @IBOutlet weak var progressLoader: UIProgressView!
  @IBOutlet weak var loadingLabel: UILabel!
}

これで読み込み機能時の演出を作ります。チープでもいいでしょ。許せ、、、

Firebaseの初期化とアプリ本体とログイン状況の共有

この処理は必須級の関数です。リンク先のFirebaseのページを元に適切なKeyChainKeyを設定してください。

  func initFirebase() {
    FirebaseApp.configure()
    
    do {
      // KeyChainでアプリ本体とFirebase Authを共有
      try Auth.auth().useUserAccessGroup("KeyChainKey")
    } catch {
      // この処理の詳細は後述します orz
      closeView()
    }
  }

この関数を好きなタイミングで読んで欲しいのとFirebaseの処理を使う前までに必ず読み込まれて欲しいです。が、AppDelegateをここでは使用できないのでloadView()などで呼び出すことをおすすめします。

API呼び出し部分と最重要処理

この処理callApis()が呼び出される前に必ずFirebaseApp.configure()が呼び出されている必要があります。上の内容ができていれば大丈夫でしょうが一応、注意してください。

  /// どっかで呼ばれてるself.progressLoader.setProgress(0, animated: true)
  func closeView() {
    self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
  }
  
  func callApis() {
    Task {
      defer {
        closeView()
      }

      do {
        guard let jwt = try? await Auth.auth().currentUser?.getIDToken() else {
          throw """
ログインしていません。
アプリでログイン後、再度、実行してください。
"""
        }
        
        DispatchQueue.main.async {
          self.loadingLabel.text = "次の情報の取得中..."
          self.progressLoader.setProgress(1/n, animated: true)
        }

最重要処理1つ目self.extensionContext!.completeRequest(returningItems, completionHandler)とは

この処理を呼び出すことで自動でShareExtensionの画面を閉じることができます。手で閉じることもできますが、UXを損ねそうなので適切なタイミングでこれを呼び出すようにします。Concurrencyじゃないというのはびっくりしました。

最重要処理2つ目DispatchQueue.main.async {}とは

SwiftのUIKitではUIの更新のタイミングがメインのスレッドでないといけないのでメインスレッドに処理を入れ込むためにこれを書いています。まぁ、有名ですね。

deferをしている理由

実際の処理ではメッセージを表示して4秒後ぐらいで画面を閉じたいので、これを記載しています。各箇所で呼ぶより楽なのでこうしているってだけです。deferの中身は省略してます。

まとめ

Flutter(iOS)でShareExtensionを使用する方法は、普通にSwiftでShareExtensionを入れてCocoaPodsでライブラリを入れる感じで大丈夫でした。もし、よろしければShareExtensionを使ってみるのはいかがでしょうか?

Flutterでは、2度とやらん、、、

脚注
  1. アニメ「ジョジョの奇妙な冒険第3部スターダスト・クルセイダース」第48話 ↩︎

mutex Tech Blog

Discussion