I am trying to save an array of custom classes in Core Data as a transformable attribute, but keep getting the following error when trying to load the saved data:
NSSecureUnarchiveFromData transformer> threw while decoding a value. ({
NSUnderlyingError = "Error Domain=NSCocoaErrorDomain Code=4864 \"value for key 'NS.objects' was of unexpected class 'MyCustomClass'
In the Core Data schema I have set the transformer to "NSSecureUnarchiveFromData" and the Custom Class to "NSArray" (since I want to save an array of "MyCustomClass")
MyCustomClass.h
#interface MyCustomClass : NSObject <NSSecureCoding>
#property (nonatomic, assign) NSString *identifier;
MyCustomClass.m
#implementation MyCustomClass
+ (BOOL)supportsSecureCoding {
return YES;
}
- (void)encodeWithCoder:(nonnull NSCoder *)coder {
[coder encodeObject:self.identifier forKey:#"Identifier"];
}
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
if (self = [super init]) {
self.identifier = [coder decodeObjectOfClass:[NSString class] forKey:#"Identifier"];
}
return self;
}
I even tried to change the property declaration in "MyCustomClass+CoreDataProperties" to NSArray<MyCustomClass *> but got the same error.
What step am I missing or doing wrong please?
I was missing a few steps. In addition to the code in the question, the following had to be done to get this to work:
• Change the attribute's Custom Class in the model editor to NSArray
• Create a new class called MyCustomClassTransformer:
#interface MyCustomClassTransformer: NSSecureUnarchiveFromDataTransformer {}
#end
#implementation MyCustomClassTransformer
+ (Class)transformedValueClass {
return [MyCustomClassTransformer class];
}
+ (BOOL)allowsReverseTransformation {
return YES;
}
+ (NSArray<Class> *)allowedTopLevelClasses {
return #[[MyCustomClass class], [NSArray class]];
}
#end
• Register new transformer in AppDelegate's "didFinishLaunchingWithOptions":
MyCustomClassTransformer *transformer = [[MyCustomClassTransformer new];
[NSValueTransformer setValueTransformer:transformer forName: #"MyCustomClassTransformer"];
Related
I am trying to use NSCoding to save and recover application state. I haven't used it before.
In my app, the protocol methods encodeWithCoder and initWithCoder are never being called. I have prepared a simple test case with the same problem so hopefully somebody can tell me what I am doing wrong.
Here is my CodingTest.h file:
#import <Foundation/Foundation.h>
#interface CodingTest : NSObject <NSCoding>
- (void) saveData;
- (void) loadData;
- (id) init: (int) testValue;
#end
Here is CodingTest.m
#import "CodingTest.h"
#interface CodingTest()
#property int testInt;
#end
#implementation CodingTest
- (id) init: (int) testValue
{
_testInt = testValue;
return self;
}
-(void) loadData
{
CodingTest *newTestClass = [NSKeyedUnarchiver unarchiveObjectWithFile:#"testfile"];
}
-(void) saveData
{
[NSKeyedArchiver archiveRootObject:self toFile:#"testfile"];
}
- (void) encodeWithCoder:(NSCoder *)encoder {
[encoder encodeInt:_testInt forKey:#"intValue"];
}
- (id)initWithCoder:(NSCoder *)decoder {
int oldInt = [decoder decodeIntForKey:#"intValue"];
return [self init:oldInt];
}
#end
I call it as follows:
CodingTest *testCase = [[CodingTest alloc] init:27];
[testCase saveData ];
[testCase loadData];
init, saveData and loadData are all being called. But encodeWithEncoder and initWithCoder are never called. What am I doing wrong?
The problem is that "testfile" on its own is not a valid filename. If this is changed to "tmp/testfile" it works fine.
Interestingly, if you get the file name wrong on encode, it won't call the decode function, even though the decode call doesn't specify the file name.
Is there a standard pattern for implementing a mutable/immutable object class pair in Objective-C?
I currently have something like the following, which I wrote based off this link
Immutable Class:
#interface MyObject : NSObject <NSMutableCopying> {
NSString *_value;
}
#property (nonatomic, readonly, strong) NSString *value;
- (instancetype)initWithValue:(NSString *)value;
#end
#implementation MyObject
#synthesize value = _value;
- (instancetype)initWithValue:(NSString *)value {
self = [self init];
if (self) {
_value = value;
}
return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
return [[MyMutableObject allocWithZone:zone] initWithValue:self.value];
}
#end
Mutable Class:
#interface MyMutableObject : MyObject
#property (nonatomic, readwrite, strong) NSString *value;
#end
#implementation MyMutableObject
#dynamic value;
- (void)setValue:(NSString *)value {
_value = value;
}
#end
This works, but it exposes the iVar. Is there a better implementation that remedies this situation?
Your solution follows a very good pattern: the mutable class does not duplicate anything from its base, and exposes an additional functionality without storing any additional state.
This works, but it exposes the iVar.
Due to the fact that instance variables are #protected by default, the exposed _value is visible only to the classes inheriting MyObject. This is a good tradeoff, because it helps you avoid data duplication without publicly exposing the data member used for storing the state of the object.
Is there a better implementation that remedies this situation?
Declare the value property in a class extension. An extension is like a category without a name, but must be part of the class implementation. In your MyMutableObject.m file, do this:
#interface MyMutableObject ()
#property(nonatomic, readwrite, strong) value
#end
Now you've declared your property, but it's only visible inside your implementation.
The answer from dasblinkenlight is correct. The pattern provided in the question is fine. I provide an alternative that differs in two ways. First, at the expense of an unused iVar in the mutable class, the property is atomic. Second, as with many foundation classes, a copy of an immutable instance simply returns self.
MyObject.h:
#interface MyObject : NSObject <NSCopying, NSMutableCopying>
#property (atomic, readonly, copy) NSString *value;
- (instancetype)initWithValue:(NSString *)value NS_DESIGNATED_INITIALIZER;
#end
MyObject.m
#import "MyObject.h"
#import "MyMutableObject.h"
#implementation MyObject
- (instancetype)init {
return [self initWithValue:nil];
}
- (instancetype)initWithValue:(NSString *)value {
self = [super init];
if (self) {
_value = [value copy];
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
- (id)mutableCopyWithZone:(NSZone *)zone {
// Do not use the iVar here or anywhere else.
// This pattern requires always using self.value instead of _value (except in the initializer).
return [[MyMutableObject allocWithZone:zone] initWithValue:self.value];
}
#end
MyMutableObject.h:
#import "MyObject.h"
#interface MyMutableObject : MyObject
#property (atomic, copy) NSString *value;
#end
MyMutableObject.m:
#import "MyMutableObject.h"
#implementation MyMutableObject
#synthesize value = _value; // This is not the same iVar as in the superclass.
- (instancetype)initWithValue:(NSString *)value {
// Pass nil in order to not use the iVar in the parent.
// This is reasonably safe because this method has been declared with NS_DESIGNATED_INITIALIZER.
self = [super initWithValue:nil];
if (self) {
_value = [value copy];
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
// The mutable class really does need to copy, unlike super.
return [[MyObject allocWithZone:zone] initWithValue:self.value];
}
#end
A fragment of test code:
NSMutableString *string = [NSMutableString stringWithString:#"one"];
MyObject *object = [[MyObject alloc] initWithValue:string];
[string appendString:#" two"];
NSLog(#"object: %#", object.value);
MyObject *other = [object copy];
NSAssert(object == other, #"These should be identical.");
MyMutableObject *mutable1 = [object mutableCopy];
mutable1.value = string;
[string appendString:#" three"];
NSLog(#"object: %#", object.value);
NSLog(#"mutable: %#", mutable1.value);
Some debugging right after the last line above:
2017-12-15 21:51:20.800641-0500 MyApp[6855:2709614] object: one
2017-12-15 21:51:20.801423-0500 MyApp[6855:2709614] object: one
2017-12-15 21:51:20.801515-0500 MyApp[6855:2709614] mutable: one two
(lldb) po mutable1->_value
one two
(lldb) po ((MyObject *)mutable1)->_value
nil
As mentioned in the comments this requires discipline in the base class to use the getter instead of the iVar. Many would consider that a good thing, but that debate is off-topic here.
A minor difference you might notice is that I have used the copy attribute for the property. This could be made strong instead with very little change to the code.
I am learning iOS programming and am confused by the following code regarding the use of keyword self.
From my understanding, self is like Java's this. It refers to the current instance. When I want to call a class method, the usual way should be like [PlayingCard validSuits]; But it's also OK to invade a class method on an instance, right? Like [self validSuits]; (I am in the class so self refers to an instance of PlayingCard)
But in the following code, it gives error somewhere but looks ok elsewhere.(Pointed out by 3 comments, this is within Xcode 5.1)
Am I missing anything?
(P.S. I think I am having the similar problem as here, which no one answered yet. He got the same error even using [PlayingCard validSuits]. )
// PlayingCard.m
#import "PlayingCard.h"
#implementation PlayingCard
#synthesize suit = _suit;
+ (NSArray *)validSuits {
return #[#"♠︎", #"♣︎", #"♥︎", #"♦︎"];
}
+ (NSArray *)rankStrings {
return #[#"?", #"A", #"2", #"3", #"4",#"5",#"6",#"7",#"8",#"9",#"10",#"J",#"Q",#"K"];
}
+ (NSUInteger)maxRank {
return [[PlayingCard rankStrings] count] -1;
//1. [self rankStrings] works fine.**
}
//override super class's method
- (NSString *)contents {
NSArray *rankStrings = [PlayingCard rankStrings];
//2. if change rankStrings to self, then error:
//No visible #interface for 'PlayingCard' declares the selector 'rankStrings'
return [rankStrings[self.rank] stringByAppendingString:self.suit];
}
- (void) setSuit:(NSString *)suit {
if ( [[PlayingCard validSuits] containsObject:suit]) {
//3.error when changed to [self validsuits]
//No visible #interface for 'PlayingCard' declares the selector 'validsuits'**
_suit = suit;
}
}
- (NSString *) suit {
return _suit ? _suit : #"?";
}
#end
The header file:
// PlayingCard.h
#import "Card.h"
#interface PlayingCard : Card
#property (nonatomic, strong) NSString *suit;
#property (nonatomic) NSUInteger rank;
+ (NSArray *) validSuits;
+ (NSUInteger) maxRank;
#end
If you are calling another class method from inside a class method (of the same class) you can just use [self classMethod]. If however you are in an instance method and you need to call that classes class method you can use [[self class] classMethod]
As pointed out by #Martin R - if you subclass PlayingCard, calling self in a class method will then be that subclass and not PlayingCard.
EDIT:
For completeness you need to do:
// PlayingCard.m
#import "PlayingCard.h"
#implementation PlayingCard
#synthesize suit = _suit;
+ (NSArray *)validSuits {
return #[#"♠︎", #"♣︎", #"♥︎", #"♦︎"];
}
+ (NSArray *)rankStrings {
return #[#"?", #"A", #"2", #"3", #"4",#"5",#"6",#"7",#"8",#"9",#"10",#"J",#"Q",#"K"];
}
+ (NSUInteger)maxRank {
return [[self rankStrings] count] -1;
}
//override super class's method
- (NSString *)contents {
NSArray *rankStrings = [[self class] rankStrings];
return [rankStrings[self.rank] stringByAppendingString:self.suit];
}
- (void) setSuit:(NSString *)suit {
if ( [[[self class] validSuits] containsObject:suit]) {
_suit = suit;
}
}
- (NSString *) suit {
return _suit ? _suit : #"?";
}
#end
I want to allow deep copy of my class object and am trying to implement copyWithZone but the call to [super copyWithZone:zone] yields the error:
error: no visible #interface for 'NSObject' declares the selector 'copyWithZone:'
#interface MyCustomClass : NSObject
#end
#implementation MyCustomClass
- (id)copyWithZone:(NSZone *)zone
{
// The following produces an error
MyCustomClass *result = [super copyWithZone:zone];
// copying data
return result;
}
#end
How should I create a deep copy of this class?
You should add the NSCopying protocol to your class's interface.
#interface MyCustomClass : NSObject <NSCopying>
Then the method should be:
- (id)copyWithZone:(NSZone *)zone {
MyCustomClass *result = [[[self class] allocWithZone:zone] init];
// If your class has any properties then do
result.someProperty = self.someProperty;
return result;
}
NSObject doesn't conform to the NSCopying protocol. This is why you can't call super copyWithZone:.
Edit: Based on Roger's comment, I have updated the first line of code in the copyWithZone: method. But based on other comments, the zone can safely be ignored.
I understand how to store a custom object in NSUser Defaults but when i tried to implement saving an object with an object of different type inside it (it is called composition if i'm not mistaken) following the same steps for inner object as i did for the first one i got runtime error. Could you please minutely describe steps that i have to undertake in order to save and retrieve everything correctly
All your objects should implement NSCoding protocol. NSCoding works recursively for objects that would be saved. For example, you have 2 custom classes
#interface MyClass : NSObject {
}
#property(nonatomic, retain) NSString *myString;
#property(nonatomic, retain) MyAnotherClass *myAnotherClass;
#interface MyAnotherClass : NSObject {
}
#property(nonatomic, retain) NSNumber *myNumber;
For saving MyClass object to NSUserDefaults you need to implement NSCoding protocol to both these classes:
For first class:
-(void)encodeWithCoder:(NSCoder *)encoder{
[encoder encodeObject:self.myString forKey:#"myString"];
[encoder encodeObject:self.myAnotherClass forKey:#"myAnotherClass"];
}
-(id)initWithCoder:(NSCoder *)decoder{
self = [super init];
if ( self != nil ) {
self.myString = [decoder decodeObjectForKey:#"myString"];
self.myAnotherClass = [decoder decodeObjectForKey:#"myAnotherClass"];
}
return self;
}
For second class:
-(void)encodeWithCoder:(NSCoder *)encoder{
[encoder encodeObject:self.myNumber forKey:#"myNumber"];
}
-(id)initWithCoder:(NSCoder *)decoder{
self = [super init];
if ( self != nil ) {
self.myNumber = [decoder decodeObjectForKey:#"myNumber"];
}
return self;
}
Note, if your another class (MyAnotherClass above) has also custom object then that custom object should implement NSCoding as well. Even you have NSArray which implicity contains custom objects you should implement NSCoding for these objects.