酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

Share Extensionの実装でハマったところ

天どん v1.0の頃にいただいた「Safariからの共有したい」という要望に対応するためにShare Extensionを実装しました。ikatomoでToday Extensionの実装はしたことはありましたが、Share Extensionを実装するのは今回が初めてでした。

Share Extensionというのはコレです。ちなみに、つつじヶ丘にツツジはなくてどちらかというと千歳烏山の近くにツツジの名所があります。

URLの共有だけであればとても簡単だったのですが、Share Extensionは色んなデータを投げることができるため試行錯誤を凝らすハメになりました。結構、テストに時間がかかってしまいました。

Share Extension自体の実装方法に関しては、下記の記事がとても参考になりました。

目次

ハマりどころ

前述の通り、僕にはToday Extensionを実装した経験があります。そのため、AppGroupなどでハマることはありませんでした。

ただ、今回Share Extension特有の事例で結構ハマった感があります。天どんでのShare Extensionの実装にあたりハマった問題と解決方法を紹介していきたいと思います。

ローカライズ名にならない

InfoPlist.stringに日本語名を記入していましたが、Activity Viewsに表示されるアプリには反映されていませんでした。

Share ExtensionのInfo.plistで「Application has localized display name」を追加してValueに「YES」を指定します。

f:id:ch3cooh393:20170505020440p:plain

Source Code表示の場合には、下記のkeyを追加するとよいでしょう。

<key>LSHasLocalizedDisplayName</key>
<true/>

アイコンが変わらない

下図のようにActivity Views上にもきちんとアプリアイコンを表示させたいですね。アイコンを指定していないとデフォルトアイコンのバッテンアイコンになってしまいます。

f:id:ch3cooh393:20170505020512p:plain

Share Extension のターゲットを開いても、アイコンを指定するUIはありません。

ホストアプリ側にAssets.xcassetsが元々あると思います。share_extensionターゲットでも参照するようにします。

f:id:ch3cooh393:20170510011813p:plain

Assets.xcassetsの中のどのファイルをアイコンとして表示させるのかを別途指定しておく必要があります。

Build Settingsで「Assets Catalog App Icon Set Name」を検索します。名前の通りなのですがエクステンションでのアイコンの名前を指定します。アプリアイコンと同じAppIconと指定しました。

f:id:ch3cooh393:20170510011615p:plain

これでビルドするとターゲット一覧でもアイコンが反映されていると思います。

複数のアカウントが存在する場合、どのアカウントで投稿するのか指定したい

複数のアカウントが存在する場合、どのアカウントで投稿するのか指定したいです。共有ダイアログはとても小さい表示領域なのですが、画面遷移をおこなうことができます。

configurationItems()で共有ダイアログの画面下部にオプションを追加することができます。

    override func configurationItems() -> [Any]! {
        var items = [SLComposeSheetConfigurationItem]()

        //共有するアカウントの設定
        if let item = SLComposeSheetConfigurationItem() {
            item.title = "アカウント"
            item.value = user?.accountName ?? "unknown"
            item.tapHandler = {
                let vc = ShareAccountsViewController()
                vc.delegate = self
                self.pushConfigurationViewController(vc)
            }
            items.append(item)
        }

        return items
    }

アカウントセルをタップされたらアカウント選択画面に遷移して、ログイン済みのMastodonアカウントを指定します。

任意のタイミングで表示を変えたい

アカウントを選択してホーム画面へ戻ってきましたが、共有ダイアログ上の表示は変わっていないはずです。画面が戻って来たのをトリガーにしてself.reloadConfig()を実行しましょう。

再度、configurationItems()の処理が実行されます。

未ログインの場合にホストアプリを起動させたいけれど openURL() が存在しない

投稿系アプリのShare Extensionであれば、投稿するにあたりあらかじめログインしていることが前提になると思います。

天どんでも、どこのMastodonインスタンスにもログインしていない場合には、アプリを起動させてユーザーへログインすることを促したいです。実際にできるのか検討してみました。

iOS 8時代には使えていた方法が最新の環境では使えなくなっているのが結構あります。下記の方法は、iOS 8ではUIWebViewを使ってアプリ起動する方法が使えていましたが iOS 9以降では利用できなくなっています。

他の人も同じように悩むところらしく、StackOverFlowで下記の方法が紹介されていました。

実装するのであれば下記のようになります。

override func viewDidAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    let context = self.extensionContext!
    
    // NOTE: 天どんに1アカウントもログイン情報が存在しない!
    if currentUser() == nil {
        let alert = UIAlertController(title: "天どん", message: "ログインしていないと使えません、ごめんね。", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: L("cancel"), style: .default, handler: { (_) in
            context.completeRequest(returningItems: nil, completionHandler: nil)
        }))
        alert.addAction(UIAlertAction(title: L("login"), style: .cancel, handler: { (_) in

            let url = NSURL(string: "tendon://")
            let context = NSExtensionContext()
            context.open(url! as URL, completionHandler: nil)
            
            var responder = self as UIResponder?
            while (responder != nil){
                if responder?.responds(to: Selector("openURL:")) == true{
                    responder?.perform(Selector("openURL:"), with: url)
                    context.completeRequest(returningItems: nil, completionHandler: nil)
                }
                responder = responder!.next
            }
        }))
        present(alert, animated: true, completion: nil)
    }
}

ただし、iOS 8で使えていたUIWebViewを使う方法もこの紹介した方法もApple公式の方法ではありません。openURLが使えるのはToday Extensionだけのようです。

「天どん」では未ログインユーザーの場合にはアプリでログインして欲しい旨をアラートで表示させてShare Extensionを閉じるようにしました。

f:id:ch3cooh393:20170505015627p:plain

実装例としては以下のようになります。

override func viewDidAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    let context = self.extensionContext!
    
    // NOTE: 天どんに1アカウントもログイン情報が存在しない!
    if currentUser() == nil {
        let alert = UIAlertController(title: "天どん", message: "ログインしていないと使えません、ごめんね。", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { (_) in
            context.completeRequest(returningItems: nil, completionHandler: nil)
        }))
        present(alert, animated: true, completion: nil)
    }
}

複数の画像を読み込めない

attachmentsに複数画像が含まれています。

foreachでぐるぐる画像取得させると良いでしょう。ただし、処理が失敗した場合でも成功した場合でもエクステンションの振る舞いとしてなんらかの応答を返す必要があることを留意しましょう。

override func didSelectPost() {
    
    //NOTE: inputItemsが2つになるパターンが実運用上あり得るのかな?
    guard let inputItem = self.extensionContext?.inputItems[0] as? NSExtensionItem else {
        self.extensionContext!.cancelRequest(withError: TendonError.argument)
        return
    }
    
    var attachments = [Future<Any, TendonError>]()
    
    //NOTE: attachmentsに含まれているだけ非同期で画像を読み込む
    for attachment in inputItem.attachments!.filter({ $0 is NSItemProvider }).map({ $0 as! NSItemProvider }) {
        attachments.append(loadItem(provider: attachment))
    }
    attachments.sequence().onSuccess { (array) in
    
        //Mastodonへ投稿する
        TendonClient.shared.postStatus(user: user, status: statusText, sensitive: false, imageUrls: assets, visibility: visibility).onSuccess { (_) in
            self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
            }.onFailure { (error) in
                self.extensionContext!.cancelRequest(withError: error)
        }
    }.onFailure { (error) in
        self.extensionContext!.cancelRequest(withError: error)
    }
}

Activity Viewsから自分のアプリを選ぶとフリーズする

うまく実装できたはずなのに、Activity Viewsから自分のアプリを選ぶと共有元のアプリがフリーズしたように固まってしまいました。

viewDidAppear(_ animated: Bool)をoverrideしている場合、super.viewDidAppear(animated)を呼ばないとダイアログが表示されずにフリーズしたようになります。

下記のように実装している場合にはフリーズします。

override func viewDidAppear(_ animated: Bool) {
    //super.viewDidAppear(animated)

}

ただ、このケースは普通に実装していれば発生しないので気にする必要はないかもしれません。

Newsアプリから共有すると、Activity Viewsに自分のアプリが表示されない

AppStoreアプリやNewsアプリからの共有ができないと教えていただきました。

下図のようにNewアプリからの共有の場合にはActivity Viewsが表示されていません。Safariからの共有の場合にはActivity Viewsに天どんが表示されていることがわかります。

f:id:ch3cooh393:20170516111516p:plain

NSExtensionActivationSupportsTextをtrueにすることで、Activity Viewsにアプリが表示されるようになりました。

 <key>NSExtension</key>
    <dict>
        <key>NSExtensionAttributes</key>
        <dict>
            <key>NSExtensionActivationRule</key>
            <dict>
                <key>NSExtensionActivationSupportsText</key>
                <true/>
                <key>NSExtensionActivationSupportsFileWithMaxCount</key>
                <integer>1</integer>
                <key>NSExtensionActivationSupportsImageWithMaxCount</key>
                <integer>4</integer>
                <key>NSExtensionActivationSupportsTextWithMaxCount</key>
                <integer>1</integer>
                <key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
                <integer>1</integer>
                <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
                <integer>1</integer>
            </dict>
        </dict>

関連記事

この他にもiOSでの開発ネタを紹介しております。Tipsなどまとめておりますのでこちらのページをご参照ください。