システム開発部のTです。
前回につづいて、今回もShareExtensionのネタを書いていきたいと思います。
今回は、ShareExtensionの機能を利用するときにコールされるUIをカスタマイズするやり方を書いていきたいと思います。

UIのカスタマイズ自体、iOSのプログラマーにとっては簡単にできるからなのか、取り上げているサイトがあまり無かったように見受けられたので、ちょっと掘り下げていくように書いていければと思います。

なお、前回の記事で取り上げたコードを流用するので、こちらを初めて見る方は、一度前回の記事を参照いただければと思います。


ShareViewControllerのコード初期化

前回の記事からの続きになりますが、まずはShareViewControllerのコードを見ていきましょう。
プロジェクト作成直後だと、以下の内容で生成されているかと思います。

import UIKit
import Social

class ShareViewController: SLComposeServiceViewController {

    override func isContentValid() -> Bool {
        // Do validation of contentText and/or NSExtensionContext attachments here
        return true
    }

    override func didSelectPost() {
        // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.

        // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
        self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
    }

    override func configurationItems() -> [Any]! {
        // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
        return []
    }

}

上記を以下の内容に変更します。

//class ShareViewController: SLComposeServiceViewController {
class ShareViewController: UIViewController { // <- UIViewControllerに変更する

    override func viewDidLoad() { // UI初期化のためのviewDidLoadを追加
        
    }
    
//    override func isContentValid() -> Bool {  // 不要なので削除
//        // Do validation of contentText and/or NSExtensionContext attachments here
//        return true
//    }
//
//    override func didSelectPost() { // 不要なので削除
//        // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
//
//        // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
//        self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
//    }
//
//    override func configurationItems() -> [Any]! { // 不要なので削除
//        // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
//        return []
//    }

}

一旦、ShareViewControllerはここまで。
上記ではわかりやすいようにコメント化していますが、バッサリ削除してしまってもOKです。
以降では、いよいよUIのカスタマイズをしていきたいと思います。

MainInterface.storyboardをカスタマイズ

では次にUIをカスタマイズしていきます。
っていっても、普通に画面を生成するのとあまり変わりありません。
まずは、MainInterface.storyboardを開きます。

中身見てもらうとわかりますが、SafeAreaには何も入っていませんね・・・。
ではここに簡単なUIを構築していきたいと思います。

以下、簡単なUIを作ってみました。

共有されたデータを表示する領域、登録ボタン、キャンセルボタン・・・って感じです。
登録ボタンっていっても、本件では実際に登録するような処理はオミットします。

上記で定義した項目は、ShareViewControllerに反映してください。

ここまでで、ShareViewControllerの内容は以下のようになっているかと思います。

import UIKit
import Social

class ShareViewController: UIViewController {

    @IBOutlet weak var shareTextLabel: UILabel!
    
    override func viewDidLoad() {
    }
    
    @IBAction func onTapRegist(_ sender: Any) {
    }
    
    @IBAction func onTapCancel(_ sender: Any) {
    }

}

ここまで実装したら、一旦アプリを実行してみましょう。
ここで気をつけてほしいのは、親プロジェクトをインストールしてから、ShareExtensionのサブプロジェクトを実行すること。

インストール後、Safariを起動し、共有ボタンを押下してください。
共有先にTestappExtensionsを選択すると、以下の「テストタイトル」画面が表示されるかと思います。

ここまでくれば、あとは処理を埋め込むだけですね。
とりあえず、ブラウザからURLが連携されるので、その処理を埋め込みましょう。

処理の実装

viewDidLoad()に共有された内容を取得するためのコードを入れます。
本件では、URLを取得するコードを入れました。

override func viewDidLoad() {
    super.viewDidLoad()
    guard
        let item = self.extensionContext?.inputItems.first as? NSExtensionItem,
        let itemProvider = item.attachments?.first
    else {
        let error = NSError(domain: "Etc Error", code: 0, userInfo: [NSLocalizedDescriptionKey: "An error description"])
        // cancelRequestで呼び出し元にエラー通知し、共有画面を終了する
        self.extensionContext?.cancelRequest(withError: error)
        return
    }
    
    if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
        itemProvider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) {[weak self] (url, error) in
            if let url = url as? NSURL {
                guard let urlString = url.absoluteString else {
                    let error = NSError(domain: "URL not found error", code: 0, userInfo: [NSLocalizedDescriptionKey: "An error description"])
                    self?.extensionContext?.cancelRequest(withError: error)
                    return
                }
                DispatchQueue.main.async {
                    self?.shareTextLabel.text = urlString
                }
            } else {
                let error = NSError(domain: "URL not found error", code: 0, userInfo: [NSLocalizedDescriptionKey: "An error description"])
                self?.extensionContext?.cancelRequest(withError: error)
                return
            }
        }
    }
}

URLが取得できなかったときのエラーハンドリングとして、以下のコードを埋め込んでいます。

self?.extensionContext?.cancelRequest(withError: error)

cancelRequestを実行すると、共有画面を終了し、呼び出し元にエラーを通知することができます。

そして、ボタン押下時の処理も入れていきます。
一応登録とキャンセルで2つ処理を分けていますが、本件では処理を同一にしました。

@IBAction func onTapRegist(_ sender: Any) {
    // 共有画面を閉じる。returningItemsは呼び出し元に渡したい項目がある場合に設定する
    self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}

@IBAction func onTapCancel(_ sender: Any) {
    self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}

ボタン押下時の処理にて、以下を埋め込んでいます。
completeRequestは、共有画面を終了し、returningItemsに設定した内容を呼び出し元に通知することができます。
本件では、通知する内容は無いため、空配列を設定しています。

self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)

ここまで実装して、最終的に以下の内容になっているかと思います。

import UIKit
import Social
import MobileCoreServices

class ShareViewController: UIViewController {

    @IBOutlet weak var shareTextLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        guard
            let item = self.extensionContext?.inputItems.first as? NSExtensionItem,
            let itemProvider = item.attachments?.first
        else {
            let error = NSError(domain: "Etc Error", code: 0, userInfo: [NSLocalizedDescriptionKey: "An error description"])
            // cancelRequestで呼び出し元にエラー通知し、共有画面を終了する
            self.extensionContext?.cancelRequest(withError: error)
            return
        }
        
        if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
            itemProvider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) {[weak self] (url, error) in
                if let url = url as? NSURL {
                    guard let urlString = url.absoluteString else {
                        let error = NSError(domain: "URL not found error", code: 0, userInfo: [NSLocalizedDescriptionKey: "An error description"])
                        self?.extensionContext?.cancelRequest(withError: error)
                        return
                    }
                    DispatchQueue.main.async {
                        self?.shareTextLabel.text = urlString
                    }
                } else {
                    let error = NSError(domain: "URL not found error", code: 0, userInfo: [NSLocalizedDescriptionKey: "An error description"])
                    self?.extensionContext?.cancelRequest(withError: error)
                    return
                }
            }
        }
    }
    
    @IBAction func onTapRegist(_ sender: Any) {
        // 共有画面を閉じる。returningItemsは呼び出し元に渡したい項目がある場合に設定する
        self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
    }
    
    @IBAction func onTapCancel(_ sender: Any) {
        self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
    }

}

ここまで来たら、再度ビルド&インストールして、動作確認してみましょう。

動作確認

ここまで、いかがだったでしょうか?
UIのカスタマイズも、通常のUIと同じように作れることが理解できたかと思います。


さいごに

業務で初めてShareExtensionを実装したときに、UIをカスタマイズする必要が出てきたとき、関連記事が少なかったため、勝手が分からず苦戦してしまいました。なので、本件を備忘録として残そうと思いました。同じようにお悩みの方にお役になることができれば幸いです。

これからも、ShareExtensionについてもう少し深堀りした内容を連携できればと思います。

本日はここまで!



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる