Search code examples
iosswiftcore-data

How to implement Core Data Transformable type for [[Int?]] and [[String?]]


For my question I have prepared a simple SwiftUI project at GitHub.

A backend sends the following JSON data to my app, representing a game with a 15 x 15 letters:

{
    "gid":266,
    "letters":[
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,"H", null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,"U", null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,"E", null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]
    ],
    "values":[
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null, 4,  null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null, 1,  null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null, 1,  null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],
        [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]
    ],
    "tiles":[
        {"col": 8, "row": 7, "value": 1, "letter": "E"},
        {"col": 7, "row": 7, "value": 1, "letter": "U"},
        {"col": 6, "row": 7, "value": 4, "letter": "H"}
    ]
}

As you can see the letters and values are 2-dimensional arrays of String? and Int? and that is how I have defined them in the GameModel.swift which I use for JSON parsing:

struct GameModel: Codable, Identifiable {
    var id: Int32 { gid }
    let gid: Int32
    let letters: [[String?]]
    let values: [[Int32?]]
    let tiles: [TileModel]? // the previous move as an array

    // create a new Core Data entity and copy the properties
    func toEntity(viewContext: NSManagedObjectContext) -> GameEntity {
        let gameEntity = GameEntity(context: viewContext)
        gameEntity.gid = self.gid
        gameEntity.letters = self.letters
        gameEntity.values = self.values
        gameEntity.tiles = self.tiles
        return gameEntity
    }
}

struct TileModel: Codable {
    let col: Int
    let row: Int
    let value: Int
    let letter: String
}

I am trying to parse them by using the Transformable Core Data type and thus I have added these 3 lines to the Persistence.swift

let container: NSPersistentContainer

init(inMemory: Bool = false) {
    ValueTransformer.setValueTransformer(ValuesToDataTransformer(), forName: .valuesToDataTransformer)
    ValueTransformer.setValueTransformer(LettersToDataTransformer(), forName: .lettersToDataTransformer)
    ValueTransformer.setValueTransformer(TilesToDataTransformer(), forName: .tilesToDataTransformer)

    container = NSPersistentContainer(name: "TransApp")

Also I have added the 3 files:

My problem is that my custom ValueTransformer sub classes do not compile.

The error is:

Static method 'unarchivedObject(ofClass:from:)' requires that '[[Int32?]]' conform to 'NSCoding'

and similar for the 2 others.


Solution

  • This is a completely different approach avoiding Transformable. It saves only the TileModel array as JSON string and uses computed properties to create the grids and to convert the custom type to and from JSON.

    Actually you don't need to decode the grids at all.

    • In the entity declare tiles as String

      @NSManaged public var tiles: String?
      
    • Add a computed property to convert an array of TileModel to JSON

      var tileArray: [TileModel]? {
          get {
              guard let data = tiles?.data(using: .utf8) else { return nil }
              return try? JSONDecoder().decode([TileModel].self, from: data)
          }
          set {
              guard let data = try? JSONEncoder().encode(newValue) else { tiles = nil; return }
              tiles = String(data: data, encoding: .utf8)
          }
      }
      
    • And the computed properties to create the grids, there is no reason to save them in the entity

      var values : [[Int?]] {
            var grid : [[Int?]] = [
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil]]
            tileArray?.forEach{ grid[$0.col][$0.row] = $0.value }
            return grid
        }
      
      var letters : [[String?]] {
            var grid : [[String?]] = [
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil],
                [nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,nil]]
            tileArray?.forEach{ grid[$0.col][$0.row] = $0.letter }
            return grid
        }