• トップ
  • ブログ一覧
  • 【iOS14】Realmで保存したデータをWidgetに表示してみた
  • 【iOS14】Realmで保存したデータをWidgetに表示してみた

    広告メディア事業部広告メディア事業部
    2020.12.07

    IT技術

    Realmで保存したデータをWidgetに表示する方法をまとめてみた!

    小林先生

    (株)ライトコードの小林(こばやし)です!

    前回の記事では、iOS 14から追加された WidgetKit を使い、Widget 実装を実践してみました。

    今回は Realm を使って、アプリ内で保存したデータを、Widget に表示する方法をまとめていきます!

    開発環境

    開発環境は以下のとおりです。

    MacOS10.15.4
    Swift5.3
    Xcode12.0.1
    Realm5.5.0

    では早速、始めていきましょう!

    前回の記事

    【iOS14】WidgetKitを使ってみた!2020.11.24【iOS14】WidgetKitを使ってみた!WidgetKitの実装方法をまとめてみた小林先生(株)ライトコードの小林(こばやし)です!iOS14からホーム画面に...

    Widgetの元となるアプリを実装する

    それでは前回と同様、Widget の元となるアプリを、まずは実装していきます。

    ちなみに今回作るアプリは、テキストフィールドに入力した値を、Realm に保存するのみの機能です。

    構成は MVVM で、Combine を使っていきます。

    実装準備

    まずは、プロジェクトを作成します。

    プロジェクトを作成

    次に、Realm を導入します。

    今回は、SPM(Swift Package Manager)を使って、Realm をインストールしてみましょう。

    メニューバーから、「File > Swift Packages > Add Package Dependency… 」を開いて、Realm のリポジトリ URL を入力します。

    Realm をインストール

    「Up to Next Major」で、バージョンを5.0.0に指定し、次に進みます。

    「Up to Next Major」で、バージョンを5.0.0に指定

    最後に、Realm と RealmSwift にチェックをつけて、インストール開始です。

    Realm と RealmSwift にチェックをつけて、インストール開始

    これで、実装前の準備は完了です。

    アプリ実装

    今回は、Realm を使ってデータを保存するので、まずは Realm に保存する「データ項目」と「処理を定義したクラス」を作ります。

    1import Foundation
    2import RealmSwift
    3
    4class Memo: Object {
    5    @objc dynamic var id: Int = 0
    6    @objc dynamic var text: String = ""
    7
    8    override static func primaryKey() -> String? {
    9        "id"
    10    }
    11}
    12
    13extension Memo {
    14    private static var realm: Realm = try! Realm()
    15
    16    static func all() -> Results<Memo> {
    17        realm.objects(self)
    18    }
    19
    20    static func create(with memo: Memo) {
    21        try! realm.write {
    22            realm.create(Memo.self, value: memo, update: .all)
    23        }
    24    }
    25}

    次に、ViewModel を実装します。

    1import Combine
    2
    3class ContentViewModel :ObservableObject {
    4    @Published var text = Memo.all().first?.text ?? ""
    5    var saveButtonTapped = PassthroughSubject<Void, Never>()
    6
    7    private var cancellables = [AnyCancellable]()
    8
    9    init() {
    10        saveButtonTapped
    11            .sink(
    12                receiveValue: { [weak self] in
    13                    guard let self = self else { return }
    14                    let memo = Memo()
    15                    memo.text = self.text
    16                    Memo.create(with: memo)
    17                }
    18            )
    19            .store(in: &cancellables)
    20    }
    21}

    最後に、画面を実装していきましょう!

    1import SwiftUI
    2
    3struct ContentView: View {
    4    @ObservedObject var viewModel: ContentViewModel
    5
    6    var body: some View {
    7        VStack {
    8            TextEditor(text: $viewModel.text)
    9                .frame(width: UIScreen.main.bounds.width * 0.8, height: 200)
    10                .border(Color(.systemBlue), width: 1)
    11            Button(
    12                action: {
    13                    viewModel.saveButtonTapped.send()
    14                },
    15                label: {
    16                    Text("Save")
    17                }
    18            )
    19        }
    20    }
    21}
    22
    23struct ContentView_Previews: PreviewProvider {
    24    static var previews: some View {
    25        ContentView(viewModel: ContentViewModel())
    26    }
    27}

    「Build & Run」すると、こんな感じになりました!

    皆さんも、同じような感じになりましたか?

    アプリ実装

    Widgetを実装する

    それでは、作ったアプリの Widget も実装してきます。

    Widgetを追加する

    メニューバーから「File > New > Target…」を開いて、「widget」を検索します。

    メニューバーから「File > New > Target…」を開いて、「widget」を検索

    ProductName などを入力して、Finish を押します。

    次に、プロジェクト配下に新しく、Widget 用ディレクトリを追加していきましょう。

    プロジェクト配下に新しく、Widget 用ディレクトリを追加

    続いて下記画像のように、Memo.swift と ContentViewModel の Target Membership に追加します。

    Memo.swift と ContentViewModel の Target Membership に追加

    また、WidgetExtension でも RealmSwift を使用したいので、WidgetExtension の Build Phases で Library を連携します。

    WidgetExtension でも RealmSwift を使用したいので、WidgetExtension の Build Phases で Library を連携

    Widgetを実装する

    では、いよいよ Widget を実装します。

    とりあえずは、「空のデータを表示する Widget 」とします。

    1import WidgetKit
    2import SwiftUI
    3
    4struct Provider: TimelineProvider {
    5    func placeholder(in context: Context) -> SimpleEntry {
    6        let memo = Memo()
    7        return SimpleEntry(date: Date(), memo: memo)
    8    }
    9
    10    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
    11        let memo = Memo()
    12        let entry = SimpleEntry(date: Date(), memo: memo)
    13        completion(entry)
    14    }
    15
    16    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    17        var entries: [SimpleEntry] = []
    18        let currentDate = Date()
    19        let memo = Memo()
    20
    21        let entry = SimpleEntry(date: currentDate, memo: memo)
    22        entries.append(entry)
    23
    24        let timeline = Timeline(entries: entries, policy: .atEnd)
    25        completion(timeline)
    26    }
    27}
    28
    29struct SimpleEntry: TimelineEntry {
    30    let date: Date
    31    let memo: Memo
    32}
    33
    34struct WidgetWithRealmWidgetEntryView : View {
    35    var entry: Provider.Entry
    36
    37    var body: some View {
    38        Text(entry.memo.text)
    39    }
    40}
    41
    42@main
    43struct WidgetWithRealmWidget: Widget {
    44    let kind: String = "WidgetWithRealmWidget"
    45
    46    var body: some WidgetConfiguration {
    47        StaticConfiguration(kind: kind, provider: Provider()) { entry in
    48            WidgetWithRealmWidgetEntryView(entry: entry)
    49        }
    50        .configurationDisplayName("メモ")
    51        .description("メモを確認できます。")
    52    }
    53}
    54
    55struct WidgetWithRealmWidget_Previews: PreviewProvider {
    56    static var previews: some View {
    57        WidgetWithRealmWidgetEntryView(entry: SimpleEntry(date: Date(), memo: Memo()))
    58            .previewContext(WidgetPreviewContext(family: .systemSmall))
    59    }
    60}

    Widgetに保存したデータを表示する

    アプリの実装と、Widget 追加が完了したので、Widget にアプリ内で保存したデータを表示するようにします。

    App Groupsを設定する

    現在の状態では、Widget でアプリ内に保存したデータを、取得することができません。

    Widget にデータ共有するには、App Groups という機能が必要になります。

    したがってまずは、App Groups の設定を行っていきます。

    アプリの Target を選択し、Signing & Capabilities タブで、App Groups を追加します。

    アプリの Target を選択し、Signing & Capabilities タブで、App Groups を追加

    次に、下記のように名前をつけて、Group を追加します。

    名前をつけて、Group を追加

    Widget の Extension でも、同様に App Groups を追加して、先ほど作成した Group を選択します。

    これで、データを共有する準備が完了しました!

    Widget内でアプリのデータを取得する

    まず、Realm に保存する「データ項目」と、「処理を定義したクラス」を修正していきます。

    先ほど設定した App GroupsのIdentifier を、Realm.Configuration() に設定します。

    1extension Memo {
    2    // 修正箇所
    3    // private static var realm: Realm = try! Realm()
    4    private static var realm: Realm {
    5            var config = Realm.Configuration()
    6            let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.naipaka.WidgetWithRealm")!
    7            config.fileURL = url.appendingPathComponent("db.realm")
    8            let realm = try! Realm(configuration: config)
    9            return realm
    10        }
    11
    12    (省略)
    13}

    次に、Widget 内で Realm に保存したデータを、取得する処理を実装します。

    1struct Provider: TimelineProvider {
    2    func placeholder(in context: Context) -> SimpleEntry {
    3        // 修正箇所
    4        // let memo = Memo()
    5        let memo = Memo.all().first ?? Memo()
    6        return SimpleEntry(date: Date(), memo: memo)
    7    }
    8
    9    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
    10        // 修正箇所
    11        // let memo = Memo()
    12        let memo = Memo.all().first ?? Memo()
    13        let entry = SimpleEntry(date: Date(), memo: memo)
    14        completion(entry)
    15    }
    16
    17    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    18        var entries: [SimpleEntry] = []
    19        let currentDate = Date()
    20        // 修正箇所
    21        // let memo = Memo()
    22        let memo = Memo.all().first ?? Memo()
    23
    24        let entry = SimpleEntry(date: currentDate, memo: memo)
    25        entries.append(entry)
    26
    27        let timeline = Timeline(entries: entries, policy: .atEnd)
    28        completion(timeline)
    29    }
    30}

    続いて、アプリ内でデータを更新したときに Widget も更新させたいので、ContentViewModel.swift に Widget 更新処理を追加します。

    1import Combine
    2// 追加
    3import WidgetKit
    4
    5class ContentViewModel :ObservableObject {
    6    @Published var text = Memo.all().first?.text ?? ""
    7    var saveButtonTapped = PassthroughSubject<Void, Never>()
    8
    9    private var cancellables = [AnyCancellable]()
    10
    11    init() {
    12        saveButtonTapped
    13            .sink(
    14                receiveValue: { [weak self] in
    15                    guard let self = self else { return }
    16                    let memo = Memo()
    17                    memo.text = self.text
    18                    Memo.create(with: memo)
    19                    // 追加
    20                    WidgetCenter.shared.reloadAllTimelines()
    21                }
    22            )
    23            .store(in: &cancellables)
    24    }
    25}

    最後に、「Bulid & Run」して、Widget を追加すると…

    Realm に保存したデータが、Widget に表示

    無事、Realm に保存したデータが、Widget に表示されました!

    さいごに

    今回は、Realm に保存したデータを、Widget に表示する方法をまとめていきました。

    前回は静的なデータを表示するのみでしたが、アプリ内のデータが共有できるようになると、表現できる幅も広がります。

    また、 Timeline の機能を使うと、「時間になったら DB の値を更新する」といったこともできます。

    ぜひ試してみてくださいね!

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
    featureImg2020.08.14スマホ技術 特集Android開発Android開発をJavaからKotlinへ変えていくためのお勉強DelegatedPropert...

    広告メディア事業部

    広告メディア事業部

    おすすめ記事