LoginSignup
13
16

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-12-09

はじめに

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

環境

Xcode 6.3.1
swift 1.2

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

やること

  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

2. AppExtensionの作成

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

スクリーンショット 2015-12-09 19.33.47.png

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

スクリーンショット 2015-12-09 19.34.36.png

3. Embedded Frameworkの作成

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

スクリーンショット 2015-12-09 19.36.34.png

Product NameはACFrameworkとしました。

スクリーンショット 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

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

5. CoreData周りの実装

CoreData Stackの移動

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

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

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というエンティティにdatetitleをもったシンプルなものにしました。

スクリーンショット 2015-12-09 19.52.55.png

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

スクリーンショット 2015-12-09 19.53.16.png

Entityのサブクラスを作成

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

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

くわしくはこちら↓

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

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

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ディレクトリから参照しています。

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

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

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

6. AppExtension周りの実装

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

MobileCoreServicesACFrameworkをimportします。

ShareViewController.swift
import MobileCoreServices
import ACFramework

class ShareViewController: SLComposeServiceViewController {

...

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

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

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

スクリーンショット 2015-12-09 20.16.54.png

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

おわりに

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

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

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

参考資料

AppExtension

Embedded Framework

App Group

13
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
16