Encoding/Decoding Question

I have a model for a crossword, with 3 classes:

// a Clue knows the Squares where its answer appears
class Clue: Codable {
     var squares: [Square]
}

// a Square knows what clue it's a part of (both across and down)
class Square: Codable {
    var acrossClue: Clue
    var downClue: Clue
}

// a Puzzle, with a 2D array of Squares, and an array of Clues
class Puzzle: Codable {
    var solverGrid: [[Square]]
    var clues: [Clue]
}

When I try and save() a Puzzle:

 func save(){
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        do {
            let puzzleData = try encoder.encode(puzzle)
            print(String(data: puzzleData!, encoding: .utf8)!)
            
        } catch {
            print(error)
        }
    }

I get a BAD_ACCESS -- the catch block doesn't catch anything. If I use CodingKeys to limit what gets encoded, it seems that the problem is with my circular reference - a Square contains a Clue, and a Clue contains a Square. Is that not permitted in Swift?

I suspect that circular relation is stackoverflowing the program. Clue is trying to encode Square, which is trying to encode Clue.

On a separated note, it’s weird that you’re doing puzzleData! since it’s suppose to be non-optional.

Thanks for your response, Lantua. You're right, my actual code is slightly different (see below).

I did find one article that seemed to address my problem of circular references, although it's old -- Swift 4. If it's still relevant, then maybe I can do this with NSKeyedArchivers??

    var puzzleData: Data?
    @objc func save(){
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        do {
            puzzleData = try encoder.encode(puzzle)
            print(String(data: puzzleData!, encoding: .utf8)!)
            
        } catch {
            print(error)
        }

    }

AFAICT, it’s still relevant. You’ll be losing the convenience when operating with other part of the codebase, though, since you’ll be using NSCoder instead of Codable. IME, it’s quite annoying when you want to wrap Puzzle inside another Codable object.

Another option is to write your own Codable on Puzzle. Your data structure seems to follow a strict pattern of having two-way relationship. So maybe you can encode/decode only Puzzle.solverGrid, then figure out Puzzle.clues afterward. Of course, you’ll need to only encode/decode an identifier for each Clue so it does get a little tricky.

For more information about custom Codable, see Encoding and Decoding Custom Types.

Also, this WWDC Data You can Trust may prove useful when designing your own coding scheme.

1 Like

Thanks again, Lantua. I think I have a plan ...since Puzzle contains var clues: [Clue], I'll just have Square store indices to clues, and omit acrossClue and downClue from CodingKeys. I know it's inefficient, but if it works I'll be good...

class Square: Codable {
    var acrossClue: Clue
    var downClue: Clue
    var acrossClueIndex: Int
    var downClueIndex: Int
}

Hmm, that’d be much easier than what I have had in mind. Given that Square and Clue don’t do the coding by itself anymore, it doesn’t really make sense to conform them to Codable. Now, since we do the coding in Puzzle itself, we can just precompute the identifier. No need for extra information in Square. Maybe something like this:

extension Puzzle {
...
func encode(...) {
  var identifiers: [ObjectIdentifier: Int] = [:]
  for (id, clue) in clues.enumerated() {
    identifiers[.init(clue)] = id
  }

  // Now we can use identifiers.

  for row in solverGrid {
    let rowContainer = encoder.unkeyedContainer()

    for square in row {
      let container = rowContainer.nestedContainer(keyedBy: ...)

      // The subscript would fail if cluse are missing
      let acrossID = identifiers[.init(square.across)]!
      container.encode(acrossID, forKey: ...)
      container.encode...
    }
  }
}

And we can do the decoding in a similar manner.