Core Data categories not saving - ios

Trying to import an XML file that has questions, answers, and categories for how-to questions. When I attempt to run this, there are supposed to be 3 categories (and they're visible in the XML file) of LB, OP, and DP. When I run the app, it seems to assign all how-tos to the LB category. What am I missing that's causing all to be assigned to the LB category? Thanks.
howToXMLImporter.h
#import <UIKit/UIKit.h>
#import <libxml/tree.h>
#class howToXMLImporter, HowTos, HowToCategory, HowToCategoryCache;
#protocol howToXMLImporterDelegate <NSObject>
#optional
- (void)importerDidSave:(NSNotification *)saveNotification;
- (void)importerDidFinishParsingData:(howToXMLImporter *)importer;
- (void)importer:(howToXMLImporter *)importer didFailWithError:(NSError *)error;
#end
#interface howToXMLImporter : NSOperation {
#private
id <howToXMLImporterDelegate> __unsafe_unretained delegate;
// Reference to the libxml parser context
xmlParserCtxtPtr context;
NSURLConnection *xmlConnection;
BOOL done;
BOOL parsingAHowTo;
BOOL storingCharacters;
NSMutableData *characterBuffer;
HowTos *currentHowTo; // Current how-to importer working with
// The number of parsed how-tos is tracked so that the autorelease pool for the parsing thread can be periodically emptied to keep the memory footprint under control.
NSUInteger countForCurrentBatch;
NSManagedObjectContext *insertionContext;
NSPersistentStoreCoordinator *persistentStoreCoordinator;
NSEntityDescription *howToEntityDescription;
HowToCategoryCache *theCache;
NSURL *aogURL; // Source of the how-to XML file
}
#property (nonatomic, retain) NSURL *aogURL;
#property (nonatomic, assign) id <howToXMLImporterDelegate> delegate;
#property (nonatomic, retain) NSPersistentStoreCoordinator *persistentStoreCoordinator;
#property (nonatomic, retain, readonly) NSManagedObjectContext *insertionContext;
#property (nonatomic, retain, readonly) NSEntityDescription *howToEntityDescription;
#property (nonatomic, retain, readonly) HowToCategoryCache *theCache;
- (void)main;
#end
howToXMLImporter.m
#import "howToXMLImporter.h"
#import "HowTos.h"
#import "HowToCategory.h"
#import "HowToCategoryCache.h"
#import <libxml/tree.h>
// Function prototypes for SAX callbacks.
static void startElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI, int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes);
static void endElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI);
static void charactersFoundSAX(void *context, const xmlChar *characters, int length);
static void errorEncounteredSAX(void *context, const char *errorMessage, ...);
// Forward reference. The structure is defined in full at the end of the file.
static xmlSAXHandler simpleSAXHandlerStruct;
// Class extension for private properties and methods.
#interface howToXMLImporter ()
#property BOOL storingCharacters;
#property (nonatomic, retain) NSMutableData *characterBuffer;
#property BOOL done;
#property BOOL parsingAHowTo;
#property NSUInteger countForCurrentBatch;
#property (nonatomic, retain) HowTos *currentHowTo;
#property (nonatomic, retain) NSURLConnection *xmlConnection;
#property (nonatomic, retain) NSDateFormatter *dateFormatter;
#end
static double lookuptime = 0;
#implementation howToXMLImporter
// Pull in these variables
#synthesize aogURL, delegate, persistentStoreCoordinator;
#synthesize xmlConnection, done, parsingAHowTo, storingCharacters, currentHowTo, countForCurrentBatch, characterBuffer;
- (void)main {
if (delegate && [delegate respondsToSelector:#selector(importerDidSave:)]) {
[[NSNotificationCenter defaultCenter] addObserver:delegate selector:#selector(importerDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.insertionContext];
}
done = NO;
self.characterBuffer = [NSMutableData data];
NSURLRequest *theRequest = [NSURLRequest requestWithURL:aogURL];
// create the connection with the request and start loading the data
xmlConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
// This creates a context for "push" parsing in which chunks of data that are not "well balanced" can be passed to the context for streaming parsing.
context = xmlCreatePushParserCtxt(&simpleSAXHandlerStruct, (__bridge void *)(self), NULL, 0, NULL);
if (xmlConnection != nil) {
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (!done);
}
// Release resources used only in this thread.
xmlFreeParserCtxt(context);
self.characterBuffer = nil;
self.dateFormatter = nil;
self.xmlConnection = nil;
self.currentHowTo = nil;
theCache = nil;
NSError *saveError = nil;
NSAssert1([insertionContext save:&saveError], #"Unhandled error saving managed object context in import thread: %#", [saveError localizedDescription]);
if (delegate && [delegate respondsToSelector:#selector(importerDidSave:)]) {
[[NSNotificationCenter defaultCenter] removeObserver:delegate name:NSManagedObjectContextDidSaveNotification object:self.insertionContext];
}
if (self.delegate != nil && [self.delegate respondsToSelector:#selector(importerDidFinishParsingData:)]) {
[self.delegate importerDidFinishParsingData:self];
}
}
- (NSManagedObjectContext *)insertionContext {
if (insertionContext == nil) {
insertionContext = [[NSManagedObjectContext alloc] init];
[insertionContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
}
return insertionContext;
}
- (void)forwardError:(NSError *)error {
if (self.delegate != nil && [self.delegate respondsToSelector:#selector(importer:didFailWithError:)]) {
[self.delegate importer:self didFailWithError:error];
}
}
- (NSEntityDescription *)howToEntityDescription {
if (howToEntityDescription == nil) {
howToEntityDescription = [NSEntityDescription entityForName:#"HowTo" inManagedObjectContext:self.insertionContext];
}
return howToEntityDescription;
}
- (HowToCategoryCache *)theCache {
if (theCache == nil) {
theCache = [[HowToCategoryCache alloc] init];
theCache.managedObjectContext = self.insertionContext;
}
return theCache;
}
- (HowTos *)currentHowTo {
if (currentHowTo == nil) {
currentHowTo = [[HowTos alloc] initWithEntity:self.howToEntityDescription insertIntoManagedObjectContext:self.insertionContext];
}
return currentHowTo;
}
// Forward errors to the delegate.
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self performSelectorOnMainThread:#selector(forwardError:) withObject:error waitUntilDone:NO];
done = YES; // Ends the run loop.
}
// Called when a chunk of data has been downloaded.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// Process downloaded chunk of data.
xmlParseChunk(context, (const char *)[data bytes], [data length], 0);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// Signal the context that parsing is complete by passing "1" as the last parameter.
xmlParseChunk(context, NULL, 0, 1);
context = NULL;
done = YES; // Ends the run loop.
}
static const NSUInteger kImportBatchSize = 20;
- (void)finishedCurrentHowTo {
parsingAHowTo = NO;
self.currentHowTo = nil;
countForCurrentBatch++;
if (countForCurrentBatch == kImportBatchSize) {
NSError *saveError = nil;
NSAssert1([insertionContext save:&saveError], #"Unhandled error saving managed object context in how-to import thread: %#", [saveError localizedDescription]);
countForCurrentBatch = 0;
}
}
// Character data is appended to a buffer until the current element ends.
- (void)appendCharacters:(const char *)charactersFound length:(NSInteger)length {
[characterBuffer appendBytes:charactersFound length:length];
}
- (NSString *)currentString {
// Create a string with the character data using UTF-8 encoding
NSString *currentString = [[NSString alloc] initWithData:characterBuffer encoding:NSUTF8StringEncoding];
[characterBuffer setLength:0];
return currentString;
}
#end
// XML element names and their string lengths for parsing comparison; include null terminator
static const char *kName_HowTos = "How_Tos"; // Container tag for product
static const NSUInteger kLength_HowTos = 8;
static const char *kName_Question = "how-to_question"; // Didn't include how-to_id for now
static const NSUInteger kLength_Question = 16;
static const char *kName_Answer = "how-to_answer";
static const NSUInteger kLength_Answer = 14;
static const char *kName_Category = "how-to_category";
static const NSUInteger kLength_Category = 16;
static void startElementSAX(void *parsingContext, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI,
int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes) {
howToXMLImporter *importer = (__bridge howToXMLImporter *)parsingContext;
// The second parameter to strncmp is the name of the element, which we know from the XML schema of the feed. The third parameter to strncmp is the number of characters in the element name, plus 1 for the null terminator.
if (!strncmp((const char *)localname, kName_HowTos, kLength_HowTos)) {
importer.parsingAHowTo = YES;
// May want to set default variable values here
} else if (importer.parsingAHowTo &&
(
(!strncmp((const char *)localname, kName_Question, kLength_Question) ||
!strncmp((const char *)localname, kName_Answer, kLength_Answer) ||
!strncmp((const char *)localname, kName_Category, kLength_Category)
)
)
) {
importer.storingCharacters = YES;
}
}
// This callback is invoked when the parser reaches the end of a node
static void endElementSAX(void *parsingContext, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI) {
howToXMLImporter *importer = (__bridge howToXMLImporter *)parsingContext;
if (importer.parsingAHowTo == NO) return;
if (!strncmp((const char *)localname, kName_HowTos, kLength_HowTos)) {
[importer finishedCurrentHowTo];
} else if (!strncmp((const char *)localname, kName_Question, kLength_Question)) {
importer.currentHowTo.question = importer.currentString;
NSLog(#"Question: %#",importer.currentHowTo.question);
} else if (!strncmp((const char *)localname, kName_Answer, kLength_Answer)) {
importer.currentHowTo.answer = importer.currentString;
NSLog(#"Answer: %#",importer.currentHowTo.answer);
} else if (!strncmp((const char *)localname, kName_Category, kLength_Category)) {
double before = [NSDate timeIntervalSinceReferenceDate];
HowToCategory *category = [importer.theCache howToCategoryWithName:importer.currentString];
double delta = [NSDate timeIntervalSinceReferenceDate] - before;
lookuptime += delta;
importer.currentHowTo.category = category;
NSLog(#"Category: %#",importer.currentHowTo.category.name);
}
importer.storingCharacters = NO;
}
// This callback is invoked when the parser encounters character data inside a node. Importer class determines how to use the character data.
static void charactersFoundSAX(void *parsingContext, const xmlChar *characterArray, int numberOfCharacters) {
howToXMLImporter *importer = (__bridge howToXMLImporter *)parsingContext;
// A state variable, "storingCharacters", is set when nodes of interest begin and end. Determines whether character data is handled or ignored.
if (importer.storingCharacters == NO) return;
[importer appendCharacters:(const char *)characterArray length:numberOfCharacters];
}
static void errorEncounteredSAX(void *parsingContext, const char *errorMessage, ...) {
// Handle errors as appropriate
NSCAssert(NO, #"Unhandled error encountered during SAX parse.");
}
HowToCategoryCache.h
#import <Foundation/Foundation.h>
#class HowToCategory;
#interface HowToCategoryCache : NSObject {
NSManagedObjectContext *managedObjectContext;
NSUInteger cacheSize; // Number of objects that can be cached
NSMutableDictionary *cache; // A dictionary holds the actual cached items
NSEntityDescription *howToCategoryEntityDescription;
NSPredicate *howToCategoryNamePredicateTemplate;
NSUInteger accessCounter; // Counter used to determine the least recently touched item.
// Some basic metrics are tracked to help determine the optimal cache size for the problem.
CGFloat totalCacheHitCost;
CGFloat totalCacheMissCost;
NSUInteger cacheHitCount;
NSUInteger cacheMissCount;
}
#property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;
#property NSUInteger cacheSize;
#property (nonatomic, strong) NSMutableDictionary *cache;
#property (nonatomic, strong, readonly) NSEntityDescription *howToCategoryEntityDescription;
#property (nonatomic, strong, readonly) NSPredicate *howToCategoryNamePredicateTemplate;
- (HowToCategory *)howToCategoryWithName:(NSString *)name;
#end
HowToCategoryCache.m
#import "HowToCategoryCache.h"
#import "HowToCategory.h"
// CacheNode is a simple object to help with tracking cached items
#interface HowToCacheNode : NSObject {
NSManagedObjectID *objectID;
NSUInteger accessCounter;
}
#property (nonatomic, strong) NSManagedObjectID *objectID;
#property NSUInteger accessCounter;
#end
#implementation HowToCacheNode
#synthesize objectID, accessCounter;
#end
#implementation HowToCategoryCache
#synthesize managedObjectContext, cacheSize, cache;
- (id)init {
self = [super init];
if (self != nil) {
cacheSize = 15;
accessCounter = 0;
cache = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (cacheHitCount > 0) NSLog(#"average cache hit cost: %f", totalCacheHitCost/cacheHitCount);
if (cacheMissCount > 0) NSLog(#"average cache miss cost: %f", totalCacheMissCost/cacheMissCount);
howToCategoryEntityDescription = nil;
howToCategoryNamePredicateTemplate = nil;
}
// Implement the "set" accessor rather than depending on #synthesize so that we can set up registration for context save notifications.
- (void)setManagedObjectContext:(NSManagedObjectContext *)aContext {
if (managedObjectContext) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:managedObjectContext];
}
managedObjectContext = aContext;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(managedObjectContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:managedObjectContext];
}
// When a managed object is first created, it has a temporary managed object ID. When the managed object context in which it was created is saved, the temporary ID is replaced with a permanent ID. The temporary IDs can no longer be used to retrieve valid managed objects. The cache handles the save notification by iterating through its cache nodes and removing any nodes with temporary IDs.
- (void)managedObjectContextDidSave:(NSNotification *)notification {
HowToCacheNode *cacheNode = nil;
NSMutableArray *keys = [NSMutableArray array];
for (NSString *key in cache) {
cacheNode = [cache objectForKey:key];
if ([cacheNode.objectID isTemporaryID]) {
[keys addObject:key];
}
}
[cache removeObjectsForKeys:keys];
}
- (NSEntityDescription *)howToCategoryEntityDescription {
if (howToCategoryEntityDescription == nil) {
howToCategoryEntityDescription = [NSEntityDescription entityForName:#"HowToCategory" inManagedObjectContext:managedObjectContext];
}
return howToCategoryEntityDescription;
}
static NSString * const kCategoryNameSubstitutionVariable = #"NAME";
- (NSPredicate *)categoryNamePredicateTemplate {
if (howToCategoryNamePredicateTemplate == nil) {
NSExpression *leftHand = [NSExpression expressionForKeyPath:#"name"];
NSExpression *rightHand = [NSExpression expressionForVariable:kCategoryNameSubstitutionVariable];
howToCategoryNamePredicateTemplate = [[NSComparisonPredicate alloc] initWithLeftExpression:leftHand rightExpression:rightHand modifier:NSDirectPredicateModifier type:NSLikePredicateOperatorType options:0];
}
return howToCategoryNamePredicateTemplate;
}
#define USE_CACHING // Undefine this macro to compare performance without caching
- (HowToCategory *)howToCategoryWithName:(NSString *)name {
NSTimeInterval before = [NSDate timeIntervalSinceReferenceDate];
#ifdef USE_CACHING
// check cache
HowToCacheNode *cacheNode = [cache objectForKey:name];
if (cacheNode != nil) {
cacheNode.accessCounter = accessCounter++; // cache hit, update access counter
HowToCategory *category = (HowToCategory *)[self.managedObjectContext objectWithID:cacheNode.objectID];
totalCacheHitCost += ([NSDate timeIntervalSinceReferenceDate] - before);
cacheHitCount++;
return category;
}
#endif
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; // cache missed, fetch from store - if not found in store there is no category object for the name and we must create one
[fetchRequest setEntity:self.howToCategoryEntityDescription];
NSLog(#"Fetch Request: %#",fetchRequest);
NSPredicate *predicate = [self.howToCategoryNamePredicateTemplate predicateWithSubstitutionVariables:[NSDictionary dictionaryWithObject:name forKey:kCategoryNameSubstitutionVariable]];
[fetchRequest setPredicate:predicate];
NSError *error = nil;
NSArray *fetchResults = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
NSAssert1(fetchResults != nil, #"Unhandled error executing fetch request in import thread: %#", [error localizedDescription]);
HowToCategory *category = nil;
if ([fetchResults count] > 0) {
category = [fetchResults objectAtIndex:0]; // get category from fetch
} else if ([fetchResults count] == 0) {
// category not in store, must create a new category object
category = [[HowToCategory alloc] initWithEntity:self.howToCategoryEntityDescription insertIntoManagedObjectContext:managedObjectContext];
category.name = name; //name
// Set the sort order for the category
if ([category.name isEqualToString:#"LB"]) {
category.order = [NSNumber numberWithInt:1];
} else if ([category.name isEqualToString:#"DP"]) {
category.order = [NSNumber numberWithInt:2];
} else if ([category.name isEqualToString:#"OP"]) {
category.order = [NSNumber numberWithInt:3];
}
}
#ifdef USE_CACHING
// add to cache
// first check to see if cache is full
if ([cache count] >= cacheSize) {
// evict least recently used (LRU) item from cache
NSUInteger oldestAccessCount = UINT_MAX;
NSString *key = nil, *keyOfOldestCacheNode = nil;
for (key in cache) {
HowToCacheNode *tmpNode = [cache objectForKey:key];
if (tmpNode.accessCounter < oldestAccessCount) {
oldestAccessCount = tmpNode.accessCounter;
keyOfOldestCacheNode = key;
}
}
// retain the cache node for reuse
cacheNode = [cache objectForKey:keyOfOldestCacheNode];
// remove from the cache
if (keyOfOldestCacheNode != nil)
[cache removeObjectForKey:keyOfOldestCacheNode];
} else {
// create a new cache node
cacheNode = [[HowToCacheNode alloc] init];
}
cacheNode.objectID = [category objectID];
cacheNode.accessCounter = accessCounter++;
[cache setObject:cacheNode forKey:name];
#endif
totalCacheMissCost += ([NSDate timeIntervalSinceReferenceDate] - before);
cacheMissCount++;
return category;
}
#end

Related

Troubles storing and loading custom object into NSUserDefaults Objective-C

I've been trying to store and recover from the NSUserDefaults an Array of custom objects in my project but it seems something is going wrong. I've researched a lot about this issue but I couldn't find any answer to my problem.
It seems that the storing it's done OK because I don't get any error when saving data in NSUserDefaults, the problem comes when I try to get back those data: the app will crash completely with libc++abi.dylib: terminating with uncaught exception of type NSException error.
Here's my code:
Wine.h
#import <Foundation/Foundation.h>
#import UIKit;
#define NO_RATING -1
#interface WineModel : NSObject <NSCoding>
#property(copy, nonatomic) NSString *type;
#property(strong, nonatomic) UIImage *photo;
#property(strong, nonatomic) NSURL *photoURL;
#property(strong, nonatomic) NSURL *wineCompanyWeb;
#property(copy, nonatomic) NSString *notes;
#property(copy, nonatomic) NSString *origin;
#property(nonatomic) int rating;
#property(strong, nonatomic) NSArray *grapes;
#property(copy, nonatomic) NSString *name;
#property(copy, nonatomic) NSString *wineCompanyName;
- (id) initWithCoder: (NSCoder *) decoder;
- (void) encodeWithCoder: (NSCoder *) encoder;
//SOME OTHER METHODS...//
-(id) initWithName: (NSString *) aName
wineCompanyName: (NSString *) aWineCompanyName
type: (NSString *) aType
origin: (NSString *) anOrigin
grapes: (NSArray *) arrayOfGrapes
wineCompanyWeb: (NSURL *) aURL
notes: (NSString *) aNotes
rating: (int) aRating
photoURL: (NSURL *) aPhotoURL;
//For JSON
-(id) initWithDictionary: (NSDictionary *) aDict;
#end
Wine.m
#import "WineModel.h"
#implementation WineModel
#synthesize photo = _photo;
#pragma mark - Properties
-(UIImage *) photo {
//SOME MORE CODE...
return _photo;
}
- (id) initWithCoder:(NSCoder *)decoder {
if (self = [super init]) {
self.name = [decoder decodeObjectForKey:#"name"];
self.wineCompanyName = [decoder decodeObjectForKey:#"company"];
self.type = [decoder decodeObjectForKey:#"type"];
self.origin = [decoder decodeObjectForKey:#"origin"];
self.grapes = [self extractGrapesFromJSONArray:[decoder decodeObjectForKey:#"grapes"]];
self.wineCompanyWeb = [NSURL URLWithString:[decoder decodeObjectForKey:#"wine_web"]];
self.notes = [decoder decodeObjectForKey:#"notes"];
self.rating = [[decoder decodeObjectForKey:#"rating"] intValue];
self.photoURL = [NSURL URLWithString:[decoder decodeObjectForKey:#"picture"]];
}
return self;
}
- (void) encodeWithCoder:(NSCoder *)encoder {
[encoder encodeObject:self.name forKey:#"name"];
[encoder encodeObject:self.wineCompanyWeb forKey:#"company"];
[encoder encodeObject:self.type forKey:#"type"];
[encoder encodeObject:self.origin forKey:#"origin"];
[encoder encodeObject:self.grapes forKey:#"grapes"];
[encoder encodeObject:self.wineCompanyWeb forKey:#"wine_web"];
[encoder encodeObject:self.notes forKey:#"notes"];
[encoder encodeInt:self.rating forKey:#"rating"];
[encoder encodeObject:self.photoURL forKey:#"picture"];
}
#pragma mark - Init
-(id) initWithName: (NSString *) aName
wineCompanyName: (NSString *) aWineCompanyName
type: (NSString *) aType
origin: (NSString *) anOrigin
grapes: (NSArray *) arrayOfGrapes
wineCompanyWeb: (NSURL *) aURL
notes: (NSString *) aNotes
rating: (int) aRating
photoURL: (NSURL *) aPhotoURL {
if(self==[super init]) {
_name = aName;
_wineCompanyName = aWineCompanyName;
_type = aType;
_origin = anOrigin;
_grapes = arrayOfGrapes;
_wineCompanyWeb = aURL;
_notes = aNotes;
_rating = aRating;
_photoURL = aPhotoURL;
}
return self;
}
#pragma mark - JSON
-(id) initWithDictionary:(NSDictionary *)aDict {
return [self initWithName:[aDict objectForKey:#"name"]
wineCompanyName:[aDict objectForKey:#"company"]
type:[aDict objectForKey:#"type"]
origin:[aDict objectForKey:#"origin"]
grapes:[self extractGrapesFromJSONArray:[aDict objectForKey:#"grapes"]]
wineCompanyWeb:[NSURL URLWithString:[aDict objectForKey:#"wine_web"]]
notes:[aDict objectForKey:#"notes"]
rating:[[aDict objectForKey:#"rating"]intValue]
photoURL:[NSURL URLWithString:[aDict objectForKey:#"picture"]]
];
}
-(NSArray *) extractGrapesFromJSONArray: (NSArray *)JSONArray {
//SOME MORE CODE...
return grapes;
}
#end
This is the wine class. It has the <NSCoding> protocol and both methods (id) initWithCoder: (NSCoder *) decoder; and (void) encodeWithCoder: (NSCoder *) encoder;. So far I looks OK, lets move on to the next class:
Winery.h
#import <Foundation/Foundation.h>
#import "Wine.h"
#define RED_WINE_KEY #"Red"
#define WHITE_WINE_KEY #"White"
#define OTHER_WINE_KEY #"Others"
#interface WineryModel : NSObject
#property (strong, nonatomic) NSMutableArray *redWines;
#property (strong, nonatomic) NSMutableArray *whiteWines;
#property (strong, nonatomic) NSMutableArray *otherWines;
#property(readonly, nonatomic) int redWineCount;
#property(readonly, nonatomic) int whiteWineCount;
#property(readonly, nonatomic) int otherWineCount;
-(WineModel *) redWineAtIndex: (NSUInteger) index;
-(WineModel *) whiteWineAtIndex: (NSUInteger) index;
-(WineModel *) otherWineAtIndex: (NSUInteger) index;
#end
Winery.m
#import "Winery.h"
#implementation WineryModel
#pragma mark - Properties
-(int) redWineCount {
return [self.redWines count];
}
-(int) whiteWineCount {
return [self.whiteWines count];
}
-(int) otherWineCount {
return [self.otherWines count];
}
-(id) init {
if(self == [super init]) {
NSUserDefaults *userDefault=[NSUserDefaults standardUserDefaults];
//Check if there is data stored locally
if(([[[userDefault dictionaryRepresentation] allKeys] containsObject:#"redWines"])
&&([[[userDefault dictionaryRepresentation] allKeys] containsObject:#"whiteWines"])
&&([[[userDefault dictionaryRepresentation] allKeys] containsObject:#"otherWines"])) {
if([userDefault objectForKey:#"redWines"] != nil && [userDefault objectForKey:#"whiteWines"] != nil && [userDefault objectForKey:#"otherWines"] != nil) {
//Try to load data from NSUserDefaults
NSData *decodedRedWines = [userDefault objectForKey:#"redWines"];
self.redWines = [[NSKeyedUnarchiver unarchiveObjectWithData: decodedRedWines] mutableCopy]; //IT WILL CRASH HERE
NSData *decodedWhiteWines = [userDefault objectForKey:#"whiteWines"];
self.whiteWines = [[NSKeyedUnarchiver unarchiveObjectWithData: decodedWhiteWines] mutableCopy];
NSData *decodedOtherWines = [userDefault objectForKey:#"otherWines"];
self.otherWines = [[NSKeyedUnarchiver unarchiveObjectWithData: decodedOtherWines] mutableCopy];
}
} else {
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://url.com/wines.json"]]; //JSON URL
NSURLResponse *response = [[NSURLResponse alloc]init];
NSError *error;
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if(data != nil) { //No errors
//Passing from JSON to an NSArray
NSArray * JSONObjects = [NSJSONSerialization JSONObjectWithData:data
options:kNilOptions
error:&error];
if (JSONObjects != nil) {
//No errors
for(NSDictionary *dict in JSONObjects){
WineModel *wine = [[WineModel alloc] initWithDictionary:dict];
if(wine.name != nil && wine.wineCompanyName != nil && wine.type != nil && wine.origin != nil ) {
if ([wine.type isEqualToString:RED_WINE_KEY]) {
if (!self.redWines) {
self.redWines = [NSMutableArray arrayWithObject:wine];
}
else {
[self.redWines addObject:wine];
}
}
else if ([wine.type isEqualToString:WHITE_WINE_KEY]) {
if (!self.whiteWines) {
self.whiteWines = [NSMutableArray arrayWithObject:wine];
}
else {
[self.whiteWines addObject:wine];
}
}
else {
if (!self.otherWines) {
self.otherWines = [NSMutableArray arrayWithObject:wine];
}
else {
[self.otherWines addObject:wine];
}
}
}
}
} else {
NSLog(#"JSON parsing error: %#", error.localizedDescription);
}
} else {
NSLog(#"Server error: %#", error.localizedDescription);
}
//Storing the array of wine objects in the NSUserDefaults
NSData *encodedRedWines = [NSKeyedArchiver archivedDataWithRootObject:_redWines];
[userDefault setObject:encodedRedWines forKey:#"redWines"];
NSData *encodedWhiteWines = [NSKeyedArchiver archivedDataWithRootObject:_whiteWines];
[userDefault setObject:encodedWhiteWines forKey:#"whiteWines"];
NSData *encodedOtherWines = [NSKeyedArchiver archivedDataWithRootObject:_otherWines];
[userDefault setObject:encodedOtherWines forKey:#"otherWines"];
}
}
return self;
}
-(WineModel *) redWineAtIndex: (NSUInteger) index {
return [self.redWines objectAtIndex:index];
}
-(WineModel *) whiteWineAtIndex: (NSUInteger) index{
return [self.whiteWines objectAtIndex:index];
}
-(WineModel *) otherWineAtIndex: (NSUInteger) index{
return [self.otherWines objectAtIndex:index];
}
#end
So, the first time you launch the app it will download the data from a JSON file that is in the web, then store the info in the NSUserDefaults. It seems like this step it's done correctly (at least doesn't crash at this point). The problem comes after launching the app the second time. It will check if there are local data store under the NSUserDefault, if so, it'll try to load the data and store into an NSMutableAtray. Unfortunately it won't do so, It crashes here self.redWines =[NSKeyedUnarchiver unarchiveObjectWithData: decodedRedWines]; with the error code I wrote before. When debugging, I can see that there is data when retrieving the redWineskey, but it seems like something it's going wrong.
Mind that I'm using a customized initializer (initWithDictionary) for creating my wines object instead of the default init method. I don't know if it could be the reason of the crash...
Here's the full log:
2017-05-22 20:31:30.354640+0200 App[1905:891526] -[NSTaggedPointerString objectForKey:]: unrecognized selector sent to instance 0xa5c064950b08843b
2017-05-22 20:31:30.354932+0200 App[1905:891526] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSTaggedPointerString objectForKey:]: unrecognized selector sent to instance 0xa5c064950b08843b'
*** First throw call stack:
(0x18e0bafe0 0x18cb1c538 0x18e0c1ef4 0x18e0bef54 0x18dfbad4c 0x1000591d8 0x100057dec 0x18eb0a430 0x18eb10f10 0x18eaa684c 0x18eb0a430 0x18eb09b68 0x18eb08d94 0x100061118 0x1000621d0 0x10005c120 0x19425d204 0x194469738 0x19446f1e0 0x194483d18 0x19446c474 0x18fc63884 0x18fc636f0 0x18fc63aa0 0x18e06942c 0x18e068d9c 0x18e0669a8 0x18df96da4 0x194256384 0x194251058 0x100060b90 0x18cfa559c)
libc++abi.dylib: terminating with uncaught exception of type NSException
Any ideas??
Thanks in advance!!
There is a typo in your initWithCoder method:
self.wineCompanyName = [decoder decodeObjectForKey:#"comapny"];
If that does not fix it, I would look at the NSUserDefaults documentation more closely - it says "Values returned from NSUserDefaults are immutable, even if you set a mutable object as the value." Your redWines property is defined as an NSMutableArray.
To make an immutable object mutable just call mutableCopy
#property (strong, nonatomic) NSMutableArray *redWines;
...
self.redWines = [[NSKeyedUnarchiver unarchiveObjectWithData: decodedRedWines] mutableCopy];

Why NSString and NSNumber are not released after inserting it into [NSHashTable weakObjectsHashTable]?

I was trying to use NSHashTable to store some object pointers with weak references. During learning, I found that NSString(NSNumber) objects are not released after inserting it into [NSHashTable weakObjectsHashTable]. Any self defined class will work otherwise.
Any idea about why NSString(NSNumber) objects are not released?
#import <Foundation/Foundation.h>
#interface C : NSObject
#property(nonatomic) NSHashTable *hashTable;
#end
#implementation C
- (instancetype)init {
self = [super init];
if (self) {
_hashTable = [NSHashTable weakObjectsHashTable];
}
return self;
}
- (void)addObject:(NSObject *)obj {
[self.hashTable addObject:obj];
}
- (void)printAllObjects {
NSLog(#"Members: %#", [self.hashTable allObjects]);
[[self.hashTable allObjects] enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(#"%p\n", obj);
}];
}
+ (void)hi:(C *)c {
#autoreleasepool {
NSString *s;
for(int i=0; i<10000; i++) {
s = [[NSString alloc] initWithFormat:#"%d", i];
[c addObject:s];
}
}
}
#end
void hi(C *c) {
NSString *i = #"hi";
NSLog(#"%p", i);
[c addObject:i];
}
int main(int argc, char *argv[]) {
C *c = [[C alloc] init];
#autoreleasepool {
[C hi:c];
}
[c printAllObjects];
}
Xcode Version is 8.1 (8B62) and output is:
2016-12-12 11:16:30.361 0x7fff9106c920
2016-12-12 11:16:30.362 Members: (
""
)
2016-12-12 11:16:30.362 0x7fff9106c920

TableView not retrieving/displaying all records from SQLite database

Recently modified my app to include a tab view controller. I'm now unable to load all of the categories that are in my SQLite database when the app launches and goes to the Categories table view nav controller on app load (CategoriesViewController). There are 6 category names that it should display; sometimes the app displays 0 rows, other times it displays 4. Modifying the XML file that this pulls data from, I got it to pull 6 categories (after a few times of 0 and fast execution speed). Leads me to think this has to do with timing of execution vs. time until the table view is loaded with data. Any ideas on what I need to change to get CategoriesViewController to display the table only after all data is read? Here's the code; please let me know if there's anything else that you'd need to see. Thanks!
CategoriesViewController.h
#import <UIKit/UIKit.h>
#interface CategoriesViewController : UITableViewController
#property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;
- (void)fetch; // Fetches categories from data store
#end
CategoriesViewController.m
#import "CategoriesViewController.h"
#import "ProductsViewController.h"
#import "Category.h"
#import "AppDelegate.h"
#interface CategoriesViewController () // Categories View Controller interface
#property (nonatomic, strong) ProductsViewController *productController;
#property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
#property (weak, nonatomic) IBOutlet UIImageView *logoImageView;
#end
#pragma mark -
#implementation CategoriesViewController
- (void)viewDidLoad {
[super viewDidLoad];
_logoImageView.image = [UIImage imageNamed:#"company.jpg"];
[self fetch]; // Fetch categories from our data store
}
- (void)fetch {
NSError *error = nil;
BOOL success = [self.fetchedResultsController performFetch:&error]; // Fetch results; if error, assign error code, output message
NSAssert2(success, #"Unhandled error performing fetch at CategoriessViewController.m, line %d: %#", __LINE__, [error localizedDescription]);
}
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController == nil) {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Category" inManagedObjectContext:app.managedObjectContext]]; // Fetch categories
NSArray *sortDescriptors = nil;
NSString *sectionNameKeyPath = nil;
sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:#"order" ascending:YES]];
[fetchRequest setSortDescriptors:sortDescriptors];
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:app.managedObjectContext
sectionNameKeyPath:sectionNameKeyPath
cacheName:nil];
}
return _fetchedResultsController;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)table {
return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
- (UITableViewCell *)tableView:(UITableView *)table cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *kCellIdentifier = #"CategoryCell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:kCellIdentifier forIndexPath:indexPath];
Category *category = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:NSLocalizedString(#"%#", #"%#"), category.name];
return cell;
}
// Pass managed object to ProductsViewController
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:#"showProducts"]) {
ProductsViewController *detailsController = (ProductsViewController *)[segue destinationViewController];
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
detailsController.category = [self.fetchedResultsController objectAtIndexPath:selectedIndexPath];
AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[[segue destinationViewController] setManagedObjectContext:app.managedObjectContext];
}
}
#end
AppDelegate.h
#import <UIKit/UIKit.h>
#import "companyXMLImporter.h"
#interface AppDelegate : NSObject <UIApplicationDelegate, companyXMLImporterDelegate>
#property (nonatomic, strong) UIWindow *window;
#property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
#property (nonatomic, retain) NSPersistentStoreCoordinator *persistentStoreCoordinator;
- (NSDate *)lasModificationDateOfFileAtURL:(NSURL *)url;
#end
AppDelegate.m
#import "AppDelegate.h"
#import "ProductsViewController.h"
#import "CategoriesViewController.h"
#interface AppDelegate()
#property (nonatomic, strong) ProductsViewController *productsViewController;
#property (nonatomic, strong) CategoriesViewController *categoriesViewController;
// Properties for the importer and its background processing
#property (nonatomic, strong) companyXMLImporter *importer;
#property (nonatomic, strong) NSOperationQueue *operationQueue;
#property (nonatomic, strong) NSString *persistentStorePath;
#end
#implementation AppDelegate
static NSString * const kLastStoreUpdateKey = #"LastStoreUpdate"; // Identifies update object in user defaults storage
static NSTimeInterval const kRefreshTimeInterval = 3600;
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSDate *lastUpdate = [[NSUserDefaults standardUserDefaults] objectForKey:kLastStoreUpdateKey];
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:NSLocalizedString(#"Products XML", #"Products XML")]];
NSDate *lastModifiedDate = [self lasModificationDateOfFileAtURL:url];
if (lastUpdate == nil || lastModifiedDate >= lastUpdate || -[lastUpdate timeIntervalSinceNow] > kRefreshTimeInterval) {
if ([[NSFileManager defaultManager] fileExistsAtPath:self.persistentStorePath]) {
NSError *error = nil;
BOOL oldStoreRemovalSuccess = [[NSFileManager defaultManager] removeItemAtPath:self.persistentStorePath error:&error];
NSAssert3(oldStoreRemovalSuccess, #"Unhandled error adding persistent store in %s at line %d: %#", __FUNCTION__, __LINE__, [error localizedDescription]);
}
// Object to retrieve, parse, and import into CoreData store
self.importer = [[companyXMLImporter alloc] init];
self.importer.delegate = self;
// pass coordinator so importer can create its own managed object context
self.importer.persistentStoreCoordinator = self.persistentStoreCoordinator;
// URL for products XML file
self.importer.companyURL = [NSURL URLWithString:[NSString stringWithFormat:NSLocalizedString(#"Products XML", #"Products XML")]];
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[self.operationQueue addOperation:self.importer];
}
}
- (NSOperationQueue *)operationQueue {
if (_operationQueue == nil) {
_operationQueue = [[NSOperationQueue alloc] init];
}
return _operationQueue;
}
- (NSDate *)lasModificationDateOfFileAtURL:(NSURL *)url {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
// Get only header
request.HTTPMethod = #"HEAD";
NSHTTPURLResponse *response = nil;
NSError *error = nil;
[NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
if (error) {
NSLog(#"Error: %#", error.localizedDescription);
return nil;
} else if([response respondsToSelector:#selector(allHeaderFields)]) {
NSDictionary *headerFields = [response allHeaderFields];
NSString *lastModification = [headerFields objectForKey:#"Last-Modified"];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"EEE, dd MMM yyyy HH:mm:ss zzz"];
return [formatter dateFromString:lastModification];
}
return nil;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (_persistentStoreCoordinator == nil) {
NSURL *storeUrl = [NSURL fileURLWithPath:self.persistentStorePath];
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[NSManagedObjectModel mergedModelFromBundles:nil]];
NSError *error = nil;
NSPersistentStore *persistentStore = [_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:nil error:&error];
NSAssert3(persistentStore != nil, #"Unhandled error adding persistent store in %s at line %d: %#", __FUNCTION__, __LINE__, [error localizedDescription]);
}
return _persistentStoreCoordinator;
}
- (NSManagedObjectContext *)managedObjectContext {
if (_managedObjectContext == nil) {
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[self.managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
}
return _managedObjectContext;
}
- (NSString *)persistentStorePath {
if (_persistentStorePath == nil) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths lastObject];
_persistentStorePath = [documentsDirectory stringByAppendingPathComponent:#"Company.sqlite"];
}
return _persistentStorePath;
}
- (void)importerDidSave:(NSNotification *)saveNotification {
if ([NSThread isMainThread]) {
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:saveNotification];
// [self.categoriesViewController fetch];
[self.productsViewController fetch];
} else {
[self performSelectorOnMainThread:#selector(importerDidSave:) withObject:saveNotification waitUntilDone:NO];
}
}
// Main-thread import completion processing
- (void)handleImportCompletion {
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:kLastStoreUpdateKey];
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
self.importer = nil;
}
- (void)importerDidFinishParsingData:(companyXMLImporter *)importer {
[self performSelectorOnMainThread:#selector(handleImportCompletion) withObject:nil waitUntilDone:NO];
}
// Process errors received in delegate callback
- (void)handleImportError:(NSError *)error {
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
self.importer = nil;
NSString *errorMessage = [error localizedDescription];
NSString *alertTitle = NSLocalizedString(#"Error", #"Title for alert displayed when download or parse error occurs.");
NSString *okTitle = NSLocalizedString(#"OK ", #"OK Title for alert displayed when download or parse error occurs.");
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:alertTitle
message:errorMessage
delegate:nil
cancelButtonTitle:okTitle
otherButtonTitles:nil];
[alertView show];
}
- (void)importer:(companyXMLImporter *)importer didFailWithError:(NSError *)error {
[self performSelectorOnMainThread:#selector(handleImportError:) withObject:error waitUntilDone:NO];
}
#end
companyXMLImporter.h
#import <UIKit/UIKit.h>
#import <libxml/tree.h>
#class companyXMLImporter, Product, Category, CategoryCache;
#protocol companyXMLImporterDelegate <NSObject>
#optional
- (void)importerDidSave:(NSNotification *)saveNotification; // Posted when saved
- (void)importerDidFinishParsingData:(companyXMLImporter *)importer; // Called when parsing is finished
- (void)importer:(companyXMLImporter *)importer didFailWithError:(NSError *)error; // Called if error
#end
#interface companyXMLImporter : NSOperation {
#private
id <companyXMLImporterDelegate> __unsafe_unretained delegate;
// Reference to the libxml parser context
xmlParserCtxtPtr context;
NSURLConnection *xmlConnection;
BOOL done;
BOOL parsingAProduct;
// Used for getting character data from XML elements
BOOL storingCharacters;
NSMutableData *characterBuffer;
Product *currentProduct;
NSUInteger countForCurrentBatch;
NSManagedObjectContext *insertionContext;
NSPersistentStoreCoordinator *persistentStoreCoordinator;
NSEntityDescription *productEntityDescription;
CategoryCache *theCache;
NSURL *companyURL;
}
#property (nonatomic, retain) NSURL *companyURL;
#property (nonatomic, assign) id <companyXMLImporterDelegate> delegate;
#property (nonatomic, retain) NSPersistentStoreCoordinator *persistentStoreCoordinator;
#property (nonatomic, retain, readonly) NSManagedObjectContext *insertionContext;
#property (nonatomic, retain, readonly) NSEntityDescription *productEntityDescription;
#property (nonatomic, retain, readonly) CategoryCache *theCache;
- (void)main;
#end
companyXMLImporter.m
#import "companyXMLImporter.h"
#import "Product.h"
#import "Category.h"
#import "CategoryCache.h"
#import <libxml/tree.h>
// Function prototypes for SAX callbacks
static void startElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI, int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes);
static void endElementSAX(void *context, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI);
static void charactersFoundSAX(void *context, const xmlChar *characters, int length);
static void errorEncounteredSAX(void *context, const char *errorMessage, ...);
static xmlSAXHandler simpleSAXHandlerStruct; // Forward reference. Structure defined in full at end of file
#interface companyXMLImporter ()
#property BOOL storingCharacters;
#property (nonatomic, retain) NSMutableData *characterBuffer;
#property BOOL done;
#property BOOL parsingAProduct;
#property NSUInteger countForCurrentBatch;
#property (nonatomic, retain) Product *currentProduct;
#property (nonatomic, retain) NSURLConnection *xmlConnection;
#property (nonatomic, retain) NSDateFormatter *dateFormatter;
#end
static double lookuptime = 0;
#implementation companyXMLImporter
#synthesize companyURL, delegate, persistentStoreCoordinator;
#synthesize xmlConnection, done, parsingAProduct, storingCharacters, currentProduct, countForCurrentBatch, characterBuffer;
- (void)main {
if (delegate && [delegate respondsToSelector:#selector(importerDidSave:)]) {
[[NSNotificationCenter defaultCenter] addObserver:delegate selector:#selector(importerDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.insertionContext];
}
done = NO;
self.characterBuffer = [NSMutableData data];
NSURLRequest *theRequest = [NSURLRequest requestWithURL:companyURL];
xmlConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self]; // create connection with request and start loading data
context = xmlCreatePushParserCtxt(&simpleSAXHandlerStruct, (__bridge void *)(self), NULL, 0, NULL);
if (xmlConnection != nil) {
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (!done);
}
// Release thread resources
xmlFreeParserCtxt(context);
self.characterBuffer = nil;
self.dateFormatter = nil;
self.xmlConnection = nil;
self.currentProduct = nil;
theCache = nil;
NSError *saveError = nil;
NSAssert1([insertionContext save:&saveError], #"Unhandled error saving managed object context in import thread: %#", [saveError localizedDescription]);
if (delegate && [delegate respondsToSelector:#selector(importerDidSave:)]) {
[[NSNotificationCenter defaultCenter] removeObserver:delegate name:NSManagedObjectContextDidSaveNotification object:self.insertionContext];
}
if (self.delegate != nil && [self.delegate respondsToSelector:#selector(importerDidFinishParsingData:)]) {
[self.delegate importerDidFinishParsingData:self];
}
}
- (NSManagedObjectContext *)insertionContext {
if (insertionContext == nil) {
insertionContext = [[NSManagedObjectContext alloc] init];
[insertionContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
}
return insertionContext;
}
- (void)forwardError:(NSError *)error {
if (self.delegate != nil && [self.delegate respondsToSelector:#selector(importer:didFailWithError:)]) {
[self.delegate importer:self didFailWithError:error];
}
}
- (NSEntityDescription *)productEntityDescription {
if (productEntityDescription == nil) {
productEntityDescription = [NSEntityDescription entityForName:#"Product" inManagedObjectContext:self.insertionContext];
}
return productEntityDescription;
}
- (CategoryCache *)theCache {
if (theCache == nil) {
theCache = [[CategoryCache alloc] init];
theCache.managedObjectContext = self.insertionContext;
}
return theCache;
}
- (Product *)currentProduct {
if (currentProduct == nil) {
currentProduct = [[Product alloc] initWithEntity:self.productEntityDescription insertIntoManagedObjectContext:self.insertionContext];
}
return currentProduct;
}
// Forward errors to delegate
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
[self performSelectorOnMainThread:#selector(forwardError:) withObject:error waitUntilDone:NO];
done = YES; // End run loop
}
// Called when a chunk of data has been downloaded
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// Process downloaded chunk of data
xmlParseChunk(context, (const char *)[data bytes], [data length], 0);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// Signal the context that parsing is complete by passing "1" as last parameter
xmlParseChunk(context, NULL, 0, 1);
context = NULL;
done = YES; // End the loop
}
static const NSUInteger kImportBatchSize = 20;
- (void)finishedCurrentProduct {
parsingAProduct = NO;
self.currentProduct = nil;
countForCurrentBatch++;
if (countForCurrentBatch == kImportBatchSize) {
NSError *saveError = nil;
NSAssert1([insertionContext save:&saveError], #"Unhandled error saving managed object context in import thread: %#", [saveError localizedDescription]);
countForCurrentBatch = 0;
}
}
// Character data appended to a buffer until current element ends.
- (void)appendCharacters:(const char *)charactersFound length:(NSInteger)length {
[characterBuffer appendBytes:charactersFound length:length];
}
- (NSString *)currentString {
// Create a string with character data using UTF-8 encoding
NSString *currentString = [[NSString alloc] initWithData:characterBuffer encoding:NSUTF8StringEncoding];
[characterBuffer setLength:0];
return currentString;
}
#end
// XML element names, string lengths for parsing
static const char *kName_App = "App"; // Product container tag
static const NSUInteger kLength_App = 4;
static const char *kName_Sku = "prod_sku";
static const NSUInteger kLength_Sku = 9;
static const char *kName_Name = "prod_name";
static const NSUInteger kLength_Name = 10;
static const char *kName_Description = "prod_description";
static const NSUInteger kLength_Description = 17;
static const char *kName_Category = "prod_category";
static const NSUInteger kLength_Category = 14;
static const char *kName_Upc = "prod_upc";
static const NSUInteger kLength_Upc = 9;
static const char *kName_CountryCode = "prod_code_destination";
static const NSUInteger kLength_CountryCode = 22;
static const char *kName_Webpage = "prod_html_link";
static const NSUInteger kLength_Webpage = 15;
static const char *kName_Manual = "prod_manual";
static const NSUInteger kLength_Manual = 12;
static const char *kName_QuickStart = "prod_quick_start";
static const NSUInteger kLength_QuickStart = 17;
static const char *kName_Thumbnail = "prod_thumbnail";
static const NSUInteger kLength_Thumbnail = 15;
static const char *kName_MainImage = "prod_image_main";
static const NSUInteger kLength_MainImage = 16;
static const char *kName_SecondaryImage = "prod_image_secondary";
static const NSUInteger kLength_SecondaryImage = 21;
// Invoked when importer finds beginning of a node
static void startElementSAX(void *parsingContext, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI,
int nb_namespaces, const xmlChar **namespaces, int nb_attributes, int nb_defaulted, const xmlChar **attributes) {
companyXMLImporter *importer = (__bridge companyXMLImporter *)parsingContext;
if (!strncmp((const char *)localname, kName_App, kLength_App)) {
importer.parsingAProduct = YES;
} else if (importer.parsingAProduct && ((!strncmp((const char *)localname, kName_Sku, kLength_Sku) || !strncmp((const char *)localname, kName_Name, kLength_Name) || !strncmp((const char *)localname, kName_Description, kLength_Description) || !strncmp((const char *)localname, kName_Category, kLength_Category) || !strncmp((const char *)localname, kName_Upc, kLength_Upc) || !strncmp((const char *)localname, kName_CountryCode, kLength_CountryCode) || !strncmp((const char *)localname, kName_Webpage, kLength_Webpage) || !strncmp((const char *)localname, kName_Manual, kLength_Manual) || !strncmp((const char *)localname, kName_QuickStart, kLength_QuickStart) || !strncmp((const char *)localname, kName_Thumbnail, kLength_Thumbnail) || !strncmp((const char *)localname, kName_MainImage, kLength_MainImage) || !strncmp((const char *)localname, kName_SecondaryImage, kLength_SecondaryImage)))
) {
importer.storingCharacters = YES;
}
}
// Invoked when parse reaches end of a node
static void endElementSAX(void *parsingContext, const xmlChar *localname, const xmlChar *prefix, const xmlChar *URI) {
companyXMLImporter *importer = (__bridge companyXMLImporter *)parsingContext;
if (importer.parsingAProduct == NO) return;
if (!strncmp((const char *)localname, kName_App, kLength_App)) {
[importer finishedCurrentProduct];
} else if (!strncmp((const char *)localname, kName_Name, kLength_Name)) {
importer.currentProduct.name = importer.currentString;
} else if (!strncmp((const char *)localname, kName_Category, kLength_Category)) {
double before = [NSDate timeIntervalSinceReferenceDate];
Category *category = [importer.theCache categoryWithName:importer.currentString];
double delta = [NSDate timeIntervalSinceReferenceDate] - before;
lookuptime += delta;
importer.currentProduct.category = category;
} else if (!strncmp((const char *)localname, kName_Sku, kLength_Sku)) {
importer.currentProduct.sku = importer.currentString;
} else if (!strncmp((const char *)localname, kName_Description, kLength_Description)) {
importer.currentProduct.prodDescription = importer.currentString;
} else if (!strncmp((const char *)localname, kName_Upc, kLength_Upc)) {
importer.currentProduct.upc = importer.currentString;
} else if (!strncmp((const char *)localname, kName_CountryCode, kLength_CountryCode)) {
importer.currentProduct.countryCode = importer.currentString;
} else if (!strncmp((const char *)localname, kName_Webpage, kLength_Webpage)) {
importer.currentProduct.webpage = importer.currentString;
} else if (!strncmp((const char *)localname, kName_Manual, kLength_Manual)) {
importer.currentProduct.manual = importer.currentString;
} else if (!strncmp((const char *)localname, kName_QuickStart, kLength_QuickStart)) {
importer.currentProduct.quickStart = importer.currentString;
} else if (!strncmp((const char *)localname, kName_Thumbnail, kLength_Thumbnail)) {
importer.currentProduct.thumbURLString = importer.currentString;
} else if (!strncmp((const char *)localname, kName_MainImage, kLength_MainImage)) {
importer.currentProduct.mainImgURLString = importer.currentString;
} else if (!strncmp((const char *)localname, kName_SecondaryImage, kLength_SecondaryImage)) {
importer.currentProduct.secondaryImgURLString = importer.currentString;
}
importer.storingCharacters = NO;
}
// Invoked when parser encounters character data inside a node
static void charactersFoundSAX(void *parsingContext, const xmlChar *characterArray, int numberOfCharacters) {
companyXMLImporter *importer = (__bridge companyXMLImporter *)parsingContext;
// storingCharacters set when nodes of interest begin/end – determines whether character data handled/ignored
if (importer.storingCharacters == NO) return;
[importer appendCharacters:(const char *)characterArray length:numberOfCharacters];
}
// Error handling
static void errorEncounteredSAX(void *parsingContext, const char *errorMessage, ...) {
// Handle errors as appropriate
NSCAssert(NO, #"Unhandled error encountered during SAX parse.");
}
static xmlSAXHandler simpleSAXHandlerStruct = {
NULL, /* internalSubset */
NULL, /* isStandalone */
NULL, /* hasInternalSubset */
NULL, /* hasExternalSubset */
NULL, /* resolveEntity */
NULL, /* getEntity */
NULL, /* entityDecl */
NULL, /* notationDecl */
NULL, /* attributeDecl */
NULL, /* elementDecl */
NULL, /* unparsedEntityDecl */
NULL, /* setDocumentLocator */
NULL, /* startDocument */
NULL, /* endDocument */
NULL, /* startElement*/
NULL, /* endElement */
NULL, /* reference */
charactersFoundSAX, /* characters */
NULL, /* ignorableWhitespace */
NULL, /* processingInstruction */
NULL, /* comment */
NULL, /* warning */
errorEncounteredSAX, /* error */
NULL,/* fatalError //: unused error() get all the errors */
NULL, /* getParameterEntity */
NULL, /* cdataBlock */
NULL, /* externalSubset */
XML_SAX2_MAGIC, //
NULL,
startElementSAX, /* startElementNs */
endElementSAX, /* endElementNs */
NULL, /* serror */
};
You should not need to call reloadData at all to get the data to display in the table ... and you're calling it twice, in viewDidLoad and in fetch. I would get rid of these.
It does sound like you have a timing problem. Can you explain more about how the data gets into the database? When you say,
In modifying the XML file that this database pulls data from ...
I take it that you mean that when your app starts up, it reads from an XML file which is packaged with your app, and writes this information into Core Data / Sqlite. If that is indeed what you do, then maybe the problem is that this process is trying to run at the same time as your CategoriesViewController is loading. Where is the code which reads the XML into Core Data / Sqlite? Where do you call it from? Is it a synchronous or an asynchronous call?

object is not saved in array

I have the following code that parses json into custom objects, parsing works fine i have a problem with saving the objects.
-(void)handleCities:(NSNotification *)notification
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:#"_getInitialData" object:nil];
if(![notification object])
{
NSLog(#"Something went wrong");
return;
}
// Convert data to a dictionary
NSMutableDictionary * data = [NSJSONSerialization JSONObjectWithData:(NSMutableData *) [notification object] options:NSJSONReadingMutableContainers error:nil];
// Loop over all the data
if([data objectForKey:#"data"] != (id)kCFBooleanFalse)
{
for(int i = 0; i < [[[data objectForKey:#"data"] objectForKey:#"cities"] count]; i++)
{
CityObject * object = [[CityObject alloc] init];
// Loop over all the properties
for(NSString * key in [[[data objectForKey:#"data"] objectForKey:#"cities" ] objectAtIndex:i])
{
NSString * _key = key;
if([object._remap objectForKey:key])
{
NSString * value = [object._remap objectForKey:_key];
NSLog(#"Notice [%#] Remapping \"%#\" to \"%#\"", self.class, _key, value);
_key = value;
}
// Save the value for easy access
NSString * value = [[[[data objectForKey:#"data"] objectForKey:#"cities" ] objectAtIndex:i] objectForKey:_key];
// Does the value have a value
if((id)value == [NSNull null])
{
value = #"";
}
// Set the value for key
[object setValue:value forKey:_key];
}
NSMutableDictionary * set = [[[data objectForKey:#"data"] objectForKey:#"cities"] objectAtIndex:i];
int count = (int)[[set objectForKey:#"categories"] count];
for(int b = 0; b < count; b++)
{
CategoryObject * category = [[CategoryObject alloc] init];
for(NSString * key in [[set objectForKey:#"categories"] objectAtIndex:b])
{
NSString * value = [[[set objectForKey:#"categories"] objectAtIndex:b] objectForKey:key];
if((id)value == [NSNull null])
{
value = #"";
}
[category setValue:value forKey:key];
}
NSLog(#"Category %#",category.class);
NSLog(#"Object : %#",object.class);
NSLog(#"Object Categories : %#",object.categories.class);
// i can print the category object fine here
[object.categories addObject:(CategoryObject *)category];
}
[self.cities addObject:object];
}
}
CityObject * city = [self.cities objectAtIndex:0];
CategoryObject * category = [city.categories objectAtIndex:0];
NSLog(#"Category Name : %#", category.name);
[[NSNotificationCenter defaultCenter] removeObserver:self name:#"_getInitialData" object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:#"getInitialData" object:nil];
}
CityObject.h
#import <Foundation/Foundation.h>
#import "Object.h"
#interface CityObject : Object
#property (nonatomic, strong) NSString * name;
#property (nonatomic, strong) NSString * id;
#property (nonatomic, strong) NSString * state;
#property (nonatomic, strong) NSString * image;
#property (nonatomic, strong) NSString * term_order;
#property (nonatomic, strong) NSMutableArray * categories;
#property (nonatomic, strong) NSMutableDictionary * _remap;
#end
CityObject.m
#import "CityObject.h"
#implementation CityObject
-(id)init
{
if(self = [super init])
{
self._remap = [[NSMutableDictionary alloc] init];
self.categories = [[NSMutableArray alloc] init];
[self._remap setObject:#"state" forKey:#"new_city"];
}
return self;
}
#end
CategoryObject.h
#import <Foundation/Foundation.h>
#import "Object.h"
#interface CategoryObject : Object
#property (nonatomic, strong) NSString * name;
#property (nonatomic, strong) NSString * icon;
#property (nonatomic, strong) NSString * id;
#end
Before i add the CategoryObject to my array i can retrieve the values without a problem it is when i NSLog at the end i cannot get the values / objects out of the array's.
What am i doing wrong since it works great when i add the CityObjects but not when i "parse" the categories. I can read them fine before i add them to the object.categories
Error / Output
-[__NSDictionaryM name]: unrecognized selector sent to instance 0x7a482760
2015-03-24 09:21:05.649 10things[16968:337325] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSDictionaryM name]: unrecognized selector sent to instance 0x7a482760'
Problem solved, i did not have a strong pointer to the model, the Dictionary was magically being replaced to a NSMutableDictionary Once i had a strong handle everything worked properly. (thanks for the code feedback)

Reference NSManagedObject entity from inside NSValueTransformer

I'm using NSValueTranformer to encrypt certain Core Data attributes. This all works fine, except I need to be able to use a different encryption key depending on the NSManagedObject. Is there anyway I can access this entity from within my transformer class?
The use case is I have multiple users with different passwords that can access different NSManagedObject entities. If I use the same encryption key for all of the objects, someone could just reassign who owns them in the SQL db and they would still decrypt.
Any ideas on the best way to go about this?
Edit:
I should mention I'm doing this in iOS.
Third times the charm? Let me see if I can address your only-transform-when-going-to-disk requirement. Think of this as a hybrid of the other two approaches.
#interface UserSession : NSObject
+ (UserSession*)currentSession;
+ (void)setCurrentSession: (UserSession*)session;
- (id)initWithUserName: (NSString*)username andEncryptionKey: (NSData*)key;
#property (nonatomic, readonly) NSString* userName;
#property (nonatomic, readonly) NSData* encryptionKey;
#end
#implementation UserSession
static UserSession* gCurrentSession = nil;
+ (UserSession*)currentSession
{
#synchronized(self)
{
return gCurrentSession;
}
}
+ (void)setCurrentSession: (UserSession*)userSession
{
#synchronized(self)
{
gCurrentSession = userSession;
}
}
- (id)initWithUserName: (NSString*)username andEncryptionKey: (NSData*)key
{
if (self = [super init])
{
_userName = [username copy];
_encryptionKey = [key copy];
}
return self;
}
- (void)dealloc
{
_userName = nil;
_encryptionKey = nil;
}
#end
#interface EncryptingValueTransformer : NSValueTransformer
#end
#implementation EncryptingValueTransformer
- (id)transformedValue:(id)value
{
UserSession* session = [UserSession currentSession];
NSAssert(session, #"No user session! Can't decrypt!");
NSData* key = session.encryptionKey;
NSData* decryptedData = Decrypt(value, key);
return decryptedData;
}
- (id)reverseTransformedValue:(id)value
{
UserSession* session = [UserSession currentSession];
NSAssert(session, #"No user session! Can't encrypt!");
NSData* key = session.encryptionKey;
NSData* encryptedData = Encrypt(value, key);
return encryptedData;
}
#end
The only tricky part here is that you have to be sure that the current UserSession is set up before you create the managed object context and isn't changed until after the context is saved and deallocated.
Hope this helps.
You can create custom instances of NSValueTransformer subclasses that have state (i.e. the encryption key) and pass them in to -bind:toObject:withKeyPath:options: in the options dictionary using the NSValueTransformerBindingOption key.
You won't be able to set this up in IB directly since IB references value transformers by class name, but you can do it in code. If you're feeling extra ambitious you can set up the bindings in IB and then replace them with different options in code later.
It might look something like this:
#interface EncryptingValueTransformer : NSValueTransformer
#property (nonatomic,readwrite,copy) NSData* encryptionKey;
#end
#implementation EncryptingValueTransformer
- (void)dealloc
{
_encryptionKey = nil;
}
- (id)transformedValue:(id)value
{
if (!self.encryptionKey)
return nil;
// do the transformation
return value;
}
- (id)reverseTransformedValue:(id)value
{
if (!self.encryptionKey)
return nil;
// Do the reverse transformation
return value;
}
#end
#interface MyViewController : NSViewController
#property (nonatomic, readwrite, assign) IBOutlet NSControl* controlBoundToEncryptedValue;
#end
#implementation MyViewController
// Other stuff...
- (void)loadView
{
[super loadView];
// Replace IB's value tansformer binding settings (which will be by class and not instance) with specific,
// stateful instances.
for (NSString* binding in [self.controlBoundToEncryptedValue exposedBindings])
{
NSDictionary* bindingInfo = [self.controlBoundToEncryptedValue infoForBinding: binding];
NSDictionary* options = bindingInfo[NSOptionsKey];
if ([options[NSValueTransformerNameBindingOption] isEqual: NSStringFromClass([EncryptingValueTransformer class])])
{
// Out with the old
[self.controlBoundToEncryptedValue unbind: binding];
// In with the new
NSMutableDictionary* mutableOptions = [options mutableCopy];
mutableOptions[NSValueTransformerNameBindingOption] = nil;
mutableOptions[NSValueTransformerBindingOption] = [[EncryptingValueTransformer alloc] init];
[self.controlBoundToEncryptedValue bind: binding
toObject: bindingInfo[NSObservedObjectKey]
withKeyPath: bindingInfo[NSObservedKeyPathKey]
options: mutableOptions];
}
}
}
// Assuming you're using the standard representedObject pattern, this will get set every time you want
// your view to expose new model data. This is a good place to update the encryption key in the transformers'
// state...
- (void)setRepresentedObject:(id)representedObject
{
for (NSString* binding in [self.controlBoundToEncryptedValue exposedBindings])
{
id transformer = [self.controlBoundToEncryptedValue infoForBinding: NSValueBinding][NSOptionsKey][NSValueTransformerBindingOption];
EncryptingValueTransformer* encryptingTransformer = [transformer isKindOfClass: [EncryptingValueTransformer class]] ? (EncryptingValueTransformer*)transformer : nil;
encryptingTransformer.encryptionKey = nil;
}
[super setRepresentedObject:representedObject];
// Get key from model however...
NSData* encryptionKeySpecificToThisUser = /* Whatever it is... */ nil;
for (NSString* binding in [self.controlBoundToEncryptedValue exposedBindings])
{
id transformer = [self.controlBoundToEncryptedValue infoForBinding: NSValueBinding][NSOptionsKey][NSValueTransformerBindingOption];
EncryptingValueTransformer* encryptingTransformer = [transformer isKindOfClass: [EncryptingValueTransformer class]] ? (EncryptingValueTransformer*)transformer : nil;
encryptingTransformer.encryptionKey = encryptionKeySpecificToThisUser;
}
}
// ...Other stuff
#end
OK. This was bugging me so I thought about it some more... I think the easiest way is to have some sort of "session" object and then have a "derived property" on your managed object. Assuming you have an entity called UserData with a property called encryptedData, I whipped up some code that might help illustrate:
#interface UserData : NSManagedObject
#property (nonatomic, retain) NSData * unencryptedData;
#end
#interface UserData () // Private
#property (nonatomic, retain) NSData * encryptedData;
#end
// These functions defined elsewhere
NSData* Encrypt(NSData* clearData, NSData* key);
NSData* Decrypt(NSData* cipherData, NSData* key);
#interface UserSession : NSObject
+ (UserSession*)currentSession;
- (id)initWithUserName: (NSString*)username andEncryptionKey: (NSData*)key;
#property (nonatomic, readonly) NSString* userName;
#property (nonatomic, readonly) NSData* encryptionKey;
#end
#implementation UserData
#dynamic encryptedData;
#dynamic unencryptedData;
+ (NSSet*)keyPathsForValuesAffectingUnencryptedData
{
return [NSSet setWithObject: NSStringFromSelector(#selector(encryptedData))];
}
- (NSData*)unencryptedData
{
UserSession* session = [UserSession currentSession];
if (nil == session)
return nil;
NSData* key = session.encryptionKey;
NSData* encryptedData = self.encryptedData;
NSData* decryptedData = Decrypt(encryptedData, key);
return decryptedData;
}
- (void)setUnencryptedData:(NSData *)unencryptedData
{
UserSession* session = [UserSession currentSession];
NSAssert(session, #"No user session! Can't encrypt!");
NSData* key = session.encryptionKey;
NSData* encryptedData = Encrypt(unencryptedData, key);
self.encryptedData = encryptedData;
}
#end
#implementation UserSession
static UserSession* gCurrentSession = nil;
+ (UserSession*)currentSession
{
#synchronized(self)
{
return gCurrentSession;
}
}
+ (void)setCurrentSession: (UserSession*)userSession
{
#synchronized(self)
{
gCurrentSession = userSession;
}
}
- (id)initWithUserName: (NSString*)username andEncryptionKey: (NSData*)key
{
if (self = [super init])
{
_userName = [username copy];
_encryptionKey = [key copy];
}
return self;
}
-(void)dealloc
{
_userName = nil;
_encryptionKey = nil;
}
#end
The idea here is that when a given user logs in you create a new UserSession object and call +[UserSession setCurrentSession: [[UserSession alloc] initWithUserName: #"foo" andEncryptionKey: <whatever>]]. The derived property (unencryptedData) accessor and mutator get the current session and use the key to transform the values back and forth to the "real" property. (Also, don't skip over the +keyPathsForValuesAffectingUnencryptedData method. This tells the runtime about the relationship between the two properties, and will help things work more seamlessly.)

Resources