ARAnchor subclass causing issues when worldmap saved/restored

I'm sucessfully saving and restoring world maps, however, if I place an anchor that is a subclass of ARAnchor it appears to either crash during saving, or failed to load due to "Invalid configuration."
Just trying to determine if subclassing and archiving ARAnchor is intended to be possible. In the meantime I'll be looking for a work around.

Replies

Just watched the SwiftShot WWDC video and realize they did exactly this (https://developer.apple.com/videos/play/wwdc2018/605 at 17mins). I'll have to investigate more on my side why this was happening, but I didn't have much trouble when I switched back to using normal ARAnchors with unique identifiers as names.

Did you find a solution to this problem?


I've just encountered the same issue, where a subclass of ARAnchor (even if it is an entirely empty subclass, differing from ARAnchor in name only) causes the secure NSKeyedArchiver to fail when trying to turn the ARWorldMap into archived Data.


I've also discovered that turning my custom anchors back into vanilla ARAnchor objects allows the archiver to generate the archive successfully.


It seems like a significant limitation to have all archived objects distinguished solely by a textual name, but I guess this avoids all sorts of potential problems with mapping between versions of (potentially incompatible) ARAnchor subclasses.

Here's a follow-up on this topic. It seems that subclasses of ARAnchor can be archived using the secure NSKeyedArchiver if they provide explicit (not implicit) conformance to NSSecureCoding.


So something like the following won't work:


class CustomARAnchor : ARAnchor {

}


When you try to archive a ARWorldMap containing one or more CustomARAnchor objects (using NSKeyedArchiver.archivedData(withRootObject: worldMap, requiringSecureCoding: true)), the archiver throws a "Data is in the wrong format" error, and fails.


But the following does work, and a WorldMap containing anchors as defined below are successfully archived:


class CustomARAnchor : ARAnchor {


required init(anchor: ARAnchor) {

super.init(anchor: anchor)

}


override init(transform: simd_float4x4) {

super.init(transform: transform)

}


override class var supportsSecureCoding: Bool {

return true

}


required init?(coder aDecoder: NSCoder) {

super.init(coder: aDecoder)

}


override func encode(with aCoder: NSCoder) {

super.encode(with: aCoder)

}


}


Note: Swift will always insists on the first constructor; my App was using the transform initializer for the subclass and Swift then required that the subclass also provide an override for this constructor.

One final P.S. to this thread.


Anyone subclassing ARAnchor needs also to be aware that ARKit clones these objects at 60Hz (every time an ARFrame is generated). When generating an ARWorldMap, it's these "clones" of your ARAnchor objects that you will receive in the ARWorldMap's .anchors list.


For that reason, you need to ensure that your ARAnchor subclass conforms to the following rule (from https://developer.apple.com/documentation/arkit/aranchorcopying/3020708-init):


/*

Each time ARKit generates a new ARFrame object (corresponding to an incoming frame of live camera video at 60 fps), ARKit calls this initializer to copy each of the anchors associated with the previous frame.

Note: ARKit always calls this initializer with an anchor parameter of the same class as self.

If you subclass ARAnchor to add extra properties, your implementation of this initializer should copy the values of those properties, then chain to the superclass initializer. For example, an AR game might define a BoardAnchor class to encode the position and size of a game board, so its version of this initializer would copy that size property:

required init(anchor: ARAnchor) {

let other = anchor as! BoardAnchor

self.size = other.size

super.init(anchor: other)

}

*/


If your ARAnchor subclass contains optional properties, or properties with default values, it's easy to forget to clone these as well, in which case they are "lost" (reset to nil, or their explicit default values). If this happens, your ARAnchor subclasses won't be clones of the anchors you added to the ARSession at all, and you'll likely find this confusing when you try to reload and re-localize an ARSession using the archived ARWorldMap.

Hi can you add an example of an actuall subclasses ARAnchor with with custom properties and the encodable / decodable methods

Hi -

As others have mentioned, you need to properly define how your custom properties will be encoded and decoded. Not doing so will crash the app when trying to archive the data as you've described. Here is a quick example of a custom ARAnchor that can successfully be archived and unarchived in your saved world data:

class CustomAnchor: ARAnchor {
   
  // Your added property
  var customProperty: String
   
  // Init + your custom property
  init(name: String, transform: simd_float4x4, customProperty: String) {
    self.customProperty = customProperty
    super.init(name: name, transform: transform)
  }
   
  override class var supportsSecureCoding: Bool {
    return true
  }
   
  required init?(coder aDecoder: NSCoder) {
    // Try to decode and initialize your custom value based on the key you set in your encoder
    if let customProperty = aDecoder.decodeObject(forKey: "customProperty") as? String {
      self.customProperty = customProperty
    } else {
      return nil
    }
     
    super.init(coder: aDecoder)
  }
   
  // As others have mentioned - this is required to maintain your custom properties as ARKit refreshes
  required init(anchor: ARAnchor) {
    let other = anchor as! CustomAnchor
    self.customProperty = other.customProperty
    super.init(anchor: anchor)
  }
   
  // Encode your custom property using a key to be decoded
  override func encode(with aCoder: NSCoder) {
    super.encode(with: aCoder)
    aCoder.encode(customProperty, forKey: "customProperty")
  }
   
}

This code is a slimmed down version of one of Apple's examples creating a custom anchor to store "snapshot" data in their persistence demo app: https://developer.apple.com/documentation/arkit/data_management/saving_and_loading_world_data

Hello!

maybe I come late to this thread but I have a problem and I couldn't find a solution yet. I have this ARAnchor subclass and I think it has all it needs to be saved (I splitted SIMD3 in three FLoat variables because I thought it would give me less problems to save).


import ARKit
import RealityKit

class CustomARAnchor: ARAnchor {
   
  var modelScalex: Float
  var modelScaley: Float
  var modelScalez: Float
   

  init(name: String, transform: float4x4, modelScale: SIMD3<Float>) {
    self.modelScalex = modelScale.x
    self.modelScaley = modelScale.y
    self.modelScalez = modelScale.z
    super.init(name: name, transform: transform)
  }


  required init(anchor: ARAnchor) {
    let other = anchor as! CustomARAnchor
    self.modelScalex = other.modelScalex
    self.modelScaley = other.modelScaley
    self.modelScalez = other.modelScalez
    super.init(anchor: other)
  }

  override class var supportsSecureCoding: Bool {
    return true
  }


  required init?(coder aDecoder: NSCoder) {
    if let modelScalex = aDecoder.decodeObject(forKey: "modelScalex") as? Float, let modelScaley = aDecoder.decodeObject(forKey: "modelScaley") as? Float, let modelScalez = aDecoder.decodeObject(forKey: "modelScalez") as? Float {
      self.modelScalex = modelScalex
      self.modelScaley = modelScaley
      self.modelScalez = modelScalez
    } else {
      return nil
    }
    super.init(coder: aDecoder)
  }



  override func encode(with aCoder: NSCoder) {
    super.encode(with: aCoder)
    aCoder.encode(self.modelScalex, forKey: "modelScalex")
    aCoder.encode(self.modelScaley, forKey: "modelScaley")
    aCoder.encode(self.modelScalez, forKey: "modelScalez")
  }
   
}

In fact it seems it's correctly saved as this code inside the getWorldMap function prints me the names of my custom anchors

self.arView.session.getCurrentWorldMap { worldMap, _ in
      guard let map = worldMap else {
        print("Can't get current world map")
        return
      }
       
      for anchor in map.anchors {
        if let anchor = anchor as? CustomARAnchor{
          if let name = anchor.name{
            print("custom anchor " + name + " in map")
          }
        }
      }
       
      do {
        let data = try NSKeyedArchiver.archivedData(withRootObject: map, requiringSecureCoding: true)
        try data.write(to: URL(fileURLWithPath: self.worldMapFilePath), options: [.atomic])
      } catch {
        fatalError("Can't save map: \(error.localizedDescription)")
      }
    }

I save the world map in a file and then load it in another application. The Problem is that when i load the map the custom anchors won't be recognized or even loaded because the func session(_, didAdd anchors) adds just one anchor (i guess its a plane recognized) and not my custom anchors.

func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
    print("did add anchor: \(anchors.count) anchors in total")
       
    for anchor in anchors {
       
      if let name = anchor.name {
        print("DEBUG: anchor" + name)
      }
       
      if let _ = anchor as? CustomARAnchor {
        print("DEBUG: found custom anchor")
      }
      addAnchorEntityToScene(anchor: anchor)
    }
  }

All the code works fine if i use normal ARAnchors and when I use CustomARAnchors it works fluently (no crashes or errors) but it stops doing what it is supposed to do. If anybody can have an idea of where the problem is I would be very grateful. Thank in advance even for reading this!