9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

フューチャーAdvent Calendar 2023

Day 5

CoreData に登録したデータを JSON で出力したい!

Last updated at Posted at 2023-12-05

はじめに

こんにちは。HIGの清水です。
本記事は、フューチャー Advent Calendar 2023の5日目の記事です。
昨日は、 @shibukawa さんの「HTTPでも双方向ソケット通信にチャレンジしてみた(けどやめた)」でした。
今年は社内でも珍しく、iOSアプリ開発のためにSwiftを読んだり書いたりする機会が多かった一年でした。
そんな一年の締めも、Swiftに関する記事を書いていきたいと思います。

内容

今回は、CoreData に登録したデータの変更記録を JSON 形式で出力するする際に詰まった点についてお話します。
以下の順でお話していきます。

  • オブジェクトをJSON形式にエンコードして出力する
  • CoreDataのEntityモデルをJSON形式で出力する

先に結論

NSManagedObjectCodableではないため、NSManagedObjectを継承したクラスをJSONなどにエンコードする際には、カスタムエンコードロジックの実装が必要になる。

環境

  • macOS Ventura 13.6.1(22G313)
  • Xcode 15.0.1

オブジェクトをJSON形式にエンコードして出力する

Swift で自前で用意したクラスや構造体をエンコード可能にするためには、Encodable に準拠する必要があります。
Encodable に準拠する書き方に関しては、Apple の公式ドキュメント に詳しく記載してあります。
要約すると、Encodableを継承すると良いため、以下のように書けます。

Sample.playground
import Foundation

// Encodable を継承している。
class Item: Encodable {
    var count: Int
    var name: String

    init(count: Int, name: String) {
        self.count = count
        self.name = name
    }
}

let item = Item(count: 5, name: "hoge")

let encoder = JSONEncoder()
let data: Data = try encoder.encode(item)
if let jsonString = String(data: data, encoding: .utf8) {
    print(jsonString)
}

Playground での実行結果は以下です。JSON形式で出力出来ていることが確認できます。

出力結果
{"count":5,"name":"hoge"}

CoreDataのEntityモデルをJSON形式で出力する

先ほどのItemクラスにCoreDataのNSManagedObjectを継承したいと思います。

Item.swift
import Foundation
import CoreData

@objc(Item)
public class Item: NSManagedObject, Identifiable {
    @NSManaged public var count: Int16
    @NSManaged public var name: String
}

以下のように、Encodableを継承しても、JSON形式で出力できません。

Item.swift
import Foundation
import CoreData

@objc(Item)
- public class Item: NSManagedObject, Identifiable {
+ public class Item: NSManagedObject, Identifiable, Encodable {
    @NSManaged public var count: Int16
    @NSManaged public var name: String
}
①オブジェクトをJSONにエンコードして出力する
let encoder = JSONEncoder()
let data: Data = try encoder.encode(item)
if let jsonString = String(data: data, encoding: .utf8) {
    print(jsonString)
}
①の出力結果
{}

NSManagedObjectEncodable を継承する場合、一手間必要でした。
独自にエンコードロジックを用意する必要があります。

CodingKey が、エンコード(又はデコード)に必要なキーを列挙しており、
encode(to encoder: Encoder) が今回自前で用意したエンコード用の関数になります。

Item.swift
import Foundation
import CoreData

@objc(Item)
public class Item: NSManagedObject, Identifiable, Encodable {
    @NSManaged public var count: Int16
    @NSManaged public var name: String
+
+    enum CodingKeys: CodingKey {
+        case count, name
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encode(count, forKey: .count)
+        try container.encode(name, forKey: .name)
+    }
}

これで NSManagedObject クラスのオブジェクトをJSON形式で出力できるようになりました。

①の出力結果
{"name":"Hoge","count":6}

追記: なぜカスタムロジックが必要だったのか

結論、Item クラスに継承したNSManagedObjectクラスが、Codableな型ではなかったからだと考えています。

公式リファレンスに以下の記載があります。

The simplest way to make a type codable is to declare its properties using types that are already Codable. These types include standard library types like String, Int, and Double; and Foundation types like Date, Data, and URL. Any type whose properties are codable automatically conforms to Codable just by declaring that conformance.

CodableEncodable かつ Decodable)にする最も簡単な方法は、既にCodableになっている型を使ってプロパティを宣言することです。」とあります。
NSManagedObjectクラスを継承する前のItemクラスは、Int型とString型のプロパティが存在します。
ここで、公式リファレンスでそれぞれの型の説明について見てみます。

公式リファレンスの Int > Relationships > Conforms To の一覧を見ると確かに、Int が「Encodable かつ Decodable」であることが分かります。

スクリーンショット 2023-12-05 21.05.17.png

String についても同様に、「Encodable かつ Decodable」であることが確認できます。

String > Relationships > Conforms To のスクリーンショット

スクリーンショット 2023-12-05 21.11.36.png

ここで、NSManagedObject クラスについて同様に確認すると、「Encodable かつ Decodableではないことが分かりました。

スクリーンショット 2023-12-05 21.16.46.png

元々のItemクラスは全てのプロパティが「Encodable かつ Decodable」であったため、Encodableを継承するだけでエンコードできた。
NSManagedObjectクラスを継承したItemクラスは、NSManagedObject自体が「Encodable かつ Decodable」ではないため、カスタムエンコードロジックの実装が必要だった。

上記が私が調べた結論です。

検証してみた

CoreData に登録したデータをアプリ上でJSONで出力させてみました。
変更したcountnameがJSON形式で表示されていることが分かると思います。
コードは、動画の下に添付しています。

output.gif

Item.swift
import Foundation
import CoreData

@objc(Item)
public class Item: NSManagedObject, Identifiable, Encodable {
    @NSManaged public var count: Int16
    @NSManaged public var name: String

    enum CodingKeys: CodingKey {
        case count, name
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(count, forKey: .count)
        try container.encode(name, forKey: .name)
    }
}

ContentView.swift
import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>

    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        DetailView(item: item)
                    } label: {
                        HStack {
                            Text(item.count.description)
                            Text(item.name.description)
                        }
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.count = 0
            newItem.name = "Sample"

            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}
DetailView.swift
import SwiftUI
import CoreData
import Foundation

struct DetailView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @State private var count: Int16 = 0
    @State private var name: String = ""
    @State private var jsonString: String = ""
    var item: Item
    var body: some View {
        VStack {
            Button(count.description) {
                count = count + 1
            }
            TextField(item.name, text: $name)
                .border(.black)
                .padding()
                .onAppear {
                    name = item.name
                }
            Button("Print JSON") {
                let encoder = JSONEncoder()
                do {
                    let data: Data = try encoder.encode(item)
                    if let jsonStr = String(data: data, encoding: .utf8) {
                        print(jsonStr)
                        jsonString = jsonStr
                    }
                } catch {}
            }
            Text("JSON = " + jsonString)
        }
        .toolbar {
            ToolbarItem {
                Button(action: saveItem) {
                    Text("Save")
                }
            }
        }
    }

    private func saveItem() {
        withAnimation {
            item.count = count
            item.name = name

            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

さいごに

明日は、 @starswirl_k さんの「【React/TypeScript】今年のクリスマスはreact-konvaで雪を降らす」です。
お楽しみに。

9
1
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
9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?