Share Extensionでデータを共有する

![shareExtension1.png](https://qiita-image-store.s3.amazonaws.com/0/121434/16d0b183-ac19-4f85-f57f-7c2c6a3502db.png)

![shareExtension2.png](https://qiita-image-store.s3.amazonaws.com/0/121434/a7aef57a-223f-7fde-6bc2-87a15248936f.png)


[App Extension](https://developer.apple.com/jp/documentation/ExtensibilityPG.pdf)の中の**Share Extension**を用いることで、今開いてるページの記事やデータを共有したいアプリにPostして、そのデータの共有行うことが可能です。

iOS8からの新機能で、関連する記事はたくさんありますが、自分なりにまとめておきたかったので手順を書いておきます。

# 1. プロジェクトを作成
今回は SampleShareExtension というプロジェクトを作成したら、続いてShare Extension用のプロジェクトを作っていきます。

File -> New -> Target を押すと以下の画面になります。
この中から Share Extension を選択して追加します。
今回は testShareExtension と名前を付けました。

![shareExtension3.png](https://qiita-image-store.s3.amazonaws.com/0/121434/d6c02ef3-af55-71e3-a77f-9afb419a7794.png)


次に Activate “testShareExtension” scheme? と聞かれるので Activate を選択します。

しばらく待つと作成した testShareExtension の項目が追加されています。
追加された ShareViewController.swift にPost後の処理などコードを後に書いていきます。

![shareExtension4.png](https://qiita-image-store.s3.amazonaws.com/0/121434/b78de42d-a57a-5476-4c48-8d50d7239b69.png)



# 2. App Group設定

図のように Project から TARGETS -> SampleShareExtension を選択し、上の項目の Capabilities を選択します。 **Keychain Sharing と App Groups を ON にします。**

![shareExtension5.png](https://qiita-image-store.s3.amazonaws.com/0/121434/1ce4474a-b208-be23-9d3d-947112fcfc8e.png)


この 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 プログラミングガイド](https://developer.apple.com/jp/documentation/ExtensibilityPG.pdf)の25~26pに

> 「Share」/「Action」Extensionは、ある特定の型のデータしか扱えない可能性があります。取り扱い可能なデータをユーザが選択したときにのみ、ホストアプリケーションからExtensionを起動できるようにしたい場合は、ExtensionのInfo.plistプロパティリストファイルにNSExtensionActivationRuleキーを追加してください。

と記述されているように testShareExtension の Info.plist の中で画像やURLなど、どのデータを扱うかの設定を行います。

- NSExtension
    - NSExtensionAttributes
        - NSExtensionActivationRule (TRUEPREDICATE)

現状このようになっています。[NSExtensionActivationRule](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/AppExtensionKeys.html) が全ての情報を許可する状態 TRUEPREDICATE になっているので、これを以下の図のように Type を Dictionary に変えて、その中に扱う情報を記述していきます。

![shareExtension6.png](https://qiita-image-store.s3.amazonaws.com/0/121434/96de5a8b-2e26-d161-f0f6-a60a28c89640.png)



|              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

![shareExtension7.png](https://qiita-image-store.s3.amazonaws.com/0/121434/4f5f5f1b-a9e0-9c5b-94e1-06809d43e1ff.png)



# 4. Post処理

ShareViewController.swift にコードを記述していきます。
元から記述されている以下の3つのメソッドには、それぞれこのような内容を書くことができます。

- **isContentValid() -> Bool** : 文字入力されていないとPostを押せないようにする
- **didSelectPost()** : Postを押した後の処理
- **configurationItems() -> [Any]!** : 追加項目のリスト管理


まずは isContentValid() -> Bool から書いていきます。

```swift
/**
 * 文字入力されていないと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で例えるなら、入力したツイートのことです。）


<br>

次に本題のPost処理について記述していきます。
Post処理は didSelectPost() に書きます。

NSItemProviderで共有する情報に [uniform type identifiers (UTIs)](https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html) を用いるため、例えばURLを取得するのに kUTTypeURL が使用できるようにするため、 MobileCoreServices を import します。

```swift
import MobileCoreServices
```

<br>

[hasItemConformingToTypeIdentifier(puclicURL)](https://developer.apple.com/reference/foundation/nsitemprovider/1403921-hasitemconformingtotypeidentifie) で URL の情報を持っているか探し、 [loadItem](https://developer.apple.com/reference/foundation/nsitemprovider/1403900-loaditem) で情報を読み取って、そのデータを指定された型に変換します。

```swift
/**
 * 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を取得する場合は、

```swift
let url: NSURL = item as? NSURL
```

と変換してあげれば取得できる。（また、String型で保存する場合は `url.absoluteString` としてあげる必要がある。）

## 保存方法

`userDefaults.standard` のところを App Groups で共有するため、 suiteName を指定する必要がある。

```swift
let suiteName: String = "group.[自分のBundle Identifier].SampleShareExtension"
let keyName: String = "shareData"
```

```swift
// ---------------
// データの保存
// ---------------
let sharedDefaults: UserDefaults = UserDefaults(suiteName: self.suiteName)!
sharedDefaults.set(url.absoluteString!, forKey: self.keyName)  // そのページのURL保存
sharedDefaults.synchronize()
```

ViewController.swift での読み込みも同様に同じ suiteName を指定してあげれば読み込むことが可能です。

```swift
// ---------------
// データの読み込み
// ---------------
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)  // 削除
}
```

<br>

以上を踏まえて、Safari から URL や image、別アプリから PDF を読み込むコードを以下にまとめました。


# URLを取得する

Safariから そのページのURLを取得して、メインのアプリで読み込み、Safariを起動してそのページを表示する方法です。

```swift:ShareViewController.swift
/**
 * 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)
        })
    }
}
```

<br>

```swift:ViewController.swift
// --------------------
// データの読み込み（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 に表示させています。

```swift:ShareViewController.swift
/**
 * 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)
        })
    }
}
```

<br>

```swift
@IBOutlet weak var imageView: UIImageView!
```

```swift:ViewController.swift
// --------------------
// データの読み込み（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](https://developer.apple.com/reference/foundation/nsitemprovider/1403900-loaditem) ではなく [loadPreviewImage](https://developer.apple.com/reference/foundation/nsitemprovider/1403925-loadpreviewimage) を用いて指定したプレビュー画像を読み込みます。


# PDFを取得する

別アプリからPDFデータを取得して、メインのアプリに保存するやり方です。
**Info.plist から NSExtensionActivationSupportsFileWithMaxCount を追加します。**

- NSExtension
    - NSExtensionAttributes
        - NSExtensionActivationRule
            - NSExtensionActivationSupportsFileWithMaxCount  | Number | 1

<br>

また、メインのアプリから保存したPDFデータを別のアプリで開くやり方も合わせて記載しました。[UIDocumentInteractionController](https://developer.apple.com/reference/uikit/uidocumentinteractioncontroller) を用いてPDFデータを扱えるアプリケーションを表示し、選択したアプリケーションにImportすることで可能です。

```swift:ShareViewController.swift
/**
 * 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)
        })
    }
}
```

<br>

**UIDocumentInteractionControllerDelegate を追加**して以下のコードを**インスタンス変数**として記述します。

```swift
var documentInteractionController: UIDocumentInteractionController?
```

```swift:ViewController.swift
// --------------------
// データの読み込み（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)
}
```

<br>

ちなみにPDFデータがディレクトリ内にあるかの確認は、
Window -> Devices の中から対象のアプリケーションを選択することでディレクトリ内のデータを見ることができます。


# Build & Run

![shareExtension8.png](https://qiita-image-store.s3.amazonaws.com/0/121434/14362b48-b3d3-b7da-317e-dcda5a5753dc.png)


上記のように、Share Extension を追加すると2種類選択できるようになっています。

testShareExtension を選択すると Choose an app to run: と表示され、どのアプリケーションから Share Extension を動かすか選択できます。

ここでは、
testShareExtension でデータをPost -> 一度アプリを終了させる -> SampleShareExtension に切り替えてデータを読み込む
という流れで行なっています。


# おまけ

```swift:ShareViewController.swift
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 = "保存"
}
```

このように書くと以下の画像のような画面になります。
ちなみに画像を貼り付けたりすることも可能なので細かく設定したい方は、上記のコードを例に色々と追加してみるといいかもしれません。

![shareExtension9.png](https://qiita-image-store.s3.amazonaws.com/0/121434/c4910fe1-09c6-413b-d214-50d4a7b47a56.png)



# 参考にしたサイト

以下のサイトを主に参考にさせていただきました:bow_tone1:

- Share Extension
    - [How-to Build an iOS Share Extension in Swift](https://hackernoon.com/how-to-build-an-ios-share-extension-in-swift-4a2019935b2e#.7rv9hv3fj)
    - [今更ですが iOS8 で実装された Share Extension を美味しくいただきました](http://blog.asobicocoro.com/entry/2016/04/06/235825)

<br>

- UIDocumentInteractionController (PDFデータ関連)
    - [[Swift 3.0] iOSアプリ間でファイルの送受信を行う](http://joyplot.com/documents/2016/10/14/swift-file-send-recieve/)
    - [Swift PDF Download Export 実装方法](http://amarron.hatenablog.com/entry/2015/11/29/163923)
    - [Swiftで画像をPDFに変換させる](http://qiita.com/ichiki1023/items/9cdd44334285d43d5f93)

<br>

- userDefaults
    - [[Swift 3.0] Swift3.0で、NSUserDefaultsはUserDefaultsになった](http://dev.classmethod.jp/smartphone/swift-3-0-userdefaults/)
    - [【Swift】NSUserDefaultsの使い方。アプリの設定情報を保存する。](http://hajihaji-lemon.com/smartphone/swift/nsuserdefaults/)


# 最後に
Share Extension を初めて使用してみましたが、新しい記事が見つかりにくく、コードの修正などが手間になるかなと思い少し長くなってしまいましたがまとめてみました。今後は、Share Extension 以外にも App Extension を活用した機能がまだまだあるので上手に活用していきたいと思いました。

ちなみに（少し手を加えましたが）今回のサンプルコードは [GitHub](https://github.com/KosukeGit/SampleShareExtension) にあげたのでダウンロードしてぜひ試してみてください！