I'm pretty new to this, so bear with me.
I built an app using iOS Programming: The Big Nerd Ranch Guide 4th ed called Homepwner which utilizes archiving to store information when the app goes into the background and load information when it starts up again, and it works totally fine. I decided to implement the same strategy for my own app called SBL, but when the app is loaded after having been quit, the data appears to load but doesn't display properly.
The app displays an array of "CCIEvent"s in a table and they appear like the example:
7:00 AM: Wet and Dirty Diaper
8:48 AM: Fed - Both - 20 min
9:48 AM: Napped - 1 hr 0 min
But when I quit the app and start it again, the list pulls up like this (I had to identify it as code to make this post, but the info below displays on screen in my table as such):
<CCIEvent: 0x154e44e20>
<CCIEvent: 0x154e2c7b0>
<CCIEvent: 0x154e77b90>
I really don't know where to look, but here's all the code relevant to saving/loading (I think):
In CCIEvent.m
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:self.startTime forKey:@"startTime"];
[aCoder encodeObject:self.bedTime forKey:@"bedTime"];
[aCoder encodeObject:self.wakeTime forKey:@"wakeTime"];
[aCoder encodeBool:self.isNap forKey:@"isNap"];
[aCoder encodeInt:self.feedDuration forKey:@"feedDuration"];
[aCoder encodeInt:self.sourceIndex forKey:@"sourceIndex"];
[aCoder encodeInt:self.diaperIndex forKey:@"diaperIndex"];
[aCoder encodeObject:self.note forKey:@"note"];
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if (self) {
_startTime = [aDecoder decodeObjectForKey:@"startTime"];
_bedTime = [aDecoder decodeObjectForKey:@"bedTime"];
_wakeTime = [aDecoder decodeObjectForKey:@"wakeTime"];
_isNap = [aDecoder decodeBoolForKey:@"isNap"];
_feedDuration = [aDecoder decodeIntForKey:@"feedDuration"];
_sourceIndex = [aDecoder decodeIntForKey:@"sourceIndex"];
_diaperIndex = [aDecoder decodeIntForKey:@"diaperIndex"];
_note = [aDecoder decodeObjectForKey:@"note"];
}
return self;
}
In CCIEventStore.m
- (instancetype)initPrivate
{
self = [super init];
if (self) {
NSString *path = [self eventArchivePath];
_privateEvents = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
// If the array hadn't been saved previously, create an empty one
if (!_privateEvents) {
_privateEvents = [[NSMutableArray alloc] init];
NSLog(@"Did NOT load events");
} else {
NSLog(@"Loaded events");
}
}
return self;
}
- (CCIEvent *)createEventWithEventType:(NSString *)eventType
startTime:(NSDate *)startTime
bedTime:(NSDate *)bedTime
wakeTime:(NSDate *)wakeTime
isNap:(BOOL)isNap
feedDuration:(int)feedDuration
sourceIndex:(int)sourceIndex
diaperIndex:(int)diaperIndex
note:(NSString *)note
{
CCIEvent *event = [[CCIEvent alloc] initWithEventType:eventType
startTime:startTime
bedTime:bedTime
wakeTime:wakeTime
isNap:isNap
feedDuration:feedDuration
sourceIndex:sourceIndex
diaperIndex:diaperIndex
note:note];
[self.privateEvents addObject:event];
return event;
}
- (NSString *)eventArchivePath
{
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentDirectory = [documentDirectories firstObject];
return [documentDirectory stringByAppendingPathComponent:@"events.archive"];
}
- (BOOL)saveChanges
{
NSString *path = [self eventArchivePath];
return [NSKeyedArchiver archiveRootObject:self.privateEvents
toFile:path];
}
In AppDelegate.m
- (void)applicationDidEnterBackground:(UIApplication *)application {
BOOL success = [[CCIEventStore sharedStore] saveChanges];
if (success) {
NSLog(@"Saved all of the CCIEvents");
} else {
NSLog(@"Could not save any of the CCIEvents");
}
}
This is the some code from the view controller where the table is which I have called CCILogViewController.m:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
[dateFormatter setDateStyle:NSDateFormatterShortStyle];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"
forIndexPath:indexPath];
CCIEvent *event = self.sortedAndFilteredArray[indexPath.row];
if ([event.eventType isEqualToString:@"sleepEvent"] && event.wakeTime) {
NSDateComponents *dateComponentsToday = [[NSCalendar currentCalendar] components:NSCalendarUnitYear fromDate:[NSDate date]];
NSInteger yearToday = [dateComponentsToday year];
NSInteger dayOfYearToday = [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitDay
inUnit:NSCalendarUnitYear
forDate:[NSDate date]];
NSDateComponents *dateComponentsEvent = [[NSCalendar currentCalendar] components:NSCalendarUnitYear
fromDate:event.startTime];
NSInteger yearEvent = [dateComponentsEvent year];
NSInteger dayOfYearEvent = [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitDay
inUnit:NSCalendarUnitYear
forDate:event.startTime];
NSInteger dayOfYearWakeEvent;
dayOfYearWakeEvent = [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitDay
inUnit:NSCalendarUnitYear
forDate:event.wakeTime];
if (event.wakeTime && yearEvent == yearToday && dayOfYearEvent == dayOfYearToday - 1 && dayOfYearWakeEvent == dayOfYearToday) {
cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", [dateFormatter stringFromDate:event.startTime], [self.sortedAndFilteredArray[indexPath.row] description]];
} else if (event.wakeTime && yearEvent == yearToday - 1 && dayOfYearEvent == dayOfYearToday - 1 && dayOfYearEvent == 1 && dayOfYearWakeEvent == dayOfYearToday) {
cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", [dateFormatter stringFromDate:event.startTime], [self.sortedAndFilteredArray[indexPath.row] description]];
} else {
cell.textLabel.text = [self.sortedAndFilteredArray[indexPath.row] description];
}
} else if (self.dateOptionIndex == 2 || self.dateOptionIndex == 3) {
cell.textLabel.text = [NSString stringWithFormat:@"%@ %@", [dateFormatter stringFromDate:event.startTime], [self.sortedAndFilteredArray[indexPath.row] description]];
} else {
cell.textLabel.text = [self.sortedAndFilteredArray[indexPath.row] description];
}
if (event.note) {
cell.accessoryType = UITableViewCellAccessoryDetailButton;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
Here's how I get the sortedAndFilteredArray which is called in few different places including viewWillAppear:
- (void)sortAndFilterArray
{
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"startTime" ascending:self.arrayIsAscending];
NSArray *sortedArray = [[[CCIEventStore sharedStore] allEvents] sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];
NSMutableArray *sortedAndFilteredArray = [[NSMutableArray alloc] init];
NSMutableArray *finalArray = [[NSMutableArray alloc] init];
for (CCIEvent *event in sortedArray) {
switch (self.eventOptionIndex) {
case 1:
if ([event.eventType isEqualToString:@"sleepEvent"]) {
[sortedAndFilteredArray addObject:event];
}
break;
case 2:
if ([event.eventType isEqualToString:@"feedEvent"]) {
[sortedAndFilteredArray addObject:event];
}
break;
case 3:
if ([event.eventType isEqualToString:@"diaperEvent"]) {
[sortedAndFilteredArray addObject:event];
}
break;
default:
[sortedAndFilteredArray addObject:event];
break;
}
}
NSDateComponents *dateComponentsToday = [[NSCalendar currentCalendar] components:NSCalendarUnitYear fromDate:[NSDate date]];
NSInteger yearToday = [dateComponentsToday year];
NSInteger dayOfYearToday = [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitDay
inUnit:NSCalendarUnitYear
forDate:[NSDate date]];
for (CCIEvent *event in sortedAndFilteredArray) {
NSDateComponents *dateComponentsEvent = [[NSCalendar currentCalendar] components:NSCalendarUnitYear
fromDate:event.startTime];
NSInteger yearEvent = [dateComponentsEvent year];
NSInteger dayOfYearEvent = [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitDay
inUnit:NSCalendarUnitYear
forDate:event.startTime];
NSInteger dayOfYearWakeEvent;
switch (self.dateOptionIndex) {
case 0:
dayOfYearWakeEvent = [[NSCalendar currentCalendar] ordinalityOfUnit:NSCalendarUnitDay
inUnit:NSCalendarUnitYear
forDate:event.wakeTime];
// Filter here for dateIndex 0 (Today)
if ([event.eventType isEqualToString:@"sleepEvent"] && event.wakeTime && yearEvent == yearToday && dayOfYearEvent == dayOfYearToday - 1 && dayOfYearWakeEvent == dayOfYearToday) {
[finalArray addObject:event];
} else if ([event.eventType isEqualToString:@"sleepEvent"] && event.wakeTime && yearEvent == yearToday - 1 && dayOfYearEvent == dayOfYearToday - 1 && dayOfYearEvent == 1 && dayOfYearWakeEvent == dayOfYearToday) {
[finalArray addObject:event];
} else if (yearEvent == yearToday && dayOfYearEvent == dayOfYearToday) {
[finalArray addObject:event];
}
break;
case 1:
// Filter here for dateIndex 1 (Yesterday)
if (yearEvent == yearToday && dayOfYearEvent == dayOfYearToday - 1) {
[finalArray addObject:event];
} else if (yearEvent == yearToday - 1 && dayOfYearEvent == dayOfYearToday - 1 && dayOfYearEvent == 1) {
[finalArray addObject:event];
}
break;
case 2:
// Filter here for dateIndex 2 (Past Week)
if (yearEvent == yearToday && dayOfYearEvent >= dayOfYearEvent - 6) {
[finalArray addObject:event];
} else if (yearEvent == yearToday - 1 && dayOfYearEvent >= dayOfYearToday - 6 && dayOfYearEvent < 7) {
[finalArray addObject:event];
}
break;
default:
// No filter here for dateIndex 3 (All Time)
[finalArray addObject:event];
break;
}
}
self.sortedAndFilteredArray = [finalArray copy];
}
This is what is being called when getting description of a CCIEvent:
- (NSString *)description
{
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setTimeStyle:NSDateFormatterShortStyle];
[dateFormatter setDateStyle:NSDateFormatterNoStyle];
if ([self.eventType isEqualToString:@"sleepEvent"]) {
NSString *bedTimeString = [dateFormatter stringFromDate:self.bedTime];
NSString *wakeTimeString = [dateFormatter stringFromDate:self.wakeTime];
long min = self.sleepDuration/60;
long hrs = self.sleepDuration/60/60;
long rMinutes = min - hrs * 60;
if (!self.wakeTime && self.bedTime) {
if (self.isNap) {
return [[NSString alloc] initWithFormat:@"%@: Went Down for Nap", bedTimeString];
} else {
return [[NSString alloc] initWithFormat:@"%@: Went Down", bedTimeString];
}
} else if (self.wakeTime && !self.bedTime) {
if (self.isNap) {
return [[NSString alloc] initWithFormat:@"%@: Woke from Nap", wakeTimeString];
} else {
return [[NSString alloc] initWithFormat:@"%@: Woke", wakeTimeString];
}
} else if (self.sleepDuration <= 60) {
if (self.isNap) {
return [[NSString alloc] initWithFormat:@"%@: Napped - 1 min", bedTimeString];
} else {
return [[NSString alloc] initWithFormat:@"%@: Slept - 1 min", bedTimeString];
}
} else if (self.sleepDuration > 60 && self.sleepDuration < 60 * 60){
if (self.isNap) {
return [[NSString alloc] initWithFormat:@"%@: Napped - %ld min", bedTimeString, min];
} else {
return [[NSString alloc] initWithFormat:@"%@: Slept - %ld min", bedTimeString, min];
}
} else if (hrs == 1) {
if (self.isNap) {
return [[NSString alloc] initWithFormat:@"%@: Napped - 1 hr %ld min", bedTimeString, rMinutes];
} else {
return [[NSString alloc] initWithFormat:@"%@: Slept - 1 hr %ld min", bedTimeString, rMinutes];
}
} else {
if (self.isNap) {
return [[NSString alloc] initWithFormat:@"%@: Napped - %ld hrs %ld min", bedTimeString, hrs, rMinutes];
} else {
return [[NSString alloc] initWithFormat:@"%@: Slept - %ld hrs %ld min", bedTimeString, hrs, rMinutes];
}
}
} else if ([self.eventType isEqualToString:@"feedEvent"]) {
NSString *startTimeString = [dateFormatter stringFromDate:self.startTime];
NSArray *sourceArray = @[@"Both", @"Left", @"Right", @"Bottle"];
NSString *sourceString = sourceArray[self.sourceIndex];
return [[NSString alloc] initWithFormat:@"%@: Fed - %@ - %d min", startTimeString, sourceString, self.feedDuration / 60];
} else if ([self.eventType isEqualToString:@"diaperEvent"]) {
NSString *startTimeString = [dateFormatter stringFromDate:self.startTime];
NSArray *diaperArray = @[@"Wet and Dirty", @"Wet", @"Dirty"];
NSString *diaperString = diaperArray[self.diaperIndex];
return [[NSString alloc] initWithFormat:@"%@: %@ Diaper", startTimeString, diaperString];
}
return [super description];
}
I sprinkled my code with NSLogs and determined that my CCIEvents were loading just fine. I found it odd that my special description worked before loading but not after loading. Before loading it would give me the string I wanted, but after loading it would point to memory. After inspecting my description method, I noticed that I defaulted to returning the super's description method unless the CCIEvent met certain criteria, which forced me to realize that my CCIEvents have an attribute of eventType which I did not include in the encodeWithCoder: or initWithCoder: blocks. All I had to do was add eventType in there, and my description works like it should. Thanks so much, Chris! I literally left this project alone for months because I was so stumped.