为 Transformable 采用 NSSecureUnarchiveFromDataTransformer 时发生崩溃 属性

Crash when adopting NSSecureUnarchiveFromDataTransformer for a Transformable property

在 iOS 12 中,Apple 引入了 NSSecureUnarchiveFromDataTransformerName 用于 CoreData 模型实体的 Transformable 属性。我曾经将 Transformer Name 字段保留为空,这隐含地使用了 NSKeyedUnarchiveFromDataTransformerName。此转换器现已弃用,将来将该字段留空将意味着 NSSecureUnarchiveFromDataTransformerName

在 iOS 13 中,如果该字段为空,您现在会收到一个运行时警告,告诉您上述情况。我在任何地方都找不到这方面的任何文档,我得到的唯一参考是 WWDC 2018 核心数据最佳实践演讲,其中简要提到了我刚才所说的内容。

现在我有一个带有实体的模型,它直接将 HTTPURLResponse 个对象存储在可变形 属性 中。它符合NSSecureCoding,我在运行时检查supportsSecureCodingtrue

为转换器名称设置 NSSecureUnarchiveFromDataTransformerName 时崩溃并显示此消息:

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)

所以听起来 Transformable 属性只能属于这些顶级对象。

我尝试将安全转换器子类化并按照文档的建议覆盖 allowedTopLevelClasses 属性:

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

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

然后我想我可以创建一个自定义转换器名称,在模型中设置它并为该名称调用 setValueTransformer(_:forName:),但我找不到 API 来设置默认值 NSKeyedUnarchiveFromDataTransformer 作为我的自定义名称,以防我在 iOS 11.

请记住,我使用的是 Xcode 11 Beta 5,但如果我要接受所陈述的错误的含义,这似乎并不相关。

感谢任何想法。

我也尝试使用 NSSecureUnarchiveFromDataTransformer(虽然我不需要安全编码,见下文),但我没有成功。因此,我改为使用自定义值转换器。我的步骤是:

我实现了我的自定义值转换器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")
}  

第一行(@objc)为必填项,见!否则 coreData 无法识别自定义转换器!

接下来,我根据 在应用程序委托中实现了计算 属性:

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

尽早执行此操作很重要,例如在应用程序委托中,以便 coreData 在初始化时识别转换器。

最终,我在 xcdatamodeld 文件中的可转换属性的属性检查器中将 Transformer 值设置为 MyTransformer

然后代码 运行 正确无 运行 时间日志。
请注意:在我的例子中,没有必要进行安全编码,但上面的代码可以很容易地修改为使用安全编码。只需相应地修改函数 serializedeserialize

编辑(由于下面 kas-kad 的评论):

抱歉,很遗憾,我的代码不完整。

在应用程序委托中,我使用了以下计算的 属性(参见 )。这确保值转换器很早就被注册,甚至在 init 是 运行 之前。

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

override class func setValueTransformer 在我的实现中显然没有任何作用。我从某个地方复制的(不记得了)。所以完全可以省略。

NSValueTransformerName 的扩展只是允许使用 .myTransformerName 作为转换器名称。

我写了一个简单的模板 class,它可以很容易地为实现 NSSecureCoding 的任何 class 创建和注册转换器。它在 iOS 12 和 13 中对我来说工作正常,至少在我使用 UIColor 作为可转换属性的简单测试中是这样。

使用方法(以UIColor为例):

// Make UIColor adopt ValueTransforming
extension UIColor: ValueTransforming {
  static var valueTransformerName: NSValueTransformerName { 
    .init("UIColorValueTransformer")
  }
}

// Register the transformer somewhere early in app startup.
NSSecureCodingValueTransformer<UIColor>.registerTransformer()

要在 Core Data 模型中使用的转换器的名称是 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)
  }
}