App Extension(Today Extension)でRealmを使う

本記事では、iOSアプリ本体とToday Extensionとの間で[Realm Mobile Database](https://realm.io/)のファイルを共有する方法を紹介します。
その他のApp Extensionでも同様の方法が使えます。

#環境
* Xcode 8.2
* Swift 3
* Realm Swift 2.4.4
* iOS 10.2

#Today Extensionとは
Today Extensionとは、iOS 8から導入されたウィジェットを実装するための[App Extension](https://developer.apple.com/app-extensions/)の一つです。
App Extensionには他にもAction ExtensionやShare Extensionなど色々とありますが、これらを実装する場合でも本記事で紹介する方法が使えます。

App Extensionでは、アプリ本体で使用されているデータやソースコードをそのまま共有して使うことができません。
別のアプリを新たに作成しているような扱いとなります。

そこで、アプリ本体とApp Extensionの間でデータを共有するためには**App Groups**という仕組みを利用します。
ソースコードを共有するためには**Embedded Framework**という仕組みを利用します(本記事ではこちらについての説明は省略します)。

#本記事で作成するアプリ
本記事では、アプリ本体の画面に入力した文字列をそのままウィジェット上に表示するようなシンプルなアプリを作成していきます。

![app_overview.png](https://qiita-image-store.s3.amazonaws.com/0/156137/55155648-0eb0-c8c2-30d8-094aa966ef62.png)


#手順
##Step 1. プロジェクトを作成する
Xcodeで普通にプロジェクトを作成します。
Create a new Xcode project → iOS → Single View Application

Product NameはここではMyAppとします。

![step1.png](https://qiita-image-store.s3.amazonaws.com/0/156137/ff99a74c-719a-0309-1791-b586c83920b8.png)


##Step 2. Today Extensionを追加する
Xcodeメニューの File → New → Target を選択すると、Targetを追加するダイアログが現れます。
Application Extensionの中のToday Extensionを選択します。

![step2.png](https://qiita-image-store.s3.amazonaws.com/0/156137/fcafcf37-2b4e-87d8-0a20-fab2e7820461.png)

ここではToday ExtensionのProduct NameをMyExtensionとします。

![step2.1.png](https://qiita-image-store.s3.amazonaws.com/0/156137/c02c22df-200a-641f-022e-daab2855908f.png)

Activate “MyExtension” Scheme?と聞かれるのでActivateしておきます。

##Step 3. App Groupを作成する
この時点で、アプリにはMyAppとMyExtensionという2つのTargetsが作成されていることになります。
これらを同一の**App Group**に含めることで、データを共有することができます。

まず、Targetsの中からMyAppを選択し、Capabilitiesのタブを開きます。
App GroupsのスイッチがOFFになっているので、これをONにします。

![step3.png](https://qiita-image-store.s3.amazonaws.com/0/156137/6bfd0476-14ba-d592-7238-5e1dcc0a96cd.png)

次に、+ボタンをクリックしてApp GroupのIDを入力します。
ここには`group.`に続いてアプリのBundle Identifierを入力します。

これでMyAppがApp Groupに追加されました。
MyExtensionもMyAppと同一のApp Groupに追加しましょう。

Targetsの中からMyExtensionを選択し、先ほどと同様にApp GroupsのスイッチをONにします。
次に、先ほど追加したApp GroupのIDにチェックを入れるとMy ExtensionがApp Groupに追加されます。

##Step 4. Realmを導入する
Realmの詳しい導入方法は[公式ドキュメント](https://realm.io/docs/swift/latest/)を参照してください。
ここではCocoaPodsを使用してプロジェクトにRealmを導入する方法を紹介します。

まず、Xcodeを一旦閉じてCocoaPodsをインストールします。インストール方法の説明は省略します。
CocoaPodsをインストール後、プロジェクトディレクトリ直下で`pod init`を実行すると、同じ場所にPodfileが生成されます。
これを以下のように編集します。

```txt:Podfile
target 'MyApp' do
  use_frameworks!
  pod 'RealmSwift'
end

target 'MyExtension' do
  use_frameworks!
  pod 'RealmSwift'
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['SWIFT_VERSION'] = ‘3.0’
    end
  end
end
```

Podfileを保存して`pod install`を実行すると、MyAppとMyExtensionの両方にRealmが導入されます。

これ以降はMyApp.xcodeprojではなくMyApp.xcworkspaceを開いて作業を行いましょう。

##Step 5. 画面レイアウトを作成する
MyApp.xcworkspaceを開いて、MyAppのMain.storyboardを編集します。
ここでは、文字列をRealm DBに保存するためにText FieldとButtonを適当な場所に配置しておきます。

![step5.1.png](https://qiita-image-store.s3.amazonaws.com/0/156137/e3816dd4-40ee-726d-5d94-0745601ecf9e.png)

次に、ウィジェットの画面レイアウトを作成します。
MyExtensionの中に生成されているMainInterface.storyboardを編集します。
既にHello WorldというLabelが入っているはずなので、これをそのまま使っていきます。無かったら追加してください。
文字の色が白くて見えにくいので、黒に変更しておきます。

![step5.2.png](https://qiita-image-store.s3.amazonaws.com/0/156137/70529af7-7dbf-eea1-10ab-33706376d928.png)

##Step 6. コードを書く
このアプリで実現したいことは以下の3つです。

1. RealmのDBファイルを作成する
2. MyAppのText Fieldに入力された文字列をRealm DBに保存する
3. Realm DBに保存した文字列をMyExtensionのLabelに表示する

###Step 6.1. RealmのDBファイルを作成する
まず、文字列を保存するためのモデルクラスを作成します。

MyAppディレクトリの中にMyModel.swiftというファイルを生成します。
New File → Swift File → Create

MyModel.swiftを以下のように編集します。

```swift:MyModel.swift
import RealmSwift

class MyModel: Object {
    dynamic var id = 0
    dynamic var text = ""
    
    override static func primaryKey() -> String? {
        return "id"
    }
}
```

次に、これとまったく同じ内容のクラスファイルをMyExtensionディレクトリの中にも作成します。
同じ内容のソースコードを別々の場所に置くのはバグの原因になるので望ましくはありませんが、これでも一応動作します。
ソースコードをTarget間で共有したい場合は**Embedded Framework**を使用してください。

Embedded Frameworkについては以下のページに説明があります。
[App Extension Programming Guide: Handling Common Scenarios](https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html)

###Step 6.2. MyAppのText Fieldに入力された文字列をRealm DBに保存する
MyAppのMain.storyboardとViewController.swiftを開き、controlキーを押しながらMain.storyboard上のText FieldからViewController.swiftに向かってドラッグします。

![step6.2.1.png](https://qiita-image-store.s3.amazonaws.com/0/156137/5d147ce6-4b79-c11d-e2ce-8c2f49b195e5.png)

Outletを追加するダイアログが現れるので、textFieldという名前で追加します。

次に、controlキーを押しながらMain.storyboard上のButtonからViewController.swiftに向かってドラッグします。

![step6.2.2.png](https://qiita-image-store.s3.amazonaws.com/0/156137/0c4d4d08-7ee7-28bc-fc45-5a73393efa8a.png)

今回はOutletではなくActionを追加します。名前はbuttonClickedとしておきます。

![step6.2.3.png](https://qiita-image-store.s3.amazonaws.com/0/156137/41b871fc-ff0d-328e-e3ea-e95c2e0e3bcf.png)

次に、「保存ボタンをタップしたときにテキストフィールド内の文字列をRealm DBに書き込む」という処理ができるようにViewController.swiftを書き換えていきます。

最終的なコードは以下のようになります。

```swift:ViewController.swift
import UIKit
import RealmSwift

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    @IBAction func buttonClicked(_ sender: Any) {
        var config = Realm.Configuration()
        let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.MyApp")!
        config.fileURL = url.appendingPathComponent("db.realm")
        let realm = try! Realm(configuration: config)
        
        try! realm.write {
            realm.create(MyModel.self, value: ["id": 0, "text": textField.text!], update: true)
        }
    }    
}
```

注意すべきは`FileManager.default.containerURL()`の引数です。
ここにはStep 3.で設定したApp GroupのIDを入力してください。

他の場所からもRealmを参照したい場合は、Realm Configurationの`fileURL`を必ず上記のように設定してからRealmを参照してください。

Realm ConfigurationをdefaultConfigurationとして設定しておくこともできます。詳しくは[公式ドキュメント](https://realm.io/docs/swift/latest/#realm-configuration)を参照してください。

###Step 6.3. Realm DBに保存した文字列をMyExtensionのLabelに表示する
MyExtensionのMainInterface.storyboardとTodayViewController.swiftを開き、controlキーを押しながらMainInterface.storyboard上のLabelからTodayViewController.swiftに向かってドラッグします。

![step6.3.1.png](https://qiita-image-store.s3.amazonaws.com/0/156137/d1d9821c-6286-7d00-d4ee-9ef8e3da548a.png)

Outletを追加するダイアログが現れるので、labelという名前で追加します。

次に、「Realm DBに保存された文字列をウィジェットの画面が更新されるタイミングで読み込んでラベルに表示する」という処理ができるようにTodayViewController.swiftを書き換えていきます。

最終的なコードは以下のようになります。

```swift:TodayViewController.swift
import UIKit
import NotificationCenter
import RealmSwift

class TodayViewController: UIViewController, NCWidgetProviding {
    
    @IBOutlet weak var label: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) {
        var config = Realm.Configuration()
        let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.MyApp")!
        config.fileURL = url.appendingPathComponent("db.realm")
        let realm = try! Realm(configuration: config)
        let result = realm.object(ofType: MyModel.self, forPrimaryKey: 0)
        label.text = result?.text
        
        completionHandler(NCUpdateResult.newData)
    }
}
```

ここでもRealm Configurationの`fileURL`をMyAppのViewControllerと同様に設定しています。
これによって、同一のApp Groupに含まれているMyAppとMyExtensionの両方が同じRealmのDBファイルを参照することができます。

##Step 7. 実行する
MyAppを実行し、テキストフィールドに適当な文字列を入力して保存ボタンを押します。
通知センターなどからMyExtensionのウィジェットを表示するように設定すると、保存した文字列がウィジェット上に表示されます。

#参考
* [App Extension Programming Guide: Handling Common Scenarios](https://developer.apple.com/library/content/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html)

* [Realm Swift](https://realm.io/docs/swift/latest/)