11

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.

Adar Hefer
  • 1,514
  • 1
  • 12
  • 18
  • I don't have a definitive answer, but a few years ago Apple did something similar to binary stores. There is a key for the persistent store options dictionary to register other acceptable types: `NSBinaryStoreSecureDecodingClasses`. Maybe that will lead you to the answer. – Drew McCormack Aug 01 '19 at 10:15
  • Not really an answer but more an observation: from my experience reverse engineering Apple apps, they actually use proto buffs for storing class objects in coredata when they don’t support secure coding. I don’t think even Apple at this point has a solution for what you want seeing as they just went with a different archive method – Allison Aug 11 '19 at 02:11
  • I'm not sure what to make of this observation to be honest. Keep in mind though, I simply haven't tried making this change until now, but `NSSecureUnarchiveFromDataTransformerName` has been available since iOS 12, and they started telling people to switch to it since WWDC 2018 more than a year ago. – Adar Hefer Aug 11 '19 at 10:33
  • 1
    It appears the transformers are compatible. This is from the apple docs on NSKeyedArchive.archivedData `Enabling secure coding doesn’t change the output format of the archive. This means that you can encode archives with secure coding enabled, and decode them later with secure coding disabled.` – adamfowlerphoto Sep 07 '19 at 06:42
  • Great find Adam! I was pretty sure they wouldn't be compatible, it's reassuring to know this is the case. – Adar Hefer Sep 07 '19 at 13:36

2 Answers2

3

I tried to use NSSecureUnarchiveFromDataTransformer also (although I don't need secure coding, see below), but I did not have success. Thus I used a custom value transformer instead. My steps were:

I implemented my custom value transformer class:

@objc(MyTransformer)
class MyTransformer: ValueTransformer {

    override class func setValueTransformer(_ transformer: ValueTransformer?, forName name: NSValueTransformerName) {
        ValueTransformer.setValueTransformer(transformer, forName: name)
    }

    override func transformedValue(_ value: Any?) -> Any? {
        guard let value = value else { return nil }
        let data = serialize(value) // A custom function, e.g. using an NSKeyedArchiver
        return data as NSData
    }

    override class func allowsReverseTransformation() -> Bool {
        return true
    }

    override func reverseTransformedValue(_ value: Any?) -> Any? {
        guard let value = value else { return nil }
        guard let data = value as? Data else { return nil }
        let set = deserialize(data) // A custom function, e.g. using an NSKeyedUnarchiver
        return set as NSSet // Or as an NSArray, or whatever the app expects
    }
}

extension NSValueTransformerName {
    static let myTransformerName = NSValueTransformerName(rawValue: „MyTransformer")
}  

The 1st line (@objc) is required, see this post! Otherwise coreData does not recognise the custom transformer!

Next, I implemented a computed property in the app delegate, according to this post:

private let transformer: Void = {
    MyTransformer.setValueTransformer(MyTransformer(), forName: .myTransformerName)
}()  

It is important to do this early, e.g. in the app delegate, so that coreData does recognise the transformer when it is initialised.

Eventually, I set in the attribute inspector of the transformable attribute in the xcdatamodeld file the Transformer value to MyTransformer.

Then the code run correctly without run time logs.
Please note: In my case, it was not necessary to do secure coding, but the code above can easily be modified to use secure coding instead. Just modify the functions serialize and deserialize accordingly.

EDIT (due to the comment of kas-kad below):

Sorry, my code was unfortunately not complete.

In the app delegate, I used the following computed property (see this link). This ensures that the value transformer is registered very early, even before init is run.

private let transformer : Void = {
    let myTransformer = MyValueTransformer()
    ValueTransformer.setValueTransformer(myTransformer, forName:NSValueTransformerName("MyValueTransformer"))
}()  

And to override class func setValueTransformer does in my implementation obviously nothing. I copied it from somewhere (cannot remember). So one can surely omit it.

The extension of NSValueTransformerName does nothing more than to allow to use .myTransformerName as the transformer name.

Reinhard Männer
  • 14,022
  • 5
  • 54
  • 116
  • Thanks for sharing. Although I'm wondering, if you've given up on adopting `NSSecureUnarchiveFromDataTransformer` for your transformable properties, and you don't require secure coding, why not explicitly use `NSKeyedUnarchiveFromDataTransformerName` instead? This should silence the error, no? – Adar Hefer Oct 05 '19 at 14:47
  • Probably yes. I didn't try it. I tried to follow Apple's advice to set as transformer `NSSecureUnarchiveFromDataTransformer`, which did not work. In my answer, I showed a way to implement a custom transformer that can be implemented insecure or secure, as required. – Reinhard Männer Oct 05 '19 at 14:52
  • I've come up with the similar solution (preferring using custom insecure transformers compatible with good old default transformations). But my implementation misses `setValueTransformer` overriding, as well as extension part `static let myTransformerName = ` and app delegate part. Everything works fine, the model successfully recognizes the transformers that I specified in the property fields in the attribute inspector. Could you elaborate the need of those changes that I miss? – kas-kad Oct 10 '19 at 13:51
  • @kas-kad Thanks for your comment! My implementation above is unfortunately not complete, and overriding class func setValueTransformer is unnecessary. Please see my edit above. Thanks for letting me know! – Reinhard Männer Oct 11 '19 at 07:04
3

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)
  }
}
Rudedog
  • 4,323
  • 1
  • 23
  • 34
  • Gist here: https://gist.github.com/rudedogdhc/0ae51a8e97a350458386fd914f0f9874 – Rudedog Oct 23 '19 at 18:24
  • Would you happen to know why a transformer would work on simulator without issue but crash on the device as soon as it opened? The only way I get it to run is to empty the transformer field in the attributes and then get the warnings again. They are registered and the entity is new. But when I specify the transformer, the device crashes with "NSLocalizedFailureReason=CloudKit integration requires that the value transformers for transformable attributes are available via +[NSValueTransformer valueTransformerForName:], return instances of NSData, and allow reverse transformation:" – Bill3 Oct 30 '20 at 04:41