NSSecureCoding returns nil for properly saved object - nscoding

i am on macOS 10.15 (bigSUr), XCode 12, objective-c not ios.
I have document based app. It has a simple object "SHGlobalAppData" (NSObject) that contains a property object of a custom class "SHSetupDataModel" (NSObject).
When loading, initWithCoder returns nil for a saved value. Why?
This is the implementation:
I use NSSecureCoding, therefore both SHSetupDataModel and SHGlobalAppData have included the appropriate class method
+ (BOOL)supportsSecureCoding { return YES;}
Saving is done within NSDocument with secure coding
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError {
NSKeyedArchiver* archiver=[[NSKeyedArchiver alloc] initRequiringSecureCoding:YES];
[archiver encodeObject:self.appData forKey:#"appData"]; // SHSetupDataModel is a property of appData object
//[...]
}
How saving is done
When it comes to saving, this is the code for SHGlobalAppData
- (void)encodeWithCoder:(NSCoder *)coder {
// Other properties here
if (_setupData){
// Tests
NSLog(#"%#",[_setupData className]); // returns "SHSetupDataModel"
BOOL test = [_setupData isKindOfClass:[SHSetupDataModel class]]; // returns TRUE
[coder encodeObject:_setupData forKey:#"setupData"];
}
}
The above saving runs through smoothly. Tests are fine.
How loading is done
Now when loading the saved file, the following NSDocument method is invoked:
- (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError *__autoreleasing _Nullable *)outError {
NSData* data = [[NSData alloc] initWithContentsOfURL:url];
NSKeyedUnarchiver* unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:outError];
[unarchiver setRequiresSecureCoding:YES];
// Load appData
SHGlobalAppData* appData = [unarchiver decodeObjectOfClass:[SHGlobalAppData class] forKey:#"appData"];
[unarchiver finishDecoding];
// [...]
}
This invokes the initWithCoder method form SHGlobalAppData - where i get a nil result
- (id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
if ([coder containsValueForKey:#"setupData"]){
_setupData = [coder decodeObjectOfClass:[SHSetupDataModel class] forKey:#"setupData"]; // <---- This is nil. Why?
}
// [...]
}
}
Can anyone please help me why this is returning nil? Or lead me towards a more effective debugging?

The data model itself contained a NSDictionary that was not properly decoded. The solution was to analyze the outError and then step by step work through initWithCoder methods.
Finally I had to pass multiple classes in decodeObjectWithClasses:ofKey:

Related

How to update NSKeyedUnarchiver unarchiveObjectWithData to NSKeyedUnarchiver unarchivedObjectOfClass:[fromData:error:

My app currently uses this deprecated function:
id unarchivedObject=[NSKeyedUnarchiver unarchiveObjectWithData:codedData];
if([unarchivedObject isKindOfClass:[NSDictionary class]]){
// currently returns TRUE when reading existing user data.
}
To update, I've converted to this:
id unarchivedObject=[NSKeyedUnarchiver unarchivedObjectOfClass:[NSDictionary class] fromData:codedData error:nil];
if([unarchivedObject isKindOfClass:[NSDictionary class]]){
// currently returns FALSE when reading existing user data.
}
The data was originally encoded like this:
-(void)encodeWithCoder:(NSCoder*)encoder{
[encoder encodeObject:text forKey:#"text"];
}
-(instancetype)initWithCoder:(NSCoder*)decoder{
if(self=[super init]){
text=[decoder decodeObjectForKey:#"text"];
}
What could be causing the IF statement to return FALSE using the newer code?
Please note that I am concerned primarily with reading existing data stored prior to deprecating the Archiving functions. Simply changing to the newer functions does not resolve the issue.
Interesting question! I've been supporting iOS 10.0 so I haven't encountered such issue until I saw this. I was tinkering for an hour and I successfully found the issue.
What could be causing the IF statement to return FALSE using the newer
code?
It's because your unarchivedObject object is nil!
If you use the parameter error in the new method, you would see an error like this:
Error Domain=NSCocoaErrorDomain Code=4864 "This decoder will only
decode classes that adopt NSSecureCoding. Class 'QTPerson' does not
adopt it." UserInfo={NSDebugDescription=This decoder will only decode
classes that adopt NSSecureCoding. Class 'QTPerson' does not adopt it.
But how do we get the correct value for this unarchivedObject and not nil? It would take a couple of steps.
First off, make your model/class conform to <NSCoding, NSSecureCoding>
Example:
QTPerson.h
#import <Foundation/Foundation.h>
#class QTPerson;
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Object interfaces
#interface QTPerson : NSObject <NSCoding, NSSecureCoding>
#property (nonatomic, copy) NSString *text;
#end
NS_ASSUME_NONNULL_END
And then implement the protocol methods:
QTPerson.m
#import "QTPerson.h"
#implementation QTPerson
+ (BOOL)supportsSecureCoding {
return YES;
}
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:_text forKey:#"text"];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super init];
if (self) {
_text = [coder decodeObjectOfClass:[NSString class] forKey:#"text"];
}
return self;
}
#end
And then when archiving an object, you would want to pass YES to the parameter requiringSecureCoding, like so:
QTPerson *person = [[QTPerson alloc] init];
person.text = #"Glenn";
NSData *codedData1 = [NSKeyedArchiver archivedDataWithRootObject:person requiringSecureCoding:YES error:nil];
[[NSUserDefaults standardUserDefaults] setValue:codedData1 forKey:#"boom"];
Lastly, when unarchiving, just do what you did correctly, like so:
NSData *codedData = [[NSUserDefaults standardUserDefaults] dataForKey:#"boom"];
NSError *er;
id unarchivedObject=[NSKeyedUnarchiver unarchivedObjectOfClass:[QTPerson class] fromData:codedData error:&er];
if([unarchivedObject isKindOfClass:[QTPerson class]]){
NSLog(#"TRUE!");
} else {
NSLog(#"FALSE!");
}
Voila! You'll get nonnull object unarchivedObject, hence the TRUE/YES value you're looking for!

Why my object deallocated by itself when switching thread in Xcode without ARC?

I have an object like this:
typedef void (^ Completion) (Response *);
// Response class
#interface Response : NSObject {
NSDictionary * kdata;
}
- (id)initWithJson:(NSDictionary *)data;
#property (nonatomic, assign) NSDictionary * data;
#end
#implementation Response
- (id)initWithJson:(NSDictionary *)data { kdata = data; }
- (NSDictionary *) data { return kdata; }
- (void) setData: (NSDictionary *)data { kdata = data; }
- (NSDictionary *) msg { return kdata[#"msg"]; }
#end
// inside a networking class X implementation
- (void) doSomething:(completionBlock)completion {
NSDictionary * json = // get from networking function, which will always have key "msg".
Response * responseObj = [[Response alloc] initWithJson:json];
dispatch_async(dispatch_get_main_queue(), ^{
if (completion != nil) { completion (responseObj); }
});
}
// inside caller method
[X doSomething:^(Response * response) {
NSLog (#"%#", [response msg]);
}
This code will raise error on accessing kdata[#"msg"], even though I'm sure from the debug that the object was initialised properly with a dictionary contains key "msg". When I debug the object, on the watch window, it shows me that the kdata data type keeps changing, from NSArrayM, NSSet, NSDictionary, etc. And its contents also keep changing. I even add retain keyword when calling completion ([responseObj retain]); but still produce error.
But if the code in class X is changed into like this:
// inside a networking class X implementation
- (void) doSomething:(completionBlock)completion {
NSDictionary * json = // get from networking function, which will always have key "msg".
Response * responseObj = [[Response alloc] initWithJson:json];
if (completion != nil) { completion (responseObj); } // here is the change, no more switching to main thread
}
// inside caller method - no change here
[X doSomething:^(Response * response) {
NSLog (#"%#", [response msg]);
}
The code works perfectly. Why is that happened? This is built in Xcode without ARC.
EDIT: someone mentioned about the init. This is my mistake that what was written above is not exactly my code, and I copy the init method wrong. This is my init method:
- (instancetype) initWithData:(NSDictionary *)freshData {
NSParameterAssert(freshData); // make sure not nil
self = [super init];
if (self) {
kdata = freshData;
}
return self;
}
The problem is the object get's released right when you call the 'async' .
The way you declared your object is added to the autorelease pool since the control does not wait for 'async' to complete and the control return's by reaching the end of function 'doSomething' and releasing it's local objects which were added to the autorelease pool, and after that the memory location is used for other data and that's what you see confusing data.
I think by adding the __block specifier in front of your declaration you instruct the code to capture this object in following blocks strongly and release it when the block finished executing. Give it a try.
// inside a networking class X implementation
- (void) doSomething:(completionBlock)completion {
NSDictionary * json = // get from networking function, which will always have key "msg".
__block Response * responseObj = [[Response alloc] initWithJson:json];
dispatch_async(dispatch_get_main_queue(), ^{
if (completion != nil) { completion (responseObj); }
});
}
- (id)initWithJson:(NSDictionary *)data { kdata = data; }
You need call supers init here and return self.
Start to learn basics.

iOS Something going wrong between Singleton, Delegate and SocketIO

I have multiple views where I need to handle the network connection of socket.io, so I created singleton class namely MC_SocketHandler. Below is the code of the MC_SocketHandler class.
// MC_SocketHandler.h
#import <Foundation/Foundation.h>
#import "SocketIO.h"
#interface MC_SocketHandler : NSObject <SocketIODelegate>
// SocketIO
//#property (nonatomic) SocketIO *socketConnection;
+ (MC_SocketHandler *) sharedSocketHanderObj;
+ (SocketIO *) initHandShake;
+ (SocketIO *) getSocketConnection;
-(bool) isConnected;
-(void) disConnect;
-(void) fireAgentLeftChat;
#end
// MC_SocketHandler.m
#import "MC_SocketHandler.h"
#import "MC_APIUtility.h"
#implementation MC_SocketHandler
SocketIO *socketConnection = nil;
static MC_SocketHandler *sharedSocketObj = nil;
+ (MC_SocketHandler *) sharedSocketHanderObj {
if (sharedSocketObj == nil)
sharedSocketObj = [[MC_SocketHandler alloc] init];
return sharedSocketObj;
}
+(SocketIO*) initHandShake {
if (socketConnection == nil) {
NSDictionary *headers = [NSDictionary dictionaryWithObjectsAndKeys:[MC_APIUtility getApiToken], #"token", nil];
socketConnection = [[SocketIO alloc] initWithDelegate:(id)self ];
[socketConnection connectToHost:domain onPort:447 withParams:headers];
}
return socketConnection;
}
+ (SocketIO *) getSocketConnection {
return socketConnection;
}
-(bool) isConnected {
if (socketConnection == nil)
return socketConnection.isConnected;
return false;
}
-(void) disConnect {
if (socketConnection != nil && socketConnection.isConnected)
[socketConnection disconnect];
NSLog(#"Disconnected --- %hhd", socketConnection.isConnected );
return;
}
// SocketIO Delegate
-(void) socketIODidConnect:(SocketIO *)socket {
NSLog(#"Socket has Connected....");
}
-(void) socketIO:(SocketIO *)socket didReceiveEvent:(SocketIOPacket *)packet {
NSString *data = packet.data;
NSLog(#"---- didReceoveEvent - data - %#", data);
// Grab data from packet
NSDictionary *dict = packet.dataAsJSON;
NSLog(#"EVENT DATA :- %# DICT :- %#", data, dict);
/*
EVENTS To Listen
onSuccessInit
visitor_info
new_visitor
agent_online
agent_offline
agent_logout
*/
dict = nil;
// Pull out args fro mdict
//NSArray *args = dict[#"args"];
}
-(void) socketIO:(SocketIO *)socket didReceiveMessage:(SocketIOPacket *)packet {
NSLog(#"Rcvd Message - %#", packet.data);
}
-(void) socketIO:(SocketIO *)socket didSendMessage:(SocketIOPacket *)packet {
NSLog(#"Send Msg - %#", packet.dataAsJSON);
}
-(void) socketIO:(SocketIO *)socket onError:(NSError *)error {
NSLog(#"Error - %#", error);
}
-(void) socketIODidDisconnect:(SocketIO *)socket disconnectedWithError:(NSError *)error {
NSLog(#"Disconnected With Error - %#", error);
}
-(void) fireAgentLeftChat {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:[MainAppDataObject sharedAppDataObject].activeAgentChatItem.chatSessionId forKey:#"chat_session_id"];
[socketConnection sendEvent:#"agentLeftChat" withData:dict];
return;
}
- (void)dealloc {
socketConnection = nil;
}
#end
Code that I use it in 1 of my views :
// Init SocketIO
SocketIO *socket = [MC_SocketHandler initHandShake];
// Fire Agent Online event
[socket sendEvent:#"setAgentOnline" withData:nil];
Handshake is being done properly, setAgentOnline event is send properly. Other events that I fire are also done properly. BUT,
when socket gets connected thru initHandshake, I believe "Socket has Connected...." should be seen in logs as that is written in socketIODidConnect delegate method. Similarly, I receive event (I see logs of socket.m class), but my delegate method didReceiveEvent is never called. Same way I don't see any logs of any delegate methods.
In initHandShake method only I have set the delegate also :
socketConnection = [[SocketIO alloc] initWithDelegate:(id)self ];
yet why these methods aren't called.
I was also wondering, when I receive events, on different events I got to perform different actions. How will I transfer to particular view (View's obj won't be shared with this to call his method) ? And If I create delegate, then I will have to handle all delegate methods in all views. What's will be the best method to work out with this ? And why this Singleton & delegate methods aren't being linked & called when I have set the delegate. Where am I going wrong ?
Any help, guidance is highly appreciative. Thanks alot.
In SocketIO, you create a SocketIO
Is that right?
In fact called "socketConnection". Am i right?
AT THAT TIME...
you must set the delegate !!!
Essentially, your code must look like this,
socketConnection = make one of these.
socketConnection.delegate = self;
It's possible this is your fundamental problem. I hope it helps!
PS you should, almost certainly, use only properties in iOS development. get rid of your "traditional" variables and use only properties.

custom [super init] (subclassing)

Given the following classes:
MCAchievementCenter:MCModel (subclass)
-(id) initWithDelgate:(id<MCAchievementNotifications>)delegate {
self = [super initWithRessource:#"achievements"];
if (self)
{
self.delegate = delegate;
}
return self;
}
MCModel (superclass)
-(instancetype)initWithRessource:(NSString *)ressource {
NSString* ressourcePath = [[NSBundle mainBundle] pathForResource:ressource
ofType:#"json"];
NSData* raw = [NSData dataWithContentsOfFile:ressourcePath];
return [super initWithJSONData:raw];
}
Note: -initWithJSONData:raw is a category method on NSObject that populates the object with json data. (see https://github.com/uacaps/NSObject-ObjectMap)
My Problem: The initialization of MCAchievementCenter fails as it becomes nil.
Am I doing something wrong?
Any help appreciated.
Update: app enters loop >
Check for Two things
[NSData dataWithContentsOfFile:ressourcePath] in MCModel is nil or not.
[super initWithJSONData:raw] in the category you had created returns nil or not.
It might happen that resource is not available at that path , and then when you call super initWithJSONData , that too returns nil as raw data is nil.

Core Data Unit Testing - Unsure how to trigger error case in executeFetchRequest:error:

According to NSManagedObjectContext Class Documentation...
- (NSArray *)executeFetchRequest:(NSFetchRequest *)request error:(NSError **)error
Return Value
An array of objects that meet the criteria specified by request fetched from the receiver and from the persistent stores associated with the receiver’s persistent store coordinator. If an error occurs, returns nil. If no objects match the criteria specified by request, returns an empty array.
I'm trying to create a unit test for the situation "if an error occurs, returns nil."
I would like to stay away from using OCMock (or subclassing NSManagedObjectContext to override the executeFetchRequest:error: method) because I figure there's an easy way to ensure failure of this method. So far my unit test reads...
- (void)testReportingCoreDataErrorToDelegate
{
NSManagedObjectContext *badContext = [[NSManagedObjectContext alloc] init];
[bcc setManagedObjectContext:badContext];
[bcc fetchFromCoreData];
STAssertTrue([mockDelegate didReceiveCoreDataError], #"This never asserts, it fails because the fetch request couldn't find an entity name - i.e. no managed object model");
}
Is there a simple way to trigger a fetch request returning nil?
I had the same conundrum. I like to keep unit test coverage at 100% whenever possible. There is no easy way to generate an organic error condition. In fact, I'm not sure the current implementation of the 4 store types that come with Core Data will ever trigger an error in response to executeFetchRequest:error. But as it could happen in the future, here is what I did:
I have one unit test case file that is dedicated to validating how my classes handle errors populated by executeFetchRequest:error. I define a subclass of NSIncrementalStore that always produces an error during requests in the implementation file. [NSManagedObjectContext executeFetchRequest:error] is processed by [NSPersistentStoreCoordinator executeRequest:withContext:error:] which processes [NSPersistentStore executeRequest:withContext:error:] on all stores. You may notice that the word "fetch" drops when you move to the coordinator - saves and fetch requests are handled by the same method executeRequest:withContext:error:. So I get coverage for testing against save errors and fetch requests by defining a NSPersistentStore that will always respond to saves and fetches with errors.
#define kErrorProneStore #"ErrorProneStore"
#interface ErrorProneStore : NSIncrementalStore
#end
#implementation ErrorProneStore
- (BOOL)loadMetadata:(NSError **)error
{
//Required - Apple's documentation claims you can omit setting this, but I had memory allocation issues without it.
NSDictionary * metaData = #{NSStoreTypeKey : kErrorProneStore, NSStoreUUIDKey : #""};
[self setMetadata:metaData];
return YES;
}
-(void)populateError:(NSError **)error
{
if (error != NULL)
{
*error = [[NSError alloc] initWithDomain:NSCocoaErrorDomain
code:NSPersistentStoreOperationError
userInfo:nil];
}
}
- (id)executeRequest:(NSPersistentStoreRequest *)request
withContext:(NSManagedObjectContext *)context
error:(NSError **)error
{
[self populateError:error];
return nil;
}
- (NSIncrementalStoreNode *)newValuesForObjectWithID:(NSManagedObjectID *)objectID
withContext:(NSManagedObjectContext *)context
error:(NSError **)error
{
[self populateError:error];
return nil;
}
- (id)newValueForRelationship:(NSRelationshipDescription *)relationship
forObjectWithID:(NSManagedObjectID *)objectID
withContext:(NSManagedObjectContext *)context
error:(NSError **)error
{
[self populateError:error];
return nil;
}
- (NSArray *)obtainPermanentIDsForObjects:(NSArray *)array
error:(NSError **)error
{
[self populateError:error];
return nil;
}
#end
Now you can construct the Core Data stack using the ErrorProneStore and be guaranteed your fetch requests will return nil and populate the error parameter.
- (void)testFetchRequestErrorHandling
{
NSManagedObjectModel * model = [NSManagedObjectModel mergedModelFromBundles:nil];
[NSPersistentStoreCoordinator registerStoreClass:[ErrorProneStore class]
forStoreType:kErrorProneStore];
NSPersistentStoreCoordinator * coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
NSManagedObjectContext * context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setPersistentStoreCoordinator:coordinator];
[coordinator addPersistentStoreWithType:kErrorProneStore
configuration:nil
URL:nil
options:nil
error:nil];
NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:#"AValidEntity"];
NSError * error;
[context executeFetchRequest:request
error:&error];
STAssertNotNil(error, #"Error should always be nil");
}
In my opinion it is much easier to use OCMock.
- (void)testCountForEntityFetchError {
id mockContext =[OCMockObject partialMockForObject:self.context];
[[[mockContext stub] andCall:#selector(stubbedExecuteFetchRequest:error:) onObject:self] countForFetchRequest:OCMOCK_ANY error:[OCMArg setTo:nil]];
// Your code goes here
}
- (NSArray *)stubbedExecuteFetchRequest:(NSFetchRequest *)request error:(NSError **)error {
*error = [NSError errorWithDomain:#"CRTest" code:99 userInfo:nil];
return nil;
}

Resources