Objective-C

Using Runtime to Implement Automatic Archiving and Unarchiving

By Seren  |  06 Sep, 2026  |  Leave a comment


Writing encodeWithCoder and initWithCoder by hand for every model property is tedious and error-prone. The Runtime offers a way to automate this entirely. If a model had a dozen properties, you’d have to write over a dozen almost identical lines in encodeWithCoder: and initWithCoder:. If you had dozens of models, that was hundreds of lines of repetitive code. Even worse — if you later added a property to the model, you had to remember to add a line in both methods. Miss one and you break the archive.

Later, after digging into the Runtime source code, I realized you could use class_copyIvarList to dynamically fetch all member variables of a class at runtime, and automatically complete archiving and unarchiving. After that, the archiving and unarchiving code for all models became standardized.

The Pain of the Traditional Approach

Here’s what typical code looks like. Suppose there’s a Person class with name, age, and address:

@interface Person : NSObject <NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, copy) NSString *address;
@end

@implementation Person
- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeInteger:self.age forKey:@"age"];
    [aCoder encodeObject:self.address forKey:@"address"];
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.age = [aDecoder decodeIntegerForKey:@"age"];
        self.address = [aDecoder decodeObjectForKey:@"address"];
    }
    return self;
}
@end

With three properties it’s manageable. By the time you hit ten, it’s a drag. And the keys must match the property names exactly — a typo and the data doesn’t load.

The Runtime Approach: Using class_copyIvarList to Get All Properties Automatically

The Runtime provides class_copyIvarList, which gets all member variables of a class — including private variables and the _xxx variables automatically generated by @property.

Ivar *class_copyIvarList(Class cls, unsigned int *outCount);

After getting all member variables, use ivar_getName to get the variable name as the key, then use KVC’s valueForKey: and setValue:forKey: to read and write values. You can completely avoid manually writing archiving and unarchiving code for each property.

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(ivars);  // Don't forget to release the memory
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (unsigned int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
    }
    return self;
}

This removes repeated enumeration, but it does not remove schema maintenance. A new field should be reviewed before it is persisted, and long-lived archives need stable keys, type validation, and a migration strategy. The basic decoder below is suitable for explaining the Runtime loop; production code should prefer NSSecureCoding and explicit allowed classes.

Handling Superclass Properties: Traversing Up with a while Loop

The version above has a limitation: it only handles the current class’s member variables — it doesn’t handle superclass properties. If a class has a superclass, those properties should be archived and unarchived too.

The solution is to use a while loop to traverse from the current class all the way up to NSObject:

- (void)encodeWithCoder:(NSCoder *)aCoder {
    Class cls = self.class;
    while (cls && cls != [NSObject class]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList(cls, &count);
        for (unsigned int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            id value = [self valueForKey:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
        cls = [cls superclass];
    }
}

The unarchiving method works the same way. After this, all properties in the superclass are automatically archived and unarchived — no need to repeat implementations in each subclass.

Ignoring Certain Properties: Optional Filtering

In some cases, you might not want certain properties archived — like a temporary cache property, or a system object that can’t be archived. In those cases, add a filtering mechanism.

// Implement this method in the Model to return a list of property names to ignore
- (NSArray *)ignoredNames {
    return @[@"_tempCache", @"_session"];
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    Class cls = self.class;
    NSArray *ignored = [self respondsToSelector:@selector(ignoredNames)] 
                        ? [self ignoredNames] : nil;
    while (cls && cls != [NSObject class]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList(cls, &count);
        for (unsigned int i = 0; i < count; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            if ([ignored containsObject:key]) continue;
            id value = [self valueForKey:key];
            [aCoder encodeObject:value forKey:key];
        }
        free(ivars);
        cls = [cls superclass];
    }
}

Wrapping in Macros: One Line for All Models

Even with this, writing encodeWithCoder: and initWithCoder: every time is still a bit repetitive. You can wrap it in an NSObject category and have the specific model call the category methods:

// NSObject+Archive.h
@interface NSObject (Archive)
- (void)autoEncodeWithCoder:(NSCoder *)aCoder;
- (void)autoDecodeWithCoder:(NSCoder *)aDecoder;
- (NSArray *)ignoredNames;  // Optional ignore list
@end

Then in the specific Model:

- (void)encodeWithCoder:(NSCoder *)aCoder {
    [self autoEncodeWithCoder:aCoder];
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        [self autoDecodeWithCoder:aDecoder];
    }
    return self;
}

If even that feels like too much, define a macro that expands both methods.

Important Considerations

class_copyIvarList gets member variables, not properties. Member variable names in Objective-C typically have underscores, like _name and _age. When using KVC’s valueForKey: and setValue:forKey:, passing underscored keys is valid — KVC automatically matches them to the corresponding property.

C memory management: The array returned by class_copyIvarList needs to be freed with free() — it’s a C-level API not managed by ARC. Common practice is to call free(ivars) immediately after the loop to prevent memory leaks.

Superclass private variables: class_copyIvarList only gets member variables of the current class — superclass member variables are not included. So you need to use a while loop to traverse up the superclass chain.

Don’t archive NSObject‘s variables: NSObject doesn’t have any member variables worth archiving, so stop at cls != [NSObject class].

Summary

Runtime enumeration can reduce repetitive coding by discovering selected ivars and producing stable keys, while an explicit superclass boundary prevents framework internals from being archived. It is not automatically stable or complete: secure class allowlists, error handling, field review, and migration tests remain part of the design. The runnable project below demonstrates those safeguards rather than treating reflection as a substitute for a schema.

A Real Project Scenario: A Small Offline Profile Cache

I first used Runtime-assisted archiving in an app with several small offline models. The repetitive part was not writing the first encoder; it was keeping the decoder synchronized months later when another developer added a field. For the reproducible example below, I reduced that situation to a user profile containing a display name, a visit count, and a list of favorite topics. The command-line program archives the profile, restores it, and compares every value so the example has a visible pass-or-fail result.

The important change from the earlier code is that the demo adopts NSSecureCoding. Runtime discovery is used only to remove repetitive enumeration. It does not decide which classes are safe to instantiate. Each model supplies an allowlist for its ivars, and the unarchiver uses that list during decoding.

Runtime Discovery with an Explicit Schema

Automatically persisting every new ivar sounds convenient, but it can also archive a cache, delegate, secret, or incompatible value that was never intended to leave memory. In this implementation, an ivar is persisted only when its name appears in allowedClassesByIvarName. That allowlist acts as a small, reviewable schema.

+ (NSDictionary<NSString *, NSSet<Class> *> *)allowedClassesByIvarName {
    return @{
        @"_displayName": [NSSet setWithObject:[NSString class]],
        @"_visitCount": [NSSet setWithObject:[NSNumber class]],
        @"_favoriteTopics": [NSSet setWithObjects:
            [NSArray class], [NSString class], nil]
    };
}

The collection entry includes both NSArray and NSString because secure decoding must allow the container and its element type. The base class walks each model class, calls class_copyIvarList, and stops at the shared archive base class rather than wandering into framework internals.

Secure Decoding and Migration Boundaries

The archive is created with requiringSecureCoding:YES, and every value is restored through decodeObjectOfClasses:forKey:. This prevents the generic decoder from accepting an arbitrary object graph. The sample also checks the returned data, error objects, restored object, and final equality result instead of assuming every operation succeeds.

NSData *data = [NSKeyedArchiver
    archivedDataWithRootObject:original
    requiringSecureCoding:YES
    error:&archiveError];

DemoProfile *restored = [NSKeyedUnarchiver
    unarchivedObjectOfClass:[DemoProfile class]
    fromData:data
    error:&decodeError];

This pattern still needs a migration plan. Renaming an ivar changes its generated key, changing a class may make an old value invalid, and deleting a field leaves older archives with data the new model ignores. For short-lived caches, clearing incompatible data may be acceptable. For user documents or long-lived state, use stable keys, store a schema version, write migration tests, and keep backups. An archive is serialization, not encryption, so credentials and other secrets need an appropriate protected store.

Download and Run the Complete Project

The complete standalone project is available in its dedicated GitHub repository: objc-runtime-secure-archive-demo. It requires macOS and the Xcode Command Line Tools.

git clone https://github.com/2252408699/objc-runtime-secure-archive-demo.git
cd objc-runtime-secure-archive-demo
make run

A successful run prints the restored profile and Round-trip verification: PASS. The repository includes the complete model, reflective base class, secure allowlist, error handling, build command, and cleanup command. You can add a field to DemoProfile, update its allowlist, and rerun the program to see exactly how the schema changes.

Seren
Seren

A developer exploring iOS, Objective-C, Swift, and other tech stacks. Here to document my learning process, pitfalls, and growing journey across new technologies.

Your email address will not be published. Required fields are marked *