Edited at

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

More than 3 years have passed since last update.


はじめに

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のチェックボックスのチェックを忘れずに入れておきます。


2. AppExtensionの作成

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

今回はShare Extensionを選択します。

Product NameはACShareExtensionとしました。


3. Embedded Frameworkの作成

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

Product NameはACFrameworkとしました。

このEmbedded Frameworkはアプリ本体とエクステンション間でソースコードを共有するために使用します。

今回は主にCoreDataのハンドリング周りの処理をFrameworkに追加していきます。


4. App Groupsの設定

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

プロジェクト設定のCapabilities -> App GoupsスイッチをONにします。

+ボタンを押して、gourpの識別子を入力します。

この識別子はgroup.から始まるように設定します。

識別子が作成されたらチェックボックスにチェックを入れておきます。

次に、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をもったシンプルなものにしました。

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


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エンティティの名前空間を登録しておきます。

くわしくはこちら↓


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へ保存するようにしました。

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

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


おわりに

今回はAppExtensionとCoreDataを連携させる方法について紹介しました。

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

AppGroupの概念がなかなかつかめずに結構ハマりました。

アプリのファイル領域とは別の場所を共通領域とするようです。

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


参考資料


AppExtension


Embedded Framework


App Group