🔥

Core DataのNSManagedObjectをDecodableへ

2022/03/07に公開

例えば、Core Dataで「User」というEntityを作り出した時点では
Xcodeのどこかで「User」というクラスも作り出されています
その「User」クラスをそのままDecodable化にして、JSONファイルを直接にデコードできればばと思い、その実践の流れです

まずは画面から始める

デコード用のStruct(仮)を定義

Struct.swift
struct User: Codable {
    let name: String
    let age: Int
}

画面を定義

ContentView.swift
struct ContentView: View {
	@State private var users = [User]()

	let json = """
	[
		{
			"name": "Carry",
			"age": 21
		},
		{
			"name": "Ben",
			"age": 12
		}
	]
	"""
	
	var body: some View {
		List {
			Section {
				Button("Decode Data", action: loadData)
			}
			
			ForEach(users, id: \.name) { user in
				Section(user.name) {
					HStack {
						Text("Age")
						Spacer()
						Text(user.age.formatted())
					}
				}
			}
		}
	}
	
	func loadData() {
		let data = Data(json.utf8)
		do {
			let decoder = JSONDecoder()
			users = try decoder.decode([User].self, from: data)
		} catch {
			print(error.localizedDescription)
		}
	}
}

起動して、問題なく作動できるのを確認

Core DataのData Modelを作成

Struct.swiftを一旦削除します
下記のようにData Modelを作ってみます

そして、NSManagedObject Subclassを作成
以下のファイルが作り出されることを確認

User+CoreDataClass.swift
import Foundation
import CoreData


@objc(User)
public class User: NSManagedObject {
}

NSManagedObjectをDecodableに

まずはDecodableというプロトコルを追加

User+CoreDataClass.swift
+public class User: NSManagedObject, Decodable {
}

CodingKeysを追加

User+CoreDataClass.swift
public class User: NSManagedObject, Decodable {
+	enum CodingKeys: CodingKey {
+		case name, age
+	}
}

マニュアルデコードを追加

User+CoreDataClass.swift
public class User: NSManagedObject, Decodable {
	enum CodingKeys: CodingKey {
		case name, age
	}
	
+	required public convenience init(from decoder: Decoder) throws {
+        
+       guard let context = decoder.userInfo[CodingUserInfoKey(rawValue: "managedObjectContext")!] as? NSManagedObjectContext else { fatalError() }
+        
+       self.init(context: context)
+        
+       let container = try decoder.container(keyedBy: CodingKeys.self)
+       self.name = try container.decode(String.self, forKey: .name)
+       self.age = try container.decode(Int16.self, forKey: .age)
    }
}

Data Controllerを追加

DataController.swiftを新しく作成

DataController.swift
import CoreData
import Foundation

class DataController: ObservableObject {
    let container = NSPersistentContainer(name: "TestProject")
    
    init() {
        container.loadPersistentStores { description, error in
            if let error = error {
                print("Core Data failed to loaded: \(error.localizedDescription)")
                return
            }
        }
    }
}

"TestProject"の部分は、Data Modelのファイル名と同じようにします

TestProjectApp.swift(エントリーファイル)に以下のコードを追加

TestProjectApp.swift
import SwiftUI

@main
struct TestProjectApp: App {
+   @StateObject private var dataController = DataController()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
+               .environment(\.managedObjectContext, dataController.container.viewContext)
        }
    }
}

ロード関数を添削

ContentView.swiftに戻り、managedObjectContextの環境変数を追加
そして、decoderにuserInfoを追加

ContentView.swift
struct ContentView: View {
+	@Environment(\.managedObjectContext) var moc

	var body: Some View {
	// 中間略
	}
	
	func loadData() {
		let data = Data(json.utf8)
		do {
			let decoder = JSONDecoder()
+			decoder.userInfo[CodingUserInfoKey(rawValue: "managedObjectContext")!] = self.moc
			users = try decoder.decode([User].self, from: data)
		} catch {
			print(error.localizedDescription)
		}
	}
}

そしたら完成でした!

Discussion