How to save a struct to NSUserDefaults in Objectiv

2019-05-07 20:51发布

问题:

How do I save a custom struct to NSUserDefaults?

struct Paging {
    NSInteger currentPage;
    NSInteger totalResults;
    NSInteger resultsPerPage;
};

typedef struct Paging Paging;

NSUserDefaults *userInfo = [NSUserDefaults standardUserDefaults];
[userInfo setStruct:paging forKey:@"paging"];
[userInfo synchronize];

The above code produces a run-time warning:
*** -[NSUserDefaults setObject:forKey:]: Attempt to insert non-property value...

回答1:

One way is to turn it into NSData

NSData* d = [NSData dataWithBytes: paging length: sizeof(paging)];

In this case your struct is pretty simple, so you might want to add a category to NSUserDefaults to save and restore it. I'd implement that by saving an NSDictionary with four NSNumbers. A bit more work, but it means your plist will be humanly readable.



回答2:

In your specific case, you can get away with using an NSData since the members are just ints, but for future readers of this question, please read the Apple docs on Encoding and Decoding C Data Types before doing this!

Encoding and Decoding C Data Types - Structures and Bit Fields:

You should not wrap a structure with an NSData object and archive that. If the structure contains object or pointer fields, the data object isn’t going to archive them correctly. You also create a dependence on how the compiler decides to lay out the structure, which can change between versions of the compiler and may depend on other factors. A compiler is not constrained to organize a structure just as you’ve specified it in the source code—there may be arbitrary internal and invisible padding bytes between fields in the structure, for example, and the amount of these can change without notice and on different platforms. In addition, any fields that are multiple bytes in width aren’t going to get treated correctly with respect to endianness issues. You will cause yourself all sorts of compatibility trouble.

That doc does say how to properly do it (but no examples are given unfortunately - when I come up with one however, I will update this answer with the example):

The best technique for archiving a structure or a collection of bit fields is to archive the fields independently and chose the appropriate type of encoding/decoding method for each. The key names can be composed from the structure field names if you wish, like "theStruct.order", "theStruct.flags", and so on. This creates a slight dependency on the names of the fields in the source code, which over time may get renamed, but the archiving keys cannot change if you want to maintain compatibility.


A method that is very similar to NSData wrapping but uses NSValue (this is also discouraged, but will work for basic structs as well):

struct Paging {
    NSInteger currentPage;
    NSInteger totalResults;
    NSInteger resultsPerPage;
};

typedef struct Paging Paging;

- (void)someMeth {
    // Here `paging_struct` is a struct of type `Paging`:
    NSValue *pagingStructValue;
    // On its own line just for readability
    pagingStructValue = [NSValue valueWithBytes:&paging_struct
                                       objCType:@encode(Paging)];

    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setObject:pagingStructValue
                     forKey:@"paging"];
    [userDefaults synchronize];
}

NSValue Documentation



回答3:

I made the less favourable decision to use struct in my iOS app. Now I am also facing this problem where I cannot save the struct (which contains about 30 variables) into NSUserDefaults easily.

After trying many solutions given in SO and other sites, it has not working well for me. So finally I decided to create a simple method to convert the struct into NSMutableDictionary.

-(NSMutableDictionary *)structToDictionary:(StructPictures) dat {


    NSArray *values = [NSArray arrayWithObjects:
                       [NSNumber numberWithFloat:dat.bgOpacity],
                       [NSNumber numberWithInt:dat.bgType], // 1= general,
                       [NSNumber numberWithFloat:dat.glowAmt],
                       [NSNumber numberWithInt:dat.textureNum],
                       [NSNumber numberWithInt:dat.patternNum],
                       [NSNumber numberWithInt:dat.patternType],
                       [NSNumber numberWithFloat:dat.patternHue],
                       [NSNumber numberWithFloat:dat.textureSizer],
                       [NSNumber numberWithInt:dat.stickerR],
                       [NSNumber numberWithInt:dat.stickerG],
                       [NSNumber numberWithInt:dat.stickerB]

                       , nil];


    NSArray *keys = [NSArray arrayWithObjects:
                     @"bgOpacity",
                     @"bgType",
                     @"glowAmt",
                     @"textureNum",
                     @"patternNum",
                     @"patternType",
                     @"patternHue",
                     @"textureSizer",
                     @"stickerR",
                     @"stickerG",
                     @"stickerB",
                     nil];

    NSMutableDictionary *res = [[NSMutableDictionary alloc] initWithObjects:values forKeys:keys];

return res;
}

StructPictures is the name of my struct (custom). Hope this give an alternative solution to saving struct into NSUserDefaults.