100DaysOfSwiftUI Day61のChallengeをやってみる(CoreData対応)

なんとなくDay61を見てみたら、CoreData対応でした。

ちょうど、CoreData対応と非対応のプロジェクトの設定を見比べたところだったので、続けてやっていこうかと

非CoreData設定のプロジェクトを、CoreData対応する方法

変更するファイルは、2つです。

AppDelegate.swift
以下のコードを追加することで、NSPersistentContainerを使えるようになります。

lazy var persistentContainer: NSPersistentContainer = {
    /*
     The persistent container for the application. This implementation
     creates and returns a container, having loaded the store for the
     application to it. This property is optional since there are legitimate
     error conditions that could cause the creation of the store to fail.
    */
    let container = NSPersistentContainer(name: "NewCoreData")  // <- この名称を後で作るCoreDataのDataModelファイル名に変更すること
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            // 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.
             
            /*
             Typical reasons for an error here include:
             * The parent directory does not exist, cannot be created, or disallows writing.
             * The persistent store is not accessible, due to permissions or data protection when the device is locked.
             * The device is out of space.
             * The store could not be migrated to the current model version.
             Check the error message to determine what the actual problem was.
             */
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()
SceneDelegate.swift
ContentViewが生成される前に、environmentObjectとして、ManagedObjectContextを設定しておくコードです。

let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let contentView = ContentView().environment(\.managedObjectContext, context)
"DataModel"ファイル
新規ファイルとして、"Data Model"ファイルを追加します。

これで、CoreDataを使うをチェックして作ったプロジェクトと同様の状態になります。
クラスとしては、手で作ったクラスと同等の、UserList, User, Friend, Tagを作りました。

JSON経由で読み込んだオブジェクトの、NSManagedObjectへの変換

当初は、自分でClassを作っていましたが、同様のモデルをCoreData上に作ります。

SwiftUIで使うことになると思うので、コード生成したUserListに以下の追加を行います。

public var userArray: [User] {
    let set = users as? Set ?? []
    return set.sorted {
        $0.id < $1.id
    }
}

CoreDataの中では、To Manyな関係は、NSSetで表現されているので、そのままでは、SwiftUIの中でパースするのが難しくなります。
このコードを追加しておくことで、List等で使いやすくなります。

Encode/Decodeが難関

NSManagedObjectを継承したクラスを、Codableにする必要があります。文字で書くと簡単ですが、難しかったです。

NSManagedObjectを継承したクラスのinitializerには、context等が必要となります。CodableをConformする時に必要となるinitializerは、”required init(from decoder: Decoder) throws”であり、contextを引数として渡す余地がありません。

Googleし続けた結果、どうやら、decoderには、userInfoというDictionaryを付与できるので、そこにcontextを渡すことができました。

渡す方(UserListを生成した後この関数を呼んでWebから読み込ませてます)

public func loadFromWeb(urlString:String, context: NSManagedObjectContext) {
    guard let url = URL(string: urlString) else { return }
    let request = URLRequest(url: url)
    URLSession.shared.dataTask(with: request) { (data, response, error) in
        guard let data = data else {
            print("failed to get data from web")
            return
        }
        let decoder = JSONDecoder()
        decoder.userInfo[CodingUserInfoKey.context!] = context
        decoder.dateDecodingStrategy = .iso8601
        if let decodedData = try? decoder.decode(UserList.self, from: data) {
            DispatchQueue.main.async {
                for user in decodedData.userArray {
                    self.addToUsers(user)
                }
            }
        }
    }.resume()
}

受け取る方(UserListのDecodable対応のためのinit)

public required convenience init(from decoder: Decoder) throws {
    guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }
    guard let entity = NSEntityDescription.entity(forEntityName: "UserList", in: context) else { fatalError() }
    self.init(entity: entity, insertInto: context)

	....
}

NSManagedObjectを継承したクラスのDecodable対応

public required convenience init(from decoder: Decoder) throws {
    guard let context = decoder.userInfo[CodingUserInfoKey.context!] as? NSManagedObjectContext else { fatalError() }
    guard let entity = NSEntityDescription.entity(forEntityName: "UserList", in: context) else { fatalError() }
    self.init(entity: entity, insertInto: context)

    var container = try decoder.unkeyedContainer()
    while !container.isAtEnd {
        let user = try container.decode(User.self)
        self.addToUsers(user)
    }
}

どうやってDecodeするかは、JSONや構造依存ですが、initializerを定義することで、Decodableにできます。
関係のあるクラスは、数珠つなぎに呼ばれると思いますので、関係するクラスすべてをDecodable対応する必要があります。

Decodable(/Encodable)とCoreData(NSManagedObjectを継承したクラス)を混ぜることがきました

結構長くなっていますが、上記のような形で、CoreDataで管理されながらCodableにConformすることができました。

  • DecoderのuserInfo経由で、NSManagedObjectContextを受け渡す
  • Codableにするためには、NSManagedObjectを継承するクラスをコード生成しないとできない
便利プロパティその1
@NSManaged public var name: String?

CoreDataでは、すべてOptionalに定義されています。SwiftUIと組み合わせて使おうとすると、常にOptionalを外す作業が必要となります。それを避けるために、以下のようなプロパティを作っておくと、非常に作業が捗りますし、コードが読みやすくなるかと思います。

public var wrappedName: String {
    name ?? "no name"
}
便利プロパティその2
@NSManaged public var users: NSSet?

To Multiなrelationを作成すると上記のようなコードが生成されるのですが、SwiftUIと接続を予定している場合には、下記のようなSwiftUIからアクセスしやすくなるようなプロパティを作っておくと非常に便利です。

public var userArray: [User] {
    let set = users as? Set ?? []
    return set.sorted {
        $0.wrappedId < $1.wrappedId
    }
}

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です