How to test a simple data object class which implements NSCoding? - ios

I have a small data object that needs to be serialized and deserialized. Lets say it is called WeatherDetails, and it looks like this:
WeatherDetails.h
#interface WeatherDetails : NSObject <NSCoding>
{
#private
#protected
}
#pragma mark - Properties
#property (nonatomic, copy) NSString *weatherCode;
#property (nonatomic, copy) NSString *weatherDescription;
#end
WeatherDetails.m
#import "WeatherDetails.h"
#implementation WeatherDetails
NSString *const WEATHER_DETAILS_WEATHER_CODE_KEY = #"s";
NSString *const WEATHER_DETAILS_WEATHER_DESCRIPTION_KEY = #"sT";
#pragma mark - Initialization, NSCoding and Dealloc
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
_weatherCode = [aDecoder decodeObjectForKey:#"weatherCode"];
_weatherDescription = [aDecoder decodeObjectForKey:#"weatherDescription"];
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_weatherCode forKey:#"weatherCode"];
[aCoder encodeObject:_weatherDescription forKey:#"weatherDescription"];
}
Currently my tests look like this;
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import "WeatherDetails.h"
#interface WeatherDetailsTests : XCTestCase
#end
#implementation WeatherDetailsTests
- (void)testThatWeatherCodeIsEncoded
{
WeatherDetails *details = [[WeatherDetails alloc] init];
[details setWeatherCode:#"A"];
NSData *archive = [NSKeyedArchiver archivedDataWithRootObject:details];
WeatherDetails *unarchive = [NSKeyedUnarchiver unarchiveObjectWithData:archive];
XCTAssertEqualObjects(#"A", [unarchive weatherCode]);
}
- (void)testThatWeatherDescriptionIsEncoded
{
WeatherDetails *details = [[WeatherDetails alloc] init];
[details setWeatherDescription:#"A"];
NSData *archive = [NSKeyedArchiver archivedDataWithRootObject:details];
WeatherDetails *unarchive = [NSKeyedUnarchiver unarchiveObjectWithData:archive];
XCTAssertEqualObjects(#"A", [unarchive weatherDescription]);
}
I have a gut feeling that this approach to testing if all properties are correctly encoded is not really optimal as there is duplication, but I can't really think of a better approach. Does anyone have a tip for me on improving this?

What you really want to test is that the object is same before and after you archive it.
Implement a method in your WeatherDetails to compare the objects (or override isEqual:).
- (BOOL)isEqualToWeatherDetails:(WeatherDetails *)details
{
if (![details isKindOfClass:[WeatherDetails class]]) return NO;
return [self.weatherCode == details.weatherCode && self.weatherDescription isEqualToString:details.weatherDescription];
}
Then you can do all your equality comparisons at once:
- (void)testNSCoder
{
WeatherDetails *details = [[WeatherDetails alloc] init];
[details setWeatherCode:#"A"];
details.weatherDescription = #"Cloudy";
NSData *archive = [NSKeyedArchiver archivedDataWithRootObject:details];
WeatherDetails *unarchive = [NSKeyedUnarchiver unarchiveObjectWithData:archive];
XCTAssertTrue([details isEqualToWeatherDetails:unarchive]);
}
If you overrode isEqual: then you could compare doing this:
XCTAssertEqualObjects(details, unarchive);
Apple tends to add additional methods (isEqualToArray:, isEqualToDictionary:). isEqual: is used by collections like NSSet and NSDictionary

In the end I improved it by applying a custom assertion;
When To Use It
We should consider creating a Custom Assertion whenever any of the following are true:
We find ourselves writing (or cloning) the same assertion logic in test after test
We find ourselves writing Conditional Test Logic in the result verification part of our tests. That is, our calls to Assertion Methods are embedded in if statements or loops.
The result verification parts of our tests are suffering from Obscure Test because we are using procedural rather than declarative result verification in the tests.
We find ourselves doing Frequent Debugging whenever assertions fail because they do not provide enough information.
The custom assertion currently looks like this:
/*!
* #define XCTAssertEqualSerialized(value, object, selector)
* Serializes (\a object), then desirializes it and compares result's property (\a propertyName) to (\a value) with XCTAssertEqualObjects
* #param value Value to compare.
* #param object Object to serialize.
* #param selector Objects property to compare.
*/
#define XCTAssertEqualSerialized(value, object, selector) \
({ \
NSData *archived = [NSKeyedArchiver archivedDataWithRootObject:object]; \
NSObject *unarchived = [NSKeyedUnarchiver unarchiveObjectWithData:archived]; \
XCTAssertEqualObjects(value, [unarchived valueForKeyPath:NSStringFromSelector(selector)]); \
})
/*!
* #define XCTAssertNotNilSerialized(object, selector)
* Serializes (\a object), then desirializes it and checks result's property (\a propertyName) is not nil with XCTAssertNotNil
* #param object Object to serialize.
* #param selector Objects property to compare.
*/
#define XCTAssertNotNilSerialized(object, selector) \
({ \
NSData *archived = [NSKeyedArchiver archivedDataWithRootObject:object]; \
NSObject *unarchived = [NSKeyedUnarchiver unarchiveObjectWithData:archived]; \
XCTAssertNotNil([unarchived valueForKeyPath:NSStringFromSelector(selector)]); \
})
Which results in the tests looking like this:
- (void)testThatWeatherCodeIsEncoded
{
WeatherDetails *details = [[WeatherDetails alloc] init];
[details setWeatherCode:#"A"];
XCTAssertEqualSerialized(#"A", details, #selector(weatherCode));
}
- (void)testThatWeatherDescriptionIsEncoded
{
WeatherDetails *details = [[WeatherDetails alloc] init];
[details setWeatherDescription:#"A"];
XCTAssertEqualSerialized(#"A", details, #selector(weatherDescription));
}

Related

How to archive and unarchive an NSObject?

I am trying to archive an NSObject that I send through match data in a game center turn based game.
Below is my code for archiving my object
turnDataObject MyData = [[turnDataObject alloc] init];
data = [NSKeyedArchiver archivedDataWithRootObject:MyData];
This is my code for unarchiving my object
readMyData = [NSKeyedUnarchiver unarchiveObjectWithData:data] ;
However when I run this code I get an error
thread 1 exc bad access code
I think that this might have to do with sending addresses when I archive data. How do I send something that will be readable when I unarchive it?
Edit 1: I get the error on the next line after I unarchive. it says that the adress I am trying to access is null. I remember reading somewhere that I souldn't send adresses of my NSObject but I am not sure how to convert it to something else.
readMyData = [NSKeyedUnarchiver unarchiveObjectWithData:data] ;
NSLog(#"current game happens to be: %#", readMyData.currentGame);
Edit 2: here is my init with coder and encode with coder
- (instancetype)initWithCoder:(NSCoder *)decoder
{
self = [self init];
if (self) {
_currentGame = [decoder decodeObjectForKey:currentGameDataKey];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder
{
//scores data keys
[encoder encodeObject:self.currentGame forKey:currentGameDataKey];
}
Edit 3: _currentGame is in my objects .h file
#property (assign, nonatomic) NSString *currentGame;
I would suggest creating NSKeyedArchiver and NSKeyedUnarchiver objects and using those instead of using the type (which it looks like you're doing.)
I usually program in Swift but here's a shot at example code:
theArchiver NSKeyedArchiver = [[theArchiver alloc] init];
data = [theArchiver archivedDataWithRootObject:MyData];
Then you would do the same thing with the NSKeyedUnarchiver.
Your initWithCoder implementation is wrong:
self = [self init];
That should be:
self = [super init];
You need to add NSCoding protocol to MyData Class, here is the code snippet with supporting NSCoding in order to add Archiving support to NSObjet.
MyData.h
#interface MyData : NSObject <NSCoding>
#property (nonatomic, strong) NSString *currentGame;
#end
MyData.m
//This method is optional, if you need constructor for current game
- (instancetype)initWithCurrentGame:(NSDictionary *)currentGame {
self = [super init];
if (self) {
self.currentGame = currentGame;
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:self.currentGame forKey:#"currentGame"];
}
-(id)initWithCoder:(NSCoder *)decoder {
self.currentGame = [decoder decodeObjectForKey:#"currentGame"];
return self;
}

Save data even if the app re-opens - Swift Xcode 6 iOS

I'm working on a app that can randomize love couples. Just a fun thing, okey!?!? :D
But the problem, or maybe not a problem but a thing that can be much better if I get this thing to be working. In the beginning you need to write in all the names. And thats takes some time... Should I use Core Date? I don't really knows what core data is so I'm not sure. I would love if a god come to me and wrote the full code that can remember an array even if the app and phone shuts down. I have done this in java, is that simpel that it is in java? That would be great!
//Thank, Anton
For Heavy, complex data structures you would want to use core data,
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Articles/cdTechnologyOverview.html#//apple_ref/doc/uid/TP40009296-SW1
But seeing as you just want to store an array, You should look into NSUserDefaults.
NSUserDefaults will store given data as long as the app is not deleted. You will most likely want to create some kind of custom DataStorage class for this.
#interface DataStorage : NSObject <NSCoding>
#property (nonatomic, strong) NSMutableArray *arrayToStore;
+ (instancetype)sharedInstance;
- (void)save;
#end
Above is the .h file. As you can see, it follows NSCoding protocols. That provides access to methods which allow you to encode data. You will use the save method to write the data to disk.
#import "DataStorage.h"
#implementation DataStorage
#synthesize arrayOfPeople = _arrayToStore;
+ (DataStorage *)sharedInstance
{
static DataStorage *state = nil;
if ( !state )
{
NSData *data =[[NSUserDefaults standardUserDefaults] objectForKey:#"DataStorageKey"];
if (data)
{
state = [NSKeyedUnarchiver unarchiveObjectWithData:data];
}
else
{
state = [[DataStorage alloc] init];
}
}
return state;
}
- (id)init{
if (self = [super init]) {
if (!_arrayToStore) {
_arrayToStore = [[NSMutableArray alloc] init];
}
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)decoder
{
self = [self init];
if (self) {
if ([decoder decodeObjectForKey:#"DataStorageArrayToStore"]) {
_arrayToStore = [[decoder decodeObjectForKey:#"DataStorageArrayToStore"] mutableCopy];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:_arrayToStore forKey:#"DataStorageArrayToStore"];
}
- (void)save
{
NSData *appStateData = [NSKeyedArchiver archivedDataWithRootObject:self];
[[NSUserDefaults standardUserDefaults] setObject:appStateData forKey:#"DataStorageKey"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
#end
Here is the .m file, which pretty much evaluates to see if there is a saved instance of the class, and if not it will create one. [DataStorage sharedInstance]...
when you want to store some data, you will simply make the class available to said file, #import "DataStorage.m and then use
NSString *testData = [NSString stringWithFormat: #"Test Data String"];
[[DataStorage sharedInstance].arrayToStore addObject: testData];
[DataStorage sharedInstance] save];

Storing custom objects with Core Data

I'm trying to store a NSMutableArray consisting of VOs (NameVO) with Core Data but getting the following exception thrown:
'NSInvalidArgumentException', reason: '-[NameVO encodeWithCoder:]: unrecognized selector sent to instance 0x1096400a0'
My NameVO is just a simple value object, extending NSObject and contains two fields, a string and a NSMutableArray that itself contains strings. It also contains a compare function.
I'm trying to prepare this to be stored as a CD transformable attribute type with ('names' is my NameVO array):
NSData *tempNamesData = [NSKeyedArchiver archivedDataWithRootObject:names];
My question is: what do I need to do to make NameVO be accepted by NSKeyedArchiver to convert it successfully to a NSData?
I don't want NameVO to extend NSManagedObject because then I cannot instantiate and init it directly.
Now that we're in 2017, it's best to use the safer NSSecureCoding protocol instead of the older, less safe NSCoding protocol. The implementation changes are minimal:
1) ensure that your class declares its conformation to NSSecureCoding
#interface MyClass : NSObject<NSSecureCoding>
#property (nonatomic, strong) NSNumber *numberProperty;
#property (nonatomic, strong) NSString *stringProperty;
#end
2) NSSecureCoding protocol houses the same two instance methods methods in the NSCoding protocol, plus an additional class method, +supportsSecureCoding. You'll need to add that method, as well as slightly modify your -initWithCoder: method.
#implementation MyClass
// New Method for NSSecureCoding. Return YES.
+ (BOOL)supportsSecureCoding {
return YES;
}
// Your Encode Method Can Stay The Same, though I would use NSStringFromSelector whenever possible to get the keys to ensure you're always getting what you're looking for.
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.numberProperty forKey:NSStringFromSelector(#selector(numberProperty))];
[aCoder encodeObject:self.stringProperty forKey:NSStringFromSelector(#selector(stringProperty))];
}
// Slightly updated decode option
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
self.numberProperty = [aDecoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(#selector(numberProperty))];
self.stringProperty = [aDecoder decodeObjectOfClass:[NSString class] forKey:NSStringFromSelector(#selector(stringProperty))];
}
}
#end
Notice that NSCoder's -decodeObjectOfClass:withKey: requires you to specify the class that you're expecting to receive. This is a much safer way to do things.
Then, to store this decodable object in CoreData, simply create a Managed object that contains an NSData attribute and some identifying information (a string, a date, an id, or a number or something)
#interface MyClassMO : NSManagedObject
#property (nonatomic, strong) NSString *identifier;
#property (nonatomic, strong) NSData *data;
#end
#implementation MyClassMO
#dynamic identifier;
#dynamic data;
#end
In practice, it would look something like this:
- (void)storeObject:(MyClass *)object withIdentifier:(NSString *)identifier {
NSData *objectData = [NSKeyedArchived archivedDataWithRootObject:object];
NSManagedObjectContext *moc = ... // retrieve your context
// this implementation relies on new NSManagedObject initializers in the iOS 10 SDK, but you can create it any way you typically create managed objects
MyClassMO *managedObject = [[MyClassMO alloc] initWithContext:moc];
managedObject.data = objectData;
managedObject.identifier = identifier;
NSError *error;
[moc save:&error];
}
- (MyClass *)retrieveObjectWithIdentifier(NSString *)identifier {
NSManagedObject *moc = ... // retrieve your context
// This also relies on iOS 10 SDK's new factory methods available on NSManagedObject. You can create your request any way you typically do;
NSFetchRequest *request = [MyClassMO fetchRequest];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"identifier = %#", identifier];
request.predicate = predicate;
NSError *error;
NSArray<MyClassMO *> *results = [moc executeFetchRequest:request withError:&error];
// if you're only storing one object per identifier, this array should only be 1 object long. if not, you'll need to decide which object you're looking for. you also might want to implement an overwrite policy or a delete before store type thing.
MyClassMO *managedObject = results.firstObject;
NSData *objectData = managedObject.data;
MyClass *object = [NSKeyedUnarchiver unarchiveObject:objectData];
return object;
}
This solution is obviously a bit of an oversimplification, how and when you store stuff in the db is up to your needs, but the main idea is that, you'll need to make sure your custom class conforms to NSSecureCoding, and that you'll need to make a separate Managed Object class to store and retrieve your data.
As your exception says you need to implement NSCoding protocol to your class and you have to override two methods:
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (id)initWithCoder:(NSCoder *)aDecoder;
It should sorted your issue.
// EXTENDED
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
_formId = [aDecoder decodeObjectForKey:#"FormID"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:self.formId forKey:#"FormID"];
}
Use this initWithCoder and encodeWithCode method.I hope it will work for you. it works for me in same issue as u have...Use this sample code
-(id)initWithCoder:(NSCoder *)aDecoder
{
if(self = [super init]){
storePlaylist=[aDecoder decodeObjectForKey:#"storePlaylist"];
playlistName=[aDecoder decodeObjectForKey:#"playlistName"];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:storePlaylist forKey:#"storePlaylist"];
[encoder encodeObject:playlistName forKey:#"playlistName"];
}

how to store classobject that returns self

I am making a NSObjectClass that has a method in it that returns self.
This is what it looks like roughtly
storageclass.h
// storageclass vars go here
- (storageclass)assignData:(NSDictionary *)dictionary;
storageclass.m
//#synthesise everything
- (storageclass)assignData:(NSDictionary *)dictionary {
//assign values from dictionary to correct var types (i.e. NSString, Int, BOOL)
//example
Side = [dictionary valueForKey:#"Side"];
return self;
}
Then what I want to do is use this class by passing a NSDictionary var through its method to return a object of type storageclass that I can then use to access the vars using dot notation.
this is how I am trying to access this class at the moment
accessorViewController.h
storageclass *store;
#property (strong, nonatomic) storageclass *store;
accessorViewController.m
#synthesize store;
- (void)getstoreready {
[store assignData:someDictionary];
nslog(#"%#", store);
}
this NSLog returns nothing and in the debugger all of stores class vars are empty showing nothing has been assigned. I am 100% positive the dictionary vars being used in the assignData method have the correct valueForKey values.
I think it has something to do with how I am using it here [store assignData:someDictionary]; how do i catch the turned data so I can use it?
any help would be appreciated.
The store object is never initialized so it will be nil thats obvious isn't it. Initialize the store object first, then call its instance methods onto it. And by doing that, you'll have a storageclass object which is properly assigned with some dictionary already.
And if you want to have a storageclass object like your code shows, you should make your (storageclass)assignData:(NSDictionary *)dictionary method a class method instead of an instance method by putting a + sign
+(storageclass*)assignData:(NSDictionary *)dictionary;
Then properly initialize it and assign the data (dictionary to variables) accordingly and return it to the caller. For example :-
in .m file
+(storageclass*)assignData:(NSDictionary *)dictionary{
storageclass *test = [[storageclass alloc] init];
if (test) {
test.someDict = dictionary;
}
return test;
}
Then use this class method in your view controller as
- (void)getstoreready {
store = [storageClass assignData:someDictionary];
nslog(#"%#", store);
}
Also Do follow the naming convention for classes and instances. A class's name must start with a capital letter only and the opposite for any class instances.
In User.h
#interface User : NSObject
#property (nonatomic, copy) NSString *name;
- (id)initWithDictionary:(NSDictionary *)dictionary;
+ (NSArray *)usersFromArray:(NSArray *)array;
#end
In User.m
- (id)initWithDictionary:(NSDictionary *)dictionary
{
self = [super init];
if (self) {
if (dictionary)
{
self.name = dictionary[#"kUserName"];
}
}
return self;
}
+ (NSArray *)usersFromArray:(NSArray *)array
{
NSMutableArray *users = [NSMutableArray array];
for (NSDictionary *dict in array) {
User *user = [[User alloc]initWithDictionary:dict];
[users addObject:user];
}
NSSortDescriptor *descriptor = [NSSortDescriptor sortDescriptorWithKey:#"name"
ascending:YES];
return [users sortedArrayUsingDescriptors:#[descriptor]];
}
In ViewController.m
import "User.h"
self.currentArray = [User usersFromArray:array];

How to save a (custom) NSObject in Core Data

In my application i have an custom NSObject, which contains 2 mutable Arrays.
I need to save this custom NSOBject into a core data entity, but i have no real idea how i can accomplish that...
After some searching, i found out, that the best way would be to convert the nsobject to nsdata and save it in an transformable field of the entity... but i m not sure how to do that.
can someone help me?
heres to code for my custom object:
MeasureData.h
#interface MeasureData : NSObject{
}
#property (nonatomic, strong) NSMutableArray *questionsData;
#property (nonatomic, strong) NSMutableArray *answersData;
- (id) init;
#end
MeasureData.m
#import "MeasureData.h"
#implementation MeasureData
#synthesize questionsData;
#synthesize answersData;
#pragma mark -
#pragma mark int
- (id)init
{
self = [super init];
// Initalize questions array (width data from plist)
questionsData = self.makeQuestionsArray;
// NSLog(#"loaded questions array: %#",questionsData); // debug
// Initalize answers array
answersData = self.makeAnswersArray;
// NSLog(#"loaded answers array: %#",answersData); // debug
return self;
}
-(NSMutableArray *)makeQuestionsArray
{
// Initalize questions array (width data from plist)
NSString *path = [[NSBundle mainBundle] pathForResource:
#"questions.list" ofType:#"plist"];
NSMutableArray *questions = [NSMutableArray arrayWithContentsOfFile:path];
/*
[questionsData insertObject:(NSString *)string atIndex:0];
*/
return questions;
}
-(NSMutableArray *)makeAnswersArray
{
// Initalize answers array
NSMutableArray *answers = [NSMutableArray arrayWithCapacity:0];
return answers;
}
-(id)initWithCoder:(NSCoder *)decoder {
if ((self=[super init])) {
questionsData = [decoder decodeObjectForKey:#"questionsData"];
answersData = [decoder decodeObjectForKey:#"answersData"];
}
return self;
}
-(void)encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:questionsData forKey:#"questionsData"];
[encoder encodeObject:questionsData forKey:#"questionsData"];
}
#end
According to the first comment, i implemented the encoder/coder functions for my custom class. And tried to archive and encode it (i m new to ios, so it could be completly wrong) - but it dont work... can someone tell me whats wrong?
heres the encoding (which dont work XD):
NSMutableData *dataToSave = (NSMutableData *)self.measureData;
NSKeyedArchiver *archiverForData = [[NSKeyedArchiver alloc] initForWritingWithMutableData:dataToSave];
[archiverForData encodeObject:dataToSave forKey:#"dataToSave"];
[archiverForData finishEncoding];
//
//theMeasure is the CoreData Entity
theMeasure.result = dataToSave;
In outline:
create a NSMutableData
create a NSKeyedArchiver with initForWritingWithMutableData over your data
serialize your arrays / objects / whatever you need (that implements NSCoding) with encode... methods of NSCoder
create a managed object with a BLOB (binary data) type field
write your encoded data from the mutable data to this field of the managed object.
In my answer to this question you can find some useful links: NSCoding VS Core data

Resources