ioscore-dataios13nssecurecoding

Crash when adopting NSSecureUnarchiveFromDataTransformer for a Transformable property


In iOS 12 Apple introduced NSSecureUnarchiveFromDataTransformerName for use on CoreData model entities' Transformable properties. I used to keep the Transformer Name field empty, which implicitly used NSKeyedUnarchiveFromDataTransformerName. This transformer is now deprecated, and keeping the field empty in the future will mean NSSecureUnarchiveFromDataTransformerName instead.

In iOS 13, if that field is empty, you now get a runtime warning telling you the aforementioned. I couldn't find any documentation on this anywhere, the only reference I got was a WWDC 2018 Core Data Best Practices talk which briefly mentioned what I just said.

Now I have a model with an entity which directly stores HTTPURLResponse objects in a Transformable property. It conforms to NSSecureCoding, and I checked in runtime that supportsSecureCoding is true.

Setting NSSecureUnarchiveFromDataTransformerName for the Transformer Name crashes with this message:

Object of class NSHTTPURLResponse is not among allowed top level class list (
    NSArray,
    NSDictionary,
    NSSet,
    NSString,
    NSNumber,
    NSDate,
    NSData,
    NSURL,
    NSUUID,
    NSNull
) with userInfo of (null)

So it sounds like Transformable properties can only be of these top level objects.

I tried subclassing the secure transformer and override the allowedTopLevelClasses property as suggested by the documentation:

@available(iOS 12.0, *)
public class NSSecureUnarchiveHTTPURLResponseFromDataTransformer: NSSecureUnarchiveFromDataTransformer {

    override public class var allowedTopLevelClasses: [AnyClass] {
        return [HTTPURLResponse.self]
    }
}

Then I'd imagine I can create a custom transformer name, set it in the model and call setValueTransformer(_:forName:) for that name, but I couldn't find API to set the default NSKeyedUnarchiveFromDataTransformer for my custom name in case I'm on iOS 11.

Keep in mind, I'm using Xcode 11 Beta 5, but this doesn't seem to be related if I am to accept the meaning of the error I'm getting as stated.

Appreciate any thoughts.


Solution

  • I wrote a simple template class which makes it easy to create and register a transformer for any class that implements NSSecureCoding. It works fine for me in iOS 12 and 13, at least in my simple test using UIColor as the transformable attribute.

    To use it (using UIColor as an example):

    // Make UIColor adopt ValueTransforming
    extension UIColor: ValueTransforming {
      static var valueTransformerName: NSValueTransformerName { 
        .init("UIColorValueTransformer")
      }
    }
    
    // Register the transformer somewhere early in app startup.
    NSSecureCodingValueTransformer<UIColor>.registerTransformer()
    

    The name of the transformer to use in the Core Data model is UIColorValueTransformer.

    import Foundation
    
    public protocol ValueTransforming: NSSecureCoding {
      static var valueTransformerName: NSValueTransformerName { get }
    }
    
    public class NSSecureCodingValueTransformer<T: NSSecureCoding & NSObject>: ValueTransformer {
      public override class func transformedValueClass() -> AnyClass { T.self }
      public override class func allowsReverseTransformation() -> Bool { true }
    
      public override func transformedValue(_ value: Any?) -> Any? {
        guard let value = value as? T else { return nil }
        return try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true)
      }
    
      public override func reverseTransformedValue(_ value: Any?) -> Any? {
        guard let data = value as? NSData else { return nil }
        let result = try? NSKeyedUnarchiver.unarchivedObject(
          ofClass: T.self,
          from: data as Data
        )
        return result
      }
    
      /// Registers the transformer by calling `ValueTransformer.setValueTransformer(_:forName:)`.
      public static func registerTransformer() {
        let transformer = NSSecureCodingValueTransformer<T>()
        ValueTransformer.setValueTransformer(transformer, forName: T.valueTransformerName)
      }
    }