Search code examples
iosuitableviewnskeyedarchiver

Some data from object either not saving or not loading


I am working on a simple app to track my daughter's hockey games. My problem is that when I stop and restart the app some of the data doesn't get loaded back in. (I'm checking with a log statement that just shows zero regardless of what was there before.)I'm not sure if the problem is in the loading or the saving.

(Sorry for the long post, but I'm not sure where to look.)

It uses a Games class that looks like this:

#import <Foundation/Foundation.h>

@interface Game : NSObject <NSCoding>//conforms to this protocol so data can be prepared for read/write to file

//used primarily in GameFactsViewController
@property (nonatomic, strong) NSString *opponent;
@property (nonatomic, strong) NSDate *dateOfGame;

//used primarily in PlayerActionsViewController
@property (nonatomic) NSInteger shotsOnGoal;
@property (nonatomic) NSInteger shotsNotOnGoal;
@property (nonatomic) NSInteger passesCompleted;
@property (nonatomic) NSInteger passesNotCompleted;
@property (nonatomic) NSInteger takeaways;
@property (nonatomic) NSInteger giveaways;
@property (nonatomic) NSInteger faceoffsWon;
@property (nonatomic) NSInteger faceoffsLost;
@property (nonatomic) NSInteger shifts;
@property (nonatomic) NSInteger blockedShots;

@end

My problem is that the opponent and dateOfGame properties and getting saved and loaded when I start the app again, but none of the other properties are.

Tha main controller is a tableview controler with each game as a row.The opponent and dateOfGame properties are set in a tableview controller and the others in a view controller inside a tab bar controllers. The seques to these controllers are made from a disclosure indicator for the first and by clicking on the row for the second. These work fine, using this code:

- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    //check for proper seque
    if ([segue.identifier isEqualToString:@"AddGame"]) {

        //identify the top level view controller holding the GameFactsVC (see storyboard)
        UINavigationController *navigationController = segue.destinationViewController;

        //go down one level to get the GFVC
        GameFactsViewController *controller = (GameFactsViewController *) navigationController.topViewController;

        //set the current controller (the HSVC) as the delegate for the GFVC
        controller.delegate = self;

    } else if ([segue.identifier isEqualToString:@"EditGame"]) {

        //as above to get to the right place
        UINavigationController *navigationController = segue.destinationViewController;
        GameFactsViewController *controller = (GameFactsViewController *) navigationController.topViewController;
        controller.delegate = self;

        //"sender" here is what was clicked, the detail disclosure icon
        //this identifies what game data to load to edit
        NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
        controller.gameToEdit = _games[indexPath.row];

    } else if ([segue.identifier isEqualToString:@"GameDetails"]) {

        UITabBarController *tabBarController = segue.destinationViewController;
        PlayerActionsViewController *controller = (PlayerActionsViewController *)[[tabBarController viewControllers] objectAtIndex:0];
        controller.delegate = self;

        NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];

        controller.gameToEditPerformance = _games[indexPath.row];
        NSLog(@"Data passed %ld", (long)controller.gameToEditPerformance.shotsOnGoal);

    }
}

I am returning to the main controller with this method for the opponent and dateOfGame and I can log the data coming back.

-(IBAction) done
{
    //Use delegate to capture entered data and pass to HSVC
    //methods here are delegate methods listed above and defined in the HSVC

    //see if the data was empty when view loaded
    if (self.gameToEdit == nil) {

        Game *game = [[Game alloc] init];
        game.opponent = self.opponentField.text;
        game.dateOfGame = [self convertToDate:self.dateLabel.text withFormat:@"MMM d, yyyy"];


        [self.delegate gameFactsViewController:self didFinishAddingGame:game];

    } else {
        self.gameToEdit.opponent = self.opponentField.text;//updates data model; will update display in delegate method
        self.gameToEdit.dateOfGame = [self convertToDate:self.dateLabel.text withFormat:@"MMM d, yyyy"];

        [self.delegate gameFactsViewController: self didFinishEditingGame:self.gameToEdit];
    }


}

Similarly, I am passing the rest of the data back from the other controller with this line at the end of a long method that sets the data values and I can log the correct data coming back here, too.

[self.delegate playerActionsViewController:self didEditGameData:self.gameToEditPerformance];

I'm calling the Save method with these two similar methods. (The key difference as I see it as I have updated the display in the other table already, while I still need to update the main view.)

- (void) gameFactsViewController:(GameFactsViewController *)controller didFinishEditingGame:(Game *)game
{
    //edit already made in data model in GFVC
    //need to update display

    NSInteger index = [_games indexOfObject: game];//locate the item being edited in the games array
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
    UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];//get the right cell

    [self configureDataForCell:cell withGame:game];//puts the game data into the labels in the cell

    [self saveGames];//write the current data to the file

    [self dismissViewControllerAnimated:YES completion:nil];

}

- (void) playerActionsViewController: (PlayerActionsViewController *) controller didEditGameData: (Game *) game
{
    NSLog(@"Paased back shots %ld", (long) game.shotsOnGoal);
    [self saveGames];


}

Now for data saving and loading. I'm using this code:

- (void) loadGames
{
    NSString *path = [self dataFilePath];//for convenience below

    //if there is already a data file, unarchive/decode and load games array
    //else create an empty arry to hold games
    if ([[NSFileManager defaultManager] fileExistsAtPath: path]) {

        NSData *data = [[NSData alloc] initWithContentsOfFile: path];//data structure created and loaded with file data
        NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData: data];//archiver created and connected to data

        _games = [unarchiver decodeObjectForKey:@"Games"];

        [unarchiver finishDecoding];//data now in games array

    } else {

        _games = [[NSMutableArray alloc] initWithCapacity:50];
    }
}

- (void) saveGames
{
    NSMutableData *data = [[NSMutableData alloc] init];//data structure to hold the data to be saved after encoding
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];

    [archiver encodeObject:_games forKey:@"Games"];//I believe key here needs to match class name that will be saved. It tells archiver how to encode object properly

    [archiver finishEncoding];//finish encoding, with data now in data structure

    [data writeToFile:[self dataFilePath] atomically:YES];//write data structure to file determined above

}

Somewhere here there is a difference in the _games array, I guess, between the two situations, but I'm not seeing it. Or it's some other problem.

Thanks.


Solution

  • The problem was in my Games class. I forgot to include the keys to encode and decode the NSIntegers using

    [aCoder encodeInteger:self.shotsOnGoal forKey:@"ShotsOnGoal"];
    self.shotsOnGoal = [aDecoder decodeIntegerForKey:@"ShotsOnGoal"];
    

    Etc.