AppExtensionをCoreDataと連携させる(swift)


## はじめに

AppExtensionでShareしたURLをCoreDataに保存しようとしたときにいろいろハマったのでメモを残しておきます。

## 環境

Xcode 6.3.1
swift 1.2

アプリのDeployment Targetは`8.0`以上を想定しています。
検証はしていませんが、Embedded FrameworkがswiftだとiOS7でも動作するようなので、`7.0`でも問題ないかもしれません。

* [Embedded Framework を iOS7 以下で使うには - Toyship.org](http://www.toyship.org/archives/2111)

## やること

1. プロジェクトの作成
2. AppExtensionの作成
3. Embedded Frameworkの作成
4. App Groupsの設定
5. CoreData周りの実装
6. AppExtension周りの実装

### 1. プロジェクトの作成

Xcodeの[File] -> [New] -> [Project]から新しいプロジェクトを作成します。

今回は`Single View Application`でプロジェクトを作成はし、アプリ名を`AppExtension_CoreData`としました。
`Use CoreData`のチェックボックスのチェックを忘れずに入れておきます。

![スクリーンショット 2015-12-09 19.29.56.png](https://qiita-image-store.s3.amazonaws.com/0/31823/1b1daaa3-3baf-0552-4e93-4dc66fad6489.png "スクリーンショット 2015-12-09 19.29.56.png")

### 2. AppExtensionの作成

プロジェクト設定から`Application Extension`を追加します。

![スクリーンショット 2015-12-09 19.33.47.png](https://qiita-image-store.s3.amazonaws.com/0/31823/d8398c42-a506-c663-1e43-53b073dd0017.png "スクリーンショット 2015-12-09 19.33.47.png")

今回は`Share Extension`を選択します。
Product Nameは`ACShareExtension`としました。

![スクリーンショット 2015-12-09 19.34.36.png](https://qiita-image-store.s3.amazonaws.com/0/31823/eac3e3a4-6358-92c9-e586-5a3681f2c314.png "スクリーンショット 2015-12-09 19.34.36.png")

### 3. Embedded Frameworkの作成

上と同様にプロジェクト設定から`Framework & Libaray`を選択し、`Cocoa Touch Framework`を追加します。

![スクリーンショット 2015-12-09 19.36.34.png](https://qiita-image-store.s3.amazonaws.com/0/31823/d7c6b4ae-0c64-cbc0-ece5-235fcd37738a.png "スクリーンショット 2015-12-09 19.36.34.png")

Product Nameは`ACFramework`としました。

![スクリーンショット 2015-12-09 19.36.55.png](https://qiita-image-store.s3.amazonaws.com/0/31823/af2f8198-b9aa-602b-e016-842f20fa46ec.png "スクリーンショット 2015-12-09 19.36.55.png")

このEmbedded Frameworkはアプリ本体とエクステンション間でソースコードを共有するために使用します。
今回は主にCoreDataのハンドリング周りの処理をFrameworkに追加していきます。

### 4. App Groupsの設定

App Groupsはアプリ本体とエクステンション間でUserDefaultsやファイル、CoreDataなどを共有するための仕組みとなります。

プロジェクト設定の`Capabilities` -> `App Goups`スイッチをONにします。
+ボタンを押して、gourpの識別子を入力します。
この識別子は`group.`から始まるように設定します。
識別子が作成されたらチェックボックスにチェックを入れておきます。

![スクリーンショット 2015-12-09 19.41.26.png](https://qiita-image-store.s3.amazonaws.com/0/31823/76f0401e-2f44-6331-7452-fd8ef5feb1cd.png "スクリーンショット 2015-12-09 19.41.26.png")

次に、AppExtensionのターゲットに移動し、`App Groups`のスイッチをONにした後に、作成した識別子と同じもののチェックボックスにチェックを入れます。

### 5. CoreData周りの実装

#### CoreData Stackの移動

まずはAppDelegateに自動生成されるCoreData周りの処理をEmbedded Frameworkの方に全て移動します。

ACFrameworkに`ACCoreDataManager`という名前でクラスを作成しました。
このクラスはシングルトンクラスとして、sharedInstanceからCoreDataへアクセスするようにしています。

```swift:ACCoreDataMamager.swift
import UIKit
import CoreData

public class ACCoreDataMamager: NSObject {

    // MARK: - Shared Manager
    public class var sharedInstance : ACCoreDataMamager {
        struct Static {
            static let instance = ACCoreDataMamager()
        }
        return Static.instance
    }

    // MARK: - Core Data stack
    lazy var applicationDocumentsDirectory: NSURL = {
....
```

#### Entityの作成

次に、データモデルを編集します。
`AppExtension_CoreData.xcdatamodel`を選択し、エンティティを追加します。
今回は`Memo`というエンティティに`date`と`title`をもったシンプルなものにしました。

![スクリーンショット 2015-12-09 19.52.55.png](https://qiita-image-store.s3.amazonaws.com/0/31823/857fae44-9cf7-8da1-ede9-72fd52aab483.png "スクリーンショット 2015-12-09 19.52.55.png")

この際に、右カラムの`Target Membership`にエクステンションのターゲットにもチェックを入てください。

![スクリーンショット 2015-12-09 19.53.16.png](https://qiita-image-store.s3.amazonaws.com/0/31823/0d7976cb-f03a-44ba-40d1-2a643d0bbca5.png "スクリーンショット 2015-12-09 19.53.16.png")

#### Entityのサブクラスを作成

データへのアクセスを楽にするために、先ほど作成した`Memo`エンティティのサブクラスを作成します。
ACFrameworkにNSManagedObjectクラスの`ACMemoEntity`を作成します。

```swift:ACMemoEntity.swift
import UIKit
import CoreData

public class ACMemoEntity: NSManagedObject {
    @NSManaged public var title : String
    @NSManaged public var date : NSTimeInterval
}
```

作成を終えたら、`AppExtension_CoreData.xcdatamodel`に戻って、`Memo`エンティティの名前空間を登録しておきます。

![スクリーンショット 2015-12-09 19.59.02.png](https://qiita-image-store.s3.amazonaws.com/0/31823/41d39196-7e2a-00d6-cd41-0e8d1c52564d.png "スクリーンショット 2015-12-09 19.59.02.png")

くわしくはこちら↓

* [SwiftでCoreData - Qiita](http://qiita.com/watanave/items/4da9f4bc97247f780af8)

#### CoreDataのラッパークラスの作成

CoreDataのデータへのアクセスを楽にするために、ラッパークラスを作成します。
ACFrameworkに`ACDataManager`というのを作成し、`Memo`エンティティへの追加と取得部分の実装を追加します。

```swift:ACDataManager.swift
import UIKit
import CoreData

public class ACDataManager: NSObject {
    public static func createMemo(title : String, block : (Bool) -> Void) {
        let context = ACCoreDataMamager.sharedInstance.managedObjectContext
        let entity : NSEntityDescription = NSEntityDescription.entityForName("Memo", inManagedObjectContext: context!)!

        var memo : ACMemoEntity = NSEntityDescription.insertNewObjectForEntityForName(entity.name!, inManagedObjectContext: context!) as! ACMemoEntity
        memo.title = title
        memo.date = NSDate().timeIntervalSince1970

        ACCoreDataMamager.sharedInstance.saveContext(block)
    }

    public static func getMemo() -> Array<ACMemoEntity>{

        let fetchRequest : NSFetchRequest = NSFetchRequest()
        let context = ACCoreDataMamager.sharedInstance.managedObjectContext
        let entity : NSEntityDescription = NSEntityDescription.entityForName("Memo", inManagedObjectContext: context!)!
        fetchRequest.entity = entity

        let sortDescriptor = NSSortDescriptor(key: "date", ascending: false)
        let sortDescriptors = [sortDescriptor]
        fetchRequest.sortDescriptors = sortDescriptors

        var result : Array<ACMemoEntity> = Array()
        var error : NSError? = nil
        if let sortedArray = context?.executeFetchRequest(fetchRequest, error: &error){
            for object in sortedArray {
                if let memo = object as? ACMemoEntity{
                    result.append(memo)
                }
            }
        }

        return result
    }
}
```

#### データ参照先の変更

自動生成されたCoreDataのソースコードでは、データをアプリのDocumentディレクトリから参照しています。

```swift:ACCoreDatamanager.swift
lazy var applicationDocumentsDirectory: NSURL = {
        let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
        return urls[urls.count-1] as! NSURL
        }()
```

これだと、AppExtensionから参照を行うことができないため、AppGroupの共通ディレクトリを参照するように編集します。

```swift:ACCoreDatamanager.swift
lazy var applicationDocumentsDirectory: NSURL = {
        let directory = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.com.peromasamune.appextension_coredata")
        return directory!
        }()
```

### 6. AppExtension周りの実装

エクステンション部分の実装を行います。
エクステンション追加時に自動生成されるSLComposeViewControllerクラスの`ShareViewController`を編集していきます。

`MobileCoreServices`と`ACFramework`をimportします。

```swift:ShareViewController.swift
import MobileCoreServices
import ACFramework

class ShareViewController: SLComposeServiceViewController {

...
```

`didSelectedPost()`へ投稿ボタンがおされた時の処理を実装します。

```swift:ShareViewController.swift
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.
        
        let inputItem : NSExtensionItem = self.extensionContext!.inputItems.first as! NSExtensionItem
        let itemProvider : NSItemProvider = inputItem.attachments?.first as! NSItemProvider
        let contentType = kUTTypeURL as String
        
        if itemProvider.hasItemConformingToTypeIdentifier(contentType) {
            itemProvider.loadItemForTypeIdentifier(contentType, options: nil, completionHandler: { (item, error) -> Void in
                if let urlItem = item as? NSURL {
                    println("url : \(urlItem.absoluteString) title : \(self.contentText)")
                    
                    ACDataManager.createMemo(self.contentText, block: { (completed) -> Void in
                        let outputItem : NSExtensionItem = inputItem.copy() as! NSExtensionItem
                        outputItem.attributedContentText = NSAttributedString(string: self.contentText, attributes: nil)
                        let outputItems = [outputItem]
                        self.extensionContext?.completeRequestReturningItems(outputItems, completionHandler: { (completed) -> Void in
                            
                        })
                    })
                }
            })
        }
    }
```

今回は投稿されたテキストを取得して、CoreDataへ保存するようにしました。

![スクリーンショット 2015-12-09 20.15.07.png](https://qiita-image-store.s3.amazonaws.com/0/31823/4b845a0b-de67-c21a-e8ea-cfc32bcb44b3.png "スクリーンショット 2015-12-09 20.15.07.png")

SafariのActionボタンの`その他`を押すと、作成したエクステンションが表示されるので、スイッチをONにします。

![スクリーンショット 2015-12-09 20.16.54.png](https://qiita-image-store.s3.amazonaws.com/0/31823/d8d802ca-1e58-1f42-0b15-60c34021730e.png "スクリーンショット 2015-12-09 20.16.54.png")

Postボタンを押すと、入力した内容がアプリ側へ保存されていることが確認できました。

## おわりに

今回はAppExtensionとCoreDataを連携させる方法について紹介しました。
AppExtension自体の機能追加はサクッとできてしまいますが、アプリ本体との処理やデータの共有などについては実装前にいろいろと考えておいた方が良さそうです。

AppGroupの概念がなかなかつかめずに結構ハマりました。
アプリのファイル領域とは別の場所を共通領域とするようです。

本記事のサンプルコードは下記githubにあります。

* [AppExtension_CoreData - github](https://github.com/peromasamune/AppExtension_CoreData)

## 参考資料

### AppExtension
* [アプリケーション拡張機能 - Apple Developer](https://developer.apple.com/jp/documentation/UserExperience/Conceptual/MobileHIG/AppExtensions/AppExtensions.html)
* [[iOS 8] App Extensionでテンション倍増！ – Share Extension編 - Developers.IO](http://dev.classmethod.jp/references/ios8-share-extension/)
* [iOS8のApp Extensionsをつくってみる（Share 実装編）- Qiita](http://qiita.com/monoqlo/items/6ed93f0d2e719acd9474)

### Embedded Framework
* [[iOS 8] App Extension #2 – Embedded Framework を利用して共有コードを Framework 化する - Developers.IO](http://dev.classmethod.jp/references/ios-8-embedded-framework/)

### App Group
* [[iOS 8] App Extension #3 – App Group で containing app とデータを共有する - Developers.IO](http://dev.classmethod.jp/references/ios-8-app-group/)
* [Shareing Core Data between App and Extension in iOS8 - Sam Oakley](http://blog.sam-oakley.co.uk/post/92323630293/sharing-core-data-between-app-and-extension-in-ios)