App Extensionの中のShare Extensionを用いることで、今開いてるページの記事やデータを共有したいアプリにPostして、そのデータの共有行うことが可能です。
iOS8からの新機能で、関連する記事はたくさんありますが、自分なりにまとめておきたかったので手順を書いておきます。
1. プロジェクトを作成
今回は SampleShareExtension というプロジェクトを作成したら、続いてShare Extension用のプロジェクトを作っていきます。
File -> New -> Target を押すと以下の画面になります。
この中から Share Extension を選択して追加します。
今回は testShareExtension と名前を付けました。
次に Activate “testShareExtension” scheme? と聞かれるので Activate を選択します。
しばらく待つと作成した testShareExtension の項目が追加されています。
追加された ShareViewController.swift にPost後の処理などコードを後に書いていきます。
2. App Group設定
図のように Project から TARGETS -> SampleShareExtension を選択し、上の項目の Capabilities を選択します。 Keychain Sharing と App Groups を ON にします。
この App Groups によって同じグループ内でデータを userDefaults などで共有することが可能となります。
Group名は group. + Bundle Identifier で作ります。
(Bundle Identifier は、General の Identityの中の項目にある Bundle Identifier を書けば良いです。)
(例)
Bundle Identifier が Qiita.SampleShareExtension なら
App Groups の名前は group.Qiita.SampleShareExtension となります。
同様に Project から TARGETS -> testShareExtension を選択し、
Keychain Sharing と App Groups を ON にします。(先ほど作成した App Groups にチェックを入れます。)
3. Info.plist設定
App Extensions プログラミングガイドの25~26pに
「Share」/「Action」Extensionは、ある特定の型のデータしか扱えない可能性があります。取り扱い可能なデータをユーザが選択したときにのみ、ホストアプリケーションからExtensionを起動できるようにしたい場合は、ExtensionのInfo.plistプロパティリストファイルにNSExtensionActivationRuleキーを追加してください。
と記述されているように testShareExtension の Info.plist の中で画像やURLなど、どのデータを扱うかの設定を行います。
- NSExtension
- NSExtensionAttributes
- NSExtensionActivationRule (TRUEPREDICATE)
- NSExtensionAttributes
現状このようになっています。NSExtensionActivationRule が全ての情報を許可する状態 TRUEPREDICATE になっているので、これを以下の図のように Type を Dictionary に変えて、その中に扱う情報を記述していきます。
Key | Description |
---|---|
NSExtensionActivationSupportsAttachmentsWithMaxCount | a maximum number of attachments |
NSExtensionActivationSupportsAttachmentsWithMinCount | a minimum number of attachments |
NSExtensionActivationSupportsFileWithMaxCount | files in general |
NSExtensionActivationSupportsImageWithMaxCount | image files |
NSExtensionActivationSupportsMovieWithMaxCount | movie files |
NSExtensionActivationSupportsText | text |
NSExtensionActivationSupportsWebURLWithMaxCount | web URLs |
NSExtensionActivationSupportsWebPageWithMaxCount | web pages |
例えば Share Extension が
テキストを利用する、画像3枚、WebページのURLを1つまでとする場合、辞書を次のように指定してあげます。
- NSExtension
- NSExtensionAttributes
- NSExtensionActivationRule
- NSExtensionActivationSupportsText | Boolean | YES
- NSExtensionActivationSupportsImageWithMaxCount | Number | 3
- NSExtensionActivationSupportsWebURLWithMaxCount | Number | 1
- NSExtensionActivationRule
- NSExtensionAttributes
4. Post処理
ShareViewController.swift にコードを記述していきます。
元から記述されている以下の3つのメソッドには、それぞれこのような内容を書くことができます。
- isContentValid() -> Bool : 文字入力されていないとPostを押せないようにする
- didSelectPost() : Postを押した後の処理
- configurationItems() -> [Any]! : 追加項目のリスト管理
まずは isContentValid() -> Bool から書いていきます。
/**
* 文字入力されていないとPostを無効にする
*/
override func isContentValid() -> Bool {
self.charactersRemaining = self.contentText.characters.count as NSNumber!
let canPost: Bool = self.contentText.characters.count > 0
if canPost {
return true
}
return false
}
self.contentText
が入力されていない場合は、Postボタン押せなくします。
(self.contentText
は、Postする際に入力する文字のことです。Twitterで例えるなら、入力したツイートのことです。)
次に本題のPost処理について記述していきます。
Post処理は didSelectPost() に書きます。
NSItemProviderで共有する情報に uniform type identifiers (UTIs) を用いるため、例えばURLを取得するのに kUTTypeURL が使用できるようにするため、 MobileCoreServices を import します。
import MobileCoreServices
hasItemConformingToTypeIdentifier(puclicURL) で URL の情報を持っているか探し、 loadItem で情報を読み取って、そのデータを指定された型に変換します。
/**
* Postを押した後の処理
*/
override func didSelectPost() {
let extensionItem: NSExtensionItem = self.extensionContext?.inputItems.first as! NSExtensionItem
let itemProvider = extensionItem.attachments?.first as! NSItemProvider
let puclicURL = String(kUTTypeURL) // "public.url"
// shareExtension で NSURL を取得
if itemProvider.hasItemConformingToTypeIdentifier(puclicURL) {
itemProvider.loadItem(forTypeIdentifier: puclicURL, options: nil, completionHandler: { (data, error) in
// item からそれぞれそのページのURLや画像データに変換して UserDefaults で保存する
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
})
}
}
このようにして、例えばそのページのNSURLを取得する場合は、
let url: NSURL = item as? NSURL
と変換してあげれば取得できる。(また、String型で保存する場合は url.absoluteString
としてあげる必要がある。)
保存方法
userDefaults.standard
のところを App Groups で共有するため、 suiteName を指定する必要がある。
let suiteName: String = "group.[自分のBundle Identifier].SampleShareExtension"
let keyName: String = "shareData"
// ---------------
// データの保存
// ---------------
let sharedDefaults: UserDefaults = UserDefaults(suiteName: self.suiteName)!
sharedDefaults.set(url.absoluteString!, forKey: self.keyName) // そのページのURL保存
sharedDefaults.synchronize()
ViewController.swift での読み込みも同様に同じ suiteName を指定してあげれば読み込むことが可能です。
// ---------------
// データの読み込み
// ---------------
let sharedDefaults: UserDefaults = UserDefaults(suiteName: self.suiteName)!
if let url = sharedDefaults.object(forKey: self.keyName) as? String {
print(url) // URLの確認
//sharedDefaults.removeObject(forKey: self.keyName) // 削除
}
以上を踏まえて、Safari から URL や image、別アプリから PDF を読み込むコードを以下にまとめました。
URLを取得する
Safariから そのページのURLを取得して、メインのアプリで読み込み、Safariを起動してそのページを表示する方法です。
/**
* Postを押した後の処理
*/
override func didSelectPost() {
let extensionItem: NSExtensionItem = self.extensionContext?.inputItems.first as! NSExtensionItem
let itemProvider = extensionItem.attachments?.first as! NSItemProvider
let puclicURL = String(kUTTypeURL) // "public.url"
// shareExtension で NSURL を取得
if itemProvider.hasItemConformingToTypeIdentifier(puclicURL) {
itemProvider.loadItem(forTypeIdentifier: puclicURL, options: nil, completionHandler: { (item, error) in
// NSURLを取得する
if let url: NSURL = item as? NSURL {
// ----------
// 保存処理
// ----------
let sharedDefaults: UserDefaults = UserDefaults(suiteName: self.suiteName)!
sharedDefaults.set(url.absoluteString!, forKey: self.keyName) // そのページのURL保存
sharedDefaults.synchronize()
}
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
})
}
}
// --------------------
// データの読み込み(URL)
// --------------------
let sharedDefaults: UserDefaults = UserDefaults(suiteName: self.suiteName)!
if let url = sharedDefaults.object(forKey: self.keyName) as? String {
// Safari を起動してそのURLに飛ぶ
UIApplication.shared.open(URL(string: url)!)
// データの削除
sharedDefaults.removeObject(forKey: self.keyName)
}
Imageを取得する
例えば、Safariから開いているページのその画面の image を取得するやり方です。
この例では、読み込んだ画像を UIImageView に表示させています。
/**
* Postを押した後の処理
*/
override func didSelectPost() {
let extensionItem: NSExtensionItem = self.extensionContext?.inputItems.first as! NSExtensionItem
let itemProvider = extensionItem.attachments?.first as! NSItemProvider
let puclicURL = String(kUTTypeURL) // "public.url"
// shareExtension で NSURL を取得
if itemProvider.hasItemConformingToTypeIdentifier(puclicURL) {
itemProvider.loadPreviewImage(options: nil, completionHandler: { (item, error) in
// 画像を取得する
if let image = item as? UIImage {
// ----------
// 保存処理
// ----------
let sharedDefaults: UserDefaults = UserDefaults(suiteName: self.suiteName)!
sharedDefaults.setValue(UIImagePNGRepresentation(image), forKey: self.keyName) // そのページの画像をPNGで保存
sharedDefaults.synchronize()
}
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
})
}
}
@IBOutlet weak var imageView: UIImageView!
// --------------------
// データの読み込み(image)
// --------------------
let sharedDefaults: UserDefaults = UserDefaults(suiteName: self.suiteName)!
if let sharedData = sharedDefaults.object(forKey: self.keyName) {
let image: UIImage = UIImage(data: sharedData as! Data)!
self.imageView.image = image
// データの削除
sharedDefaults.removeObject(forKey: self.keyName)
}
loadItem ではなく loadPreviewImage を用いて指定したプレビュー画像を読み込みます。
PDFを取得する
別アプリからPDFデータを取得して、メインのアプリに保存するやり方です。
Info.plist から NSExtensionActivationSupportsFileWithMaxCount を追加します。
- NSExtension
- NSExtensionAttributes
- NSExtensionActivationRule
- NSExtensionActivationSupportsFileWithMaxCount | Number | 1
- NSExtensionActivationRule
- NSExtensionAttributes
また、メインのアプリから保存したPDFデータを別のアプリで開くやり方も合わせて記載しました。UIDocumentInteractionController を用いてPDFデータを扱えるアプリケーションを表示し、選択したアプリケーションにImportすることで可能です。
/**
* Postを押した後の処理
*/
override func didSelectPost() {
let extensionItem: NSExtensionItem = self.extensionContext?.inputItems.first as! NSExtensionItem
let itemProvider = extensionItem.attachments?.first as! NSItemProvider
let puclicPDF = String(kUTTypePDF) // "public.pdf"
// shareExtension で NSURL を取得
if itemProvider.hasItemConformingToTypeIdentifier(puclicPDF) {
itemProvider.loadItem(forTypeIdentifier: publicPDF, options: nil, completionHandler: { (item, error) in
// NSURLを取得する
if let url: NSURL = item as? NSURL {
do {
// NSData に変換する
let data: NSData = try NSData(contentsOf: url as URL)
// ----------
// 保存処理
// ----------
let sharedDefaults: UserDefaults = UserDefaults(suiteName: self.suiteName)!
sharedDefaults.set(data, forKey: self.keyName) // NSDataで保存
sharedDefaults.synchronize()
print("save ok")
} catch {
print("erorr")
}
}
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
})
}
}
UIDocumentInteractionControllerDelegate を追加して以下のコードをインスタンス変数として記述します。
var documentInteractionController: UIDocumentInteractionController?
// --------------------
// データの読み込み(PDF)
// --------------------
let sharedDefaults: UserDefaults = UserDefaults(suiteName: self.suiteName)!
if let sharedData = sharedDefaults.data(forKey: self.keyName) {
// DocumentDirectory参照パス
let documentsPath: NSString = NSSearchPathForDirectoriesInDomains(
FileManager.SearchPathDirectory.documentDirectory,
FileManager.SearchPathDomainMask.userDomainMask, true).first! as NSString
let filename: String = "test.pdf"
let filePath = documentsPath.appendingPathComponent(filename)
// ------------------------------
// WriteToFile を用いた保存処理
// ------------------------------
let success = (try? sharedData.write(to: URL(fileURLWithPath: filePath), options: [.atomic])) != nil
// 確認
if success {
print("Save OK")
} else {
print("Save Error")
}
// NSData を NSURL に変換する
let pdfFileURL: NSURL = NSURL(fileURLWithPath: filePath)
// ------------------------------
// 別アプリにPDFデータをImportする
// ------------------------------
documentInteractionController = UIDocumentInteractionController(url: pdfFileURL as URL)
documentInteractionController?.delegate = self
if !(documentInteractionController?.presentOpenInMenu(from: self.view.frame, in: self.view, animated: true))! {
// 送信失敗
let alert = UIAlertController(title: "Error", message: "not found application", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
// データの削除
//sharedDefaults.removeObject(forKey: self.keyName)
// ディレクトリ内の test.pdf 削除
//let fileManager = FileManager.default
//try! fileManager.removeItem(atPath: filePath)
}
ちなみにPDFデータがディレクトリ内にあるかの確認は、
Window -> Devices の中から対象のアプリケーションを選択することでディレクトリ内のデータを見ることができます。
Build & Run
上記のように、Share Extension を追加すると2種類選択できるようになっています。
testShareExtension を選択すると Choose an app to run: と表示され、どのアプリケーションから Share Extension を動かすか選択できます。
ここでは、
testShareExtension でデータをPost -> 一度アプリを終了させる -> SampleShareExtension に切り替えてデータを読み込む
という流れで行なっています。
おまけ
override func viewDidLoad() {
super.viewDidLoad()
// titleName
self.title = "テスト"
// color
self.navigationController?.navigationBar.tintColor = UIColor.white
self.navigationController?.navigationBar.backgroundColor = UIColor(red:1.0, green:0.75, blue:0.5, alpha:1.0)
// postName
let controller: UIViewController = self.navigationController!.viewControllers.first!
controller.navigationItem.rightBarButtonItem!.title = "保存"
}
このように書くと以下の画像のような画面になります。
ちなみに画像を貼り付けたりすることも可能なので細かく設定したい方は、上記のコードを例に色々と追加してみるといいかもしれません。
参考にしたサイト
以下のサイトを主に参考にさせていただきました
- Share Extension
- UIDocumentInteractionController (PDFデータ関連)
- userDefaults
最後に
Share Extension を初めて使用してみましたが、新しい記事が見つかりにくく、コードの修正などが手間になるかなと思い少し長くなってしまいましたがまとめてみました。今後は、Share Extension 以外にも App Extension を活用した機能がまだまだあるので上手に活用していきたいと思いました。
ちなみに(少し手を加えましたが)今回のサンプルコードは GitHub にあげたのでダウンロードしてぜひ試してみてください!