Persistency with NSKeyedArchiver

Discussion in 'iOS Development' started by Axis, Feb 20, 2010.

  1. Axis

    Axis Super Moderator Staff Member

    Joined:
    Dec 2, 2007
    Messages:
    6,288
    Likes Received:
    133
    Device:
    iPhone 4S (White)
    When developing an application, there is a good chance you’ll want to have some sort of persistency (saving names, passwords, scores, etc.). NSUserDefaults provides a simple way to safely store that information. However, you may need a more robust solution. Your logical data abstraction should not have to conform to limits of the persistency mechanism; you need a solution that is flexible enough to archive all of your objects, not just strings, arrays, and dictionaries. That’s where NSKeyed(Un)Archiver comes in.

    Here’s the deal: we’re working on a project for athletic coach. He needs a solution for archiving/retrieving stats for his athletes. After discussion the specifications, we’ve broken down our abstraction.

    We have a ScoreCard class, that will hold an athlete’s best time, and an array of all his scores.

    We have an Athlete class, that contains athlete-specific information, and a ScoreCard instance.

    We have a Roster class, that contains some roster-specific information, along with an array of Athlete instances.

    Here’s the code for our simple classes:

    [objc]
    // ScoreCard.h
    #import <Foundation/Foundation.h>

    @interface ScoreCard : NSObject <NSCoding> {

    NSString *bestTime;
    NSMutableArray *allTimes;

    }

    @property (copy) NSString *bestTime;
    @property (copy) NSMutableArray *allTimes;

    // other methods not relevant to this tutorial go here

    @end

    @implementation ScoreCard

    @synthesize bestTime, allTimes;

    - (id)init {

    if (self = [super init]) {

    bestTime = [[NSString alloc] init];
    allTimes = [[NSMutableArray alloc] init];

    }

    return self;

    }

    - (id)initWithCoder

    Please Register or Log in to view images

    NSCoder *)aDecoder {

    if (self = [super init]) {

    bestTime = [[aDecoder decodeObjectForKey

    Please Register or Log in to view images

    "bestTime"] retain];
    allTimes = [[aDecoder decodeObjectForKey

    Please Register or Log in to view images

    "allTimes"] retain];

    }

    return self;

    }

    - (void)encodeWithCoder

    Please Register or Log in to view images

    NSCoder *)aCoder {

    [aCoder encodeObject:bestTime forKey

    Please Register or Log in to view images

    "bestTime"];
    [aCoder encodeObject:allTimes forKey

    Please Register or Log in to view images

    "allTimes"];

    }

    - (void)dealloc {

    [bestTime release];
    [allTimes release];
    [super dealloc];

    }

    @end
    [/objc]

    Our class is conforms to the NSCoding protocol, which declares two method—initWithCoder: / encodeWithCoder:—which we have implemented.

    The ScoreCard object must ensure that its instance variables are encoded when it is to be encoded. As you can see, when our ScoreCard objects handle that in [encodeWithCoder:]

    Also present is the [initWithCoder:] method. Here, ScoreCard objects initialize their instance variables according to the information in the passed NSCoder argument.

    This is an elegant solution; we don't have to concern ourselves with the actual encoding/decoding process, and ScoreCard objects and their instance variables can be treated as regular objects.

    You may have some questions as to when these encode/decode methods are invoked. Just look at the following code for the rest of our objects, and it will be clear.

    [objc]
    // Athlete.h
    #import <Foundation/Foundation.h>

    @interface Athlete : NSObject <NSCoding> {

    NSString *name;
    NSString *bio;
    NSString *phoneNumber;
    ScoreCard *scoreCard;
    BOOL eligible;

    }

    @property (copy) NSString *name, *bio, *phoneNumber;
    @property (retain) ScoreCard *scoreCard;
    @property (getter=isEligible) BOOL eligible;

    - (void)print;

    @end


    @implementation Athlete

    @synthesize name, bio, phoneNumber, scoreCard, eligible;

    - (id)init {

    if (self = [super init]) {

    name = [[NSString alloc] init];
    bio = [[NSString alloc] init];
    phoneNumber = [[NSString alloc] init];
    scoreCard = [[ScoreCard alloc] init];
    eligible = YES;

    }

    return self;
    }

    - (id)initWithCoder

    Please Register or Log in to view images

    NSCoder *)aDecoder {

    if (self = [super init]) {

    name = [[aDecoder decodeObjectForKey

    Please Register or Log in to view images

    "name"] retain];
    bio = [[aDecoder decodeObjectForKey

    Please Register or Log in to view images

    "bio"] retain];
    phoneNumber = [[aDecoder decodeObjectForKey

    Please Register or Log in to view images

    "phoneNumber"] retain];
    scoreCard = [[aDecoder decodeObjectForKey

    Please Register or Log in to view images

    "scoreCard"] retain];
    eligible = [aDecoder decodeBoolForKey

    Please Register or Log in to view images

    "eligible"];

    }

    return self;

    }

    - (void)encodeWithCoder

    Please Register or Log in to view images

    NSCoder *)aCoder {

    [aCoder encodeObject:name forKey

    Please Register or Log in to view images

    "name"];
    [aCoder encodeObject:bio forKey

    Please Register or Log in to view images

    "bio"];
    [aCoder encodeObject

    Please Register or Log in to view images

    honeNumber forKey

    Please Register or Log in to view images

    "phoneNumber"];
    [aCoder encodeObject:scoreCard forKey

    Please Register or Log in to view images

    "scoreCard"];
    [aCoder encodeBool:eligible forKey

    Please Register or Log in to view images

    "eligible"];

    }

    - (void)print {

    NSLog(@"Name: %@\nBio: %@\nTel: %@\n\nBest Time: %@\n\nAll Times:", name, bio, phoneNumber, [scoreCard bestTime]);
    for (NSString *time in [scoreCard allTimes])
    NSLog(@"%@", time);

    }

    - (void)dealloc {

    [name release];
    [bio release];
    [phoneNumber release];
    [scoreCard release];
    [super dealloc];

    }
    @end
    [/objc]

    Nothing new there really, except for the BOOL ivar. Just note the slight change in our encode/decode methods to handle this primitive data type.

    I wrote a quick printing routine, so we can easily test this out later.

    Also, as you can see, Athlete objects have a ScoreCard instance variable. When an Athlete object gets encoded/decoded, so does the ScoreCard instance variable (all ivars do, but I bring this up because it is a custom object).

    [objc]
    // Roster.h
    #import <Foundation/Foundation.h>

    @interface Roster : NSObject <NSCoding> {

    NSMutableArray *athletes;
    int rank;

    }

    @property (retain) NSMutableArray *athletes;
    @property int rank;

    - (void)print;
    - (void)addAthlete

    Please Register or Log in to view images

    Athlete *)athlete;

    @end

    @implementation Roster

    @synthesize rank, athletes;

    - (id)init {

    if (self = [super init]) {

    rank = 0;
    athletes = [[NSMutableArray alloc] init];

    }

    return self;

    }

    - (id)initWithCoder

    Please Register or Log in to view images

    NSCoder *)aDecoder {

    if (self = [super init]) {

    athletes = [[aDecoder decodeObjectForKey

    Please Register or Log in to view images

    "athletes"] retain];
    rank = [aDecoder decodeIntForKey

    Please Register or Log in to view images

    "rank"];

    }

    return self;

    }

    - (void)encodeWithCoder

    Please Register or Log in to view images

    NSCoder *)aCoder {

    [aCoder encodeObject:athletes forKey

    Please Register or Log in to view images

    "athletes"];
    [aCoder encodeInt:rank forKey

    Please Register or Log in to view images

    "rank"];

    }

    - (void)addAthlete

    Please Register or Log in to view images

    Athlete *)athlete {

    [athletes addObject:athlete];

    }

    - (void)print {

    NSLog(@"Roster info:\nRank: %d", rank);
    for (Athlete *athlete in athletes)
    NSLog(@"%@", [athlete name]);

    }

    - (void)dealloc {

    [athletes release];
    [super dealloc];
    }

    @end
    [/objc]

    There, same deal; we handle an int this time, and I included a routine for printing player names.

    Now for a test run.

    [objc]
    static NSString *names [] = { @"Jeff Beck", @"Eric Clapton", @"Angus Young", @"John Doe", @"Jane Doe", @"Shaun White", @"Flavius Josephus" };

    // function to create a roster. in real life, this wouldn't be used, but we're just testing now
    Roster * create() {

    NSMutableArray *scoresArray = [NSMutableArray arrayWithObjects

    Please Register or Log in to view images

    "15:09:34", @"17:54:01", @"19:56:08", nil];

    Roster *roster = [[Roster alloc] init];
    for (int i = 0; i < 7; ++i) {

    Athlete *athlete = [[Athlete alloc] init];
    [athlete setName:names];
    [athlete setBio

    Please Register or Log in to view images

    "I'm a boss"];
    [athlete setPhoneNumber

    Please Register or Log in to view images

    "867-5309"];
    [athlete.scoreCard setBestTime

    Please Register or Log in to view images

    "12:30:34"];
    [athlete.scoreCard setAllTimes:scoresArray];

    [roster addAthlete:athlete];

    }

    return [roster autorelease];

    }

    int main (int argc, char **argv) {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    // create and archive a roster
    Roster *roster = create();
    [NSKeyedArchiver archiveRootObject:roster toFile

    Please Register or Log in to view images

    "/roster.archive"];

    // unarchive roster
    // Roster *roster = [NSKeyedUnarchiver unarchiveObjectWithFile

    Please Register or Log in to view images

    "/roster.archive"];
    //
    // [roster print];
    //
    // for (Athlete *athlete in [roster athletes])
    // [athlete print];

    [pool drain];
    return 0;
    }
    [/objc]

    Run the program the first time, and a Roster object will be created, populated, and archived.

    Uncomment a few lines, and you can create a Roster object from the archive.

    Please Register or Log in to view images



    It's late, I'm tired, and I realize there's more code than actual explanation. I don't think this is terribly complicated, but perhaps I missed something or should go more in-depth. Just let me know.

    Attached is an XCode project built from a Foundation-linked command line tool (template)

    Attached Files:

  2. lauNchD

    lauNchD Well-Known Member

    Joined:
    Jan 27, 2008
    Messages:
    1,844
    Likes Received:
    261
    Device:
    iPhone 5 (Black)
    Nice tutorial!

    NSKeyed(Un)Archiver works great for "small" (~ < 1000) object graphs, it basically does all the dirty work!
    I already knew how to use it, but this tutorial should help many beginners get started with persistency.
  3. bddckr

    bddckr Active Member

    Joined:
    Dec 2, 2007
    Messages:
    1,434
    Likes Received:
    18
    Device:
    iPhone 4 (Black)
    I agree, nice tut!
    If one's working with more than ~1000 one should have a look at Core Data. (In fact Core Data works for sure with "small" object graphs, but if you're looking for relationships between the objects Core Data will become the number one thing to use.

    Please Register or Log in to view images

    )
  4. daconcerror

    daconcerror Banned

    Joined:
    Sep 6, 2008
    Messages:
    2,898
    Likes Received:
    0
    Device:
    3G iPod touch
    thanks for this guide

    Please Register or Log in to view images

    i needed it
  5. lauNchD

    lauNchD Well-Known Member

    Joined:
    Jan 27, 2008
    Messages:
    1,844
    Likes Received:
    261
    Device:
    iPhone 5 (Black)
    I agree, too.

    Basically, if you want to have an efficient database system integrated with Cocoa objects, Core Data is the way to go.
    However, archiving is sometimes good for capturing and recreating any kind of scenario involving any kind of object (as long as it supports coding), because it automatically sets up the object graph for you once you've got a root object. In addition, UIViews support automatic keyed archiving (that's actually what Interface Builder uses)!

Share This Page