Correct Methods for Storing Custom Objects in NSUserDefaults: From NSCoding to NSData Conversion

Dec 03, 2025 · Programming · 12 views · 7.8

Keywords: NSUserDefaults | NSCoding | NSData | iOS Development | Data Persistence

Abstract: This article provides an in-depth exploration of the common 'Attempt to set a non-property-list object' error when storing custom objects in NSUserDefaults in iOS development. Through analysis of a typical Objective-C case study, it explains the limitations of NSUserDefaults to only store property-list objects (such as NSArray, NSDictionary, NSString, etc.) and demonstrates how to convert custom objects to NSData via the NSCoding protocol and NSKeyedArchiver for storage. The article compares different implementation approaches, offers complete code examples and best practice recommendations, helping developers avoid common pitfalls and optimize data persistence solutions.

Problem Background and Error Analysis

In iOS development, NSUserDefaults is a commonly used lightweight data persistence solution, but developers frequently encounter the Attempt to set a non-property-list object error. The core issue is that NSUserDefaults can only store specific property-list object types, including NSArray, NSDictionary, NSString, NSData, NSNumber, and NSDate. When attempting to store custom objects directly, even if they implement the NSCoding protocol, the system will still throw an exception.

Correct Implementation of NSCoding Protocol

To enable custom objects to support archiving, the two core methods of the NSCoding protocol must be properly implemented: encodeWithCoder: and initWithCoder:. Below is an improved implementation example of the BC_Person class:

@interface BC_Person : NSObject <NSCoding>
@property (nonatomic, strong) NSString *personsName;
@property (nonatomic, strong) NSArray *personsBills;
@end

@implementation BC_Person

- (void)encodeWithCoder:(NSCoder *)coder {
    // Encode string property
    [coder encodeObject:self.personsName forKey:@"BCPersonsName"];
    // Encode array property (the array itself must be a property-list object)
    [coder encodeObject:self.personsBills forKey:@"BCPersonsBillsArray"];
}

- (instancetype)initWithCoder:(NSCoder *)coder {
    self = [super init];
    if (self) {
        // Decode using the same keys as during encoding
        _personsName = [coder decodeObjectForKey:@"BCPersonsName"];
        _personsBills = [coder decodeObjectForKey:@"BCPersonsBillsArray"];
    }
    return self;
}

@end

It is important to note that implementing NSCoding alone does not allow objects to be stored directly in NSUserDefaults; it only provides the foundation for object serialization.

NSData Conversion and Storage Strategy

The correct storage process requires converting custom objects to NSData. Below is an optimized data saving method that avoids the logical errors in the original code:

- (void)savePersonArrayData:(BC_Person *)personObject {
    // 1. Add new object to mutable array
    [mutableDataArray addObject:personObject];
    
    // 2. Create archive array to store NSData representations of all objects
    NSMutableArray *archiveArray = [NSMutableArray arrayWithCapacity:mutableDataArray.count];
    for (BC_Person *person in mutableDataArray) {
        // Use NSKeyedArchiver to convert object to NSData
        NSData *personData = [NSKeyedArchiver archivedDataWithRootObject:person];
        [archiveArray addObject:personData];
    }
    
    // 3. Store NSData array to NSUserDefaults
    NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
    [userData setObject:archiveArray forKey:@"personDataArray"];
    // The synchronize method typically does not need to be called explicitly
}

The key advantage of this approach is that it creates an array of NSData objects, each of which is a property-list compatible type and thus acceptable to NSUserDefaults.

Data Retrieval and Deserialization

When retrieving data from NSUserDefaults, the reverse process must be performed:

- (NSArray *)loadPersonArrayData {
    NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
    NSArray *archiveArray = [userData objectForKey:@"personDataArray"];
    
    if (!archiveArray) {
        return @[]; // Return empty array instead of nil
    }
    
    NSMutableArray *personArray = [NSMutableArray arrayWithCapacity:archiveArray.count];
    for (NSData *personData in archiveArray) {
        // Use NSKeyedUnarchiver to convert NSData back to BC_Person object
        BC_Person *person = [NSKeyedUnarchiver unarchiveObjectWithData:personData];
        if (person) {
            [personArray addObject:person];
        }
    }
    
    return [personArray copy]; // Return immutable array
}

Performance Optimization and Alternative Approaches

While the above method is effective, it may have performance issues when handling large amounts of data. Each save requires re-archiving the entire array. A more efficient approach is to archive the entire object array directly:

// Archive entire array
NSData *allPersonsData = [NSKeyedArchiver archivedDataWithRootObject:mutableDataArray];
[userData setObject:allPersonsData forKey:@"personDataArray"];

// Unarchive entire array
NSData *savedData = [userData objectForKey:@"personDataArray"];
NSArray *loadedArray = [NSKeyedUnarchiver unarchiveObjectWithData:savedData];

This method reduces loop operations and improves storage efficiency. However, it is important to note that if objects in the array do not properly implement NSCoding, the entire operation will fail.

Swift Implementation Comparison

In Swift, the same logic can be implemented with more concise syntax. Below is a Swift version example:

import Foundation

class Person: NSObject, NSCoding {
    var name: String
    var bills: [String]
    
    init(name: String, bills: [String]) {
        self.name = name
        self.bills = bills
    }
    
    // NSCoding implementation
    required init?(coder aDecoder: NSCoder) {
        name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
        bills = aDecoder.decodeObject(forKey: "bills") as? [String] ?? []
    }
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
        aCoder.encode(bills, forKey: "bills")
    }
}

// Storage function
func savePeople(_ people: [Person]) {
    let archivedData = NSKeyedArchiver.archivedData(withRootObject: people)
    UserDefaults.standard.set(archivedData, forKey: "peopleData")
}

// Loading function
func loadPeople() -> [Person] {
    guard let data = UserDefaults.standard.data(forKey: "peopleData"),
          let people = NSKeyedUnarchiver.unarchiveObject(with: data) as? [Person] else {
        return []
    }
    return people
}

The Swift version uses safer type casting and optional handling, reducing the risk of runtime errors.

Best Practices Summary

  1. Understand Limitations: Always remember that NSUserDefaults can only store property-list objects.
  2. Implement NSCoding Correctly: Ensure all properties of custom objects are archivable types.
  3. Use NSData as Intermediate Layer: Convert objects to NSData via NSKeyedArchiver before storage.
  4. Consider Data Volume: For large amounts of data, consider more appropriate solutions like Core Data or file storage.
  5. Error Handling: Add appropriate error checking and recovery mechanisms during archiving and unarchiving.
  6. Version Compatibility: If object structures may change, consider implementing NSSecureCoding for enhanced security.

By following these principles, developers can effectively use NSUserDefaults to store complex objects while avoiding common serialization errors.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.