10

I store colours in my binary Core Data store using a transformable attribute, specifying the class of the attribute as UIColor like so:

#import "CoreDataEntity+CoreDataClass.h"
#import <UIKit/UIKit.h>


NS_ASSUME_NONNULL_BEGIN

@interface CoreDataEntity (CoreDataProperties)

+ (NSFetchRequest<CoreDataEntity *> *)fetchRequest;

@property (nullable, nonatomic, retain) UIColor *transformable;
@property (nullable, nonatomic, copy)   NSString *string;

@end

NS_ASSUME_NONNULL_END

In the iOS 11 Beta this has stopped working with an error like this :

NSUnderlyingException=value for key 'NS.objects' was of unexpected class 'UIColor'. Allowed classes are '{(\n    NSDecimalNumber,\n    NSData,\n    NSUUID,\n    NSNumber,\n    NSDate,\n    NSArray,\n    NSOrderedSet,\n    NSDictionaryMapNode,\n    NSString,\n    NSSet,\n    NSDictionary,\n    NSURL,\n    NSNull\n)}'.}";
    NSUnderlyingException = "Can't read binary data from file";
}

I managed to replicate the specific problem in an XCode project on GitHub (Must be run with the XCode Beta twice to get the error).

In the demo project the store type is controlled by NSPersistentStoreDescription and setting it to NSBinaryStoreType, which I do in the AppDelegate in the exanple project, and I add objects in application didFinishLaunchingWithOptions, otherwise it's the standard template from an iOS11 app with core data. Plus a small datamodel and classes.

If you run the project twice, the first time it creates the datastore and everything is fine. The second time, the datastore tries to open and crashes the app. This problem only seems to be related to binary datastores from what I can tell, if I use an SQL backed datastore it works. However, my app is in the wild and uses binary.

I've reported it to Apple as a bug and sought help on the developer forums, but Apple has not acknowledged the bug and no help was coming.

I'm getting a bit worried as the iOS11 release date draws nearer and I have no solution, my app just won't work in iOS11.

I've tried changing the property to NSData and seeing if it was possible to just unarchive the data, but it seems it's still stored internally as a UIColor somehow and the database just won't open.

Can anyone see a workaround? I have the app in the wild, and possibly pushing out an update to convert the datastores before iOS11 could work for some, but that isn't going to guarantee all users get the fix and they could lose their data.

EDIT 1: Radar number : 33895450

EDIT 2: It just occured to me that this applies to any transformable attribute in core data, the values supported in the error message are just the default property types.

EDIT 3: Just out of curiosity I filled out all the fields for the transformable attribute (it was never required before). I added "NSKeyedUnarchiveFromData" to value transformer name of the core data entity, it should be the default, but you never know. No effect. It must be using the value transformer anyway to know that it's a UIColor. I filled in the custom class field to be UIColor, no effect.

Edit 5 : I noticed earlier that UIColor now supports NSSecureCoding, should security somehow be the issue somehow overlooked in the other store typed.

Edit : Now that iOS is released, i’ve used one of my TSIs to further escalate this. Do i get them back if i have to use one to get them to fix their software?

Edit : Apple got back to me on my TSI, they said it’s under investigation, there is no workaround, and to wait on the bug. They refunded my TSI because they couldn’t help.

Edit 8: Same problem on macOS High Sierra, with NSColor instead of UIColor.

Apple still have not given me any feedback on my actual bug report.

4

2 回答 2

6

好吧,Apple 回复了我,有新的 persistentStore 选项!

我从苹果那里得到的文字:

/* 允许开发人员提供一组额外的类(必须实现 NSSecureCoding),在解码二进制存储时应该使用这些类。使用此选项优于使用 NSBinaryStoreInsecureDecodingCompatibilityOption。*/ COREDATA_EXTERN NSString * const NSBinaryStoreSecureDecodingClasses API_AVAILABLE(macosx(10.13),ios(11.0),tvos(11.0),watchos(4.0));

/* 表示二进制存储应该被不安全地解码。如果商店有元数据或包含非标准类的可转换属性,这可能是必要的。如果可能,开发人员应该使用 NSBinaryStoreSecureDecodingClasses 选项来指定包含的类,从而允许对二进制存储进行安全解码。在可用日期之前链接的应用程序将默认使用此选项。*/ COREDATA_EXTERN NSString * const NSBinaryStoreInsecureDecodingCompatibilityOption API_AVAILABLE(macosx(10.13),ios(11.0),tvos(11.0),watchos(4.0));

目前还不是很清楚,但基本上你必须提供一个 NSSet 类,这些类用作可转换属性,在打开持久存储时作为一个选项符合 NSSecureCoding。

我使用 UIColor 的一个例子:

NSError *localError;
NSDictionary *options;
if (@available(iOS 11.0, *)) {
    options = @{
                NSMigratePersistentStoresAutomaticallyOption : @YES,
                NSInferMappingModelAutomaticallyOption : @YES,
                NSBinaryStoreSecureDecodingClasses : [NSSet setWithObjects:[UIColor class], nil]
               };

} else {
    // Fallback on earlier versions
    options = @{
                NSMigratePersistentStoresAutomaticallyOption : @YES,
                NSInferMappingModelAutomaticallyOption : @YES,
                };
}
NSPersistentStore *newStore = [self.psc addPersistentStoreWithType:NSBinaryStoreType configuration:@"iOS" URL:psURL options:options error:&localError];

编辑:为使用 NSPersistentStoreDescription 打开核心数据持久存储的新方法添加解决方案。此代码基于当前的核心数据模板。

- (NSPersistentContainer *)persistentContainer {
    // The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it.
    @synchronized (self) {
        if (_persistentContainer == nil) {
            NSURL *defaultURL = [NSPersistentContainer defaultDirectoryURL];
            defaultURL = [defaultURL URLByAppendingPathComponent:@"CoreDataTransformableAttribBug.binary"];
            _persistentContainer = [[NSPersistentContainer alloc] initWithName:@"CoreDataTransformableAttribBug"];
            NSPersistentStoreDescription *desc = [NSPersistentStoreDescription persistentStoreDescriptionWithURL:defaultURL];

            desc.type = NSBinaryStoreType;
            if (@available(iOS 11.0, *)) {
                [desc setOption:[NSSet setWithObjects:[UIColor class], nil] forKey:NSBinaryStoreSecureDecodingClasses];
            }
            _persistentContainer.persistentStoreDescriptions = @[desc];
            [_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
                if (error != nil) {
                    // Replace this implementation with code to handle the error appropriately.
                    // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

                    /*
                     Typical reasons for an error here include:
                     * The parent directory does not exist, cannot be created, or disallows writing.
                     * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                     * The device is out of space.
                     * The store could not be migrated to the current model version.
                     Check the error message to determine what the actual problem was.
                    */
                    NSLog(@"Unresolved error %@, %@", error, error.userInfo);
                    abort();
                } else {
                    NSLog(@"Description = %@", storeDescription);
                }
            }];
        }
    }

    return _persistentContainer;
}

我还用分支中的修复更新了我的 gitHub 项目

于 2017-10-13T21:33:27.073 回答
3

乔治做了所有艰苦的工作。我只将它应用于 Swift。这是我的解决方案。我把它放在我的NSPersistentDocument后代身上。

override func configurePersistentStoreCoordinator(for url: URL, ofType fileType: String, modelConfiguration configuration: String?, storeOptions: [String : Any]? = nil) throws {
    var options = storeOptions != nil ? storeOptions! : [String:Any]()
    if #available(OSX 10.13, *) {
        options[NSBinaryStoreSecureDecodingClasses] = NSSet(object: NSColor.self)
    }
    options[NSMigratePersistentStoresAutomaticallyOption] = true
    options[NSInferMappingModelAutomaticallyOption] = true
    try super.configurePersistentStoreCoordinator(for: url, ofType: fileType, modelConfiguration: configuration, storeOptions: options)
}

现在我可以再次阅读我的文件了。谢谢乔治!

于 2017-10-15T08:57:59.627 回答