Search code examples
swiftnscoderencodableroomplan

How to save a CapturedRoom using NSCoder


I'm trying to build an app that creates a floor plan of a room. I used ARWorldMap with ARPlaneAnchors for this but I recently discovered the Beta version of the RoomPlan API, which seems to lead to far better results.

However, I used te be able to just save an ARWorldMap using the NSCoding protocol, but this throws an error when I try to encode a CapturedRoom object: -[__SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x141c18110

My code for encoding the class containing the CapturedRoom:

import RoomPlan

class RoomPlanScan: NSObject, NSCoding {
    
    var capturedRoom: CapturedRoom
    var title: String
    var notes: String
    
    init(capturedRoom: CapturedRoom, title: String, notes: String) {
        self.capturedRoom = capturedRoom
        self.title = title
        self.notes = notes
    }
    
    required convenience init?(coder: NSCoder) {
        guard let capturedRoom = coder.decodeObject(forKey: "capturedRoom") as? CapturedRoom,
              let title = coder.decodeObject(forKey: "title") as? String,
              let notes = coder.decodeObject(forKey: "notes") as? String
        else { return nil }
        
        self.init(
            capturedRoom: capturedRoom,
            title: title,
            notes: notes
        )
    }
    
    func encode(with coder: NSCoder) {
        coder.encode(capturedRoom, forKey: "capturedRoom")
        coder.encode(title, forKey: "title")
        coder.encode(notes, forKey: "notes")
    }
    
}

To be clear, the following code does work:

import RoomPlan

class RoomPlanScan: NSObject, NSCoding {
    
    var worldMap: ARWorldMap
    var title: String
    var notes: String
    
    init(worldMap: ARWorldMap, title: String, notes: String) {
        self.worldMap = worldMap
        self.title = title
        self.notes = notes
    }
    
    required convenience init?(coder: NSCoder) {
        guard let capturedRoom = coder.decodeObject(forKey: "worldMap") as? ARWorldMap,
              let title = coder.decodeObject(forKey: "title") as? String,
              let notes = coder.decodeObject(forKey: "notes") as? String
        else { return nil }
        
        self.init(
            worldMap: worldMap,
            title: title,
            notes: notes
        )
    }
    
    func encode(with coder: NSCoder) {
        coder.encode(worldMap, forKey: "worldMap")
        coder.encode(title, forKey: "title")
        coder.encode(notes, forKey: "notes")
    }
    
}

I'm writing the object to a local file using NSKeyedArchiver so it would be nice if I could keep the same structure using NSCoder. How can I fix this and save a CapturedRoom?


Solution

  • The issue is about saving CaptureRoom. According to the doc, it's not NS(Secure)Coding compliant, but it conforms to Decodable, Encodable, and Sendable

    So you can use an Encoder/Decoder, to do CaptureRoom <-> Data, you could use the bridge NSData/Data, since NSData is NS(Secure)Coding compliant.

    So, it could be something like the following code. I'll use JSONEncoder/JSONDecoder as partial Encoder/Decoder because they are quite common.

    • Encoding:
    let capturedRoomData = try! JSONEncoder().encode(capturedRoom) as NSData
    coder.encode(capturedRoomData, forKey: "capturedRoom")
    
    • Decoding:
    let captureRoomData = coder.decodeObject(forKey: "capturedRoom") as! Data
    let captureRoom = try! JSONDecoder().decode(CaptureRoom.self, data: captureRoomData)
    

    Side note:
    I used force unwrap (use of !) to simplify the code logic, but of course, you can use do/try/catch, guard let, if let, etc.)