はじめに
こんにちは。HIGの清水です。
本記事は、フューチャー Advent Calendar 2023の5日目の記事です。
昨日は、 @shibukawa さんの「HTTPでも双方向ソケット通信にチャレンジしてみた(けどやめた)」でした。
今年は社内でも珍しく、iOSアプリ開発のためにSwiftを読んだり書いたりする機会が多かった一年でした。
そんな一年の締めも、Swiftに関する記事を書いていきたいと思います。
内容
今回は、CoreData に登録したデータの変更記録を JSON 形式で出力するする際に詰まった点についてお話します。
以下の順でお話していきます。
- オブジェクトをJSON形式にエンコードして出力する
- CoreDataのEntityモデルをJSON形式で出力する
先に結論
NSManagedObject は Codableではないため、NSManagedObjectを継承したクラスをJSONなどにエンコードする際には、カスタムエンコードロジックの実装が必要になる。
環境
- macOS Ventura 13.6.1(22G313)
- Xcode 15.0.1
オブジェクトをJSON形式にエンコードして出力する
Swift で自前で用意したクラスや構造体をエンコード可能にするためには、Encodable に準拠する必要があります。
Encodable に準拠する書き方に関しては、Apple の公式ドキュメント に詳しく記載してあります。
要約すると、Encodableを継承すると良いため、以下のように書けます。
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を継承したいと思います。
import Foundation
import CoreData
@objc(Item)
public class Item: NSManagedObject, Identifiable {
@NSManaged public var count: Int16
@NSManaged public var name: String
}
以下のように、Encodableを継承しても、JSON形式で出力できません。
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
}
let encoder = JSONEncoder()
let data: Data = try encoder.encode(item)
if let jsonString = String(data: data, encoding: .utf8) {
print(jsonString)
}
{}
NSManagedObject に Encodable を継承する場合、一手間必要でした。
独自にエンコードロジックを用意する必要があります。
CodingKey が、エンコード(又はデコード)に必要なキーを列挙しており、
encode(to encoder: Encoder) が今回自前で用意したエンコード用の関数になります。
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.
「Codable(Encodable かつ Decodable)にする最も簡単な方法は、既にCodableになっている型を使ってプロパティを宣言することです。」とあります。
NSManagedObjectクラスを継承する前のItemクラスは、Int型とString型のプロパティが存在します。
ここで、公式リファレンスでそれぞれの型の説明について見てみます。
公式リファレンスの Int > Relationships > Conforms To の一覧を見ると確かに、Int が「Encodable かつ Decodable」であることが分かります。
String についても同様に、「Encodable かつ Decodable」であることが確認できます。
ここで、NSManagedObject クラスについて同様に確認すると、「Encodable かつ Decodable」ではないことが分かりました。
元々のItemクラスは全てのプロパティが「Encodable かつ Decodable」であったため、Encodableを継承するだけでエンコードできた。
NSManagedObjectクラスを継承したItemクラスは、NSManagedObject自体が「Encodable かつ Decodable」ではないため、カスタムエンコードロジックの実装が必要だった。
上記が私が調べた結論です。
検証してみた
CoreData に登録したデータをアプリ上でJSONで出力させてみました。
変更したcountとnameがJSON形式で表示されていることが分かると思います。
コードは、動画の下に添付しています。
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で雪を降らす」です。
お楽しみに。



