I find that I often have to maintain a singleton primarily to hold an array that is sometimes persisted between user sessions, that should have only one class of items, and that sometimes needs to be ordered. I got tired of writing the same boilerplate over and over so came up with this. I'd welcome critiques (particularly re: thread safety) and suggestions for how to expand this to cover more situations. Some notes:
- This only takes care of object properties, not fundamental types.
- This uses
NSUserDefaults
for inter-session persistence. - This is meant to remove boilerplate and make smart default assumptions for users.
Here's the generic object from which custom objects to be put in a singleton class should be subclassed:
CKGeneralizedItem.h
#import <Foundation/Foundation.h>
#import "CKGeneralSingletonArray.h"
@interface CKGeneralizedItem : NSObject <CKArchivingItem, NSCoding>
+ (NSArray *)objectPropertiesToArchive;
+ (NSArray *)boolPropertiesToArchive;
+ (NSArray *)floatPropertiesToArchive;
+ (NSArray *)intPropertiesToArchive;
@end
CKGeneralizedItem.m
#import "CKGeneralizedItem.h"
@implementation CKGeneralizedItem
#pragma mark Encoding
- (void) encodeWithCoder:(NSCoder *)aCoder {
for (NSString *keyName in [[self class] objectPropertiesToArchive]) {
[aCoder encodeObject:[self valueForKey:keyName] forKey:keyName];
}
return;
}
- (id)initWithCoder:(NSCoder *)coder;
{
self = [super init];
if (self != nil)
{
for (NSString *keyName in [[self class] objectPropertiesToArchive]){
[self setValue:[coder decodeObjectForKey:keyName] forKey:keyName];
}
}
return self;
}
#pragma mark Archival properties
+ (NSArray *)objectPropertiesToArchive {
return @[];
}
+ (NSArray *)boolPropertiesToArchive {
return @[];
}
+ (NSArray *)floatPropertiesToArchive {
return @[];
}
+ (NSArray *)intPropertiesToArchive {
return @[];
}
#pragma mark Inspection
- (NSString *) description {
NSString *descriptionString = @"";
for (NSString *keyString in [[self class] objectPropertiesToArchive]) {
descriptionString = [descriptionString stringByAppendingString:[NSString stringWithFormat:@"%@: %@\n", keyString, [self valueForKey:keyString]]];
}
return descriptionString;
}
@end
Here's the singleton array:
CKGeneralSingletonArray.h
#import <Foundation/Foundation.h>
@protocol CKArchivingItem <NSObject>
#pragma mark State persistence
- (void) retrieveItemsFromArchive;
- (void) archiveItems;
@end
@interface CKGeneralSingletonArray : NSObject
@property (strong, atomic) NSString *arrayObjectClassString;
@property (strong, atomic) NSComparator sortWithThisComparator;
@property NSBinarySearchingOptions binarySearchOption;
+ (instancetype) sharedManager;
// array membership
- (void) insertItem:(id<NSCoding, CKArchivingItem>) item;
- (void) clearAllItems;
- (void) printItems;
// saving/uploading data
- (void) uploadItems;
// archiving items
- (void) archiveItems;
- (void) retrieveItemsFromArchive;
- (void) setArchiveName: (NSString *)archiveName;
@end
CKGeneralSingletonArray.m
#import "CKGeneralSingletonArray.h"
#import "CKGeneralizedItem.h"
#define kCKGeneralizedItemsDefaultTheadName "com.yourAppHere.Bundle"
#define kCKGeneralizedItemsDefaultArchivingName @"yourAppKeyedArchiving"
#define kCKGeneralizedItemsDefaultNSUserDefaultsSuite @"com.DefaultsThisApp.AppBundle"
@interface CKGeneralSingletonArray ()
@property (strong, nonatomic) NSMutableArray <id<NSCoding, CKArchivingItem>> *items;
@property (strong, nonatomic) NSString *archiveString;
@property (strong, nonatomic) NSString *threadingQueueString;
@property (strong, nonatomic) NSString *userDefaultsString;
@property dispatch_queue_t itemsAccessQueue;
@end
@implementation CKGeneralSingletonArray
@synthesize items, itemsAccessQueue, archiveString, threadingQueueString, arrayObjectClassString, sortWithThisComparator, binarySearchOption;
#pragma mark Instantiation
+ (instancetype) sharedManager {
static dispatch_once_t once;
static id sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (id) init {
self = [super init];
if (self) {
self.items = [NSMutableArray new];
self.binarySearchOption = NSBinarySearchingFirstEqual;
}
return self;
}
#pragma mark Public-facing array manipulation
- (void) insertItem:(id<NSCoding, CKArchivingItem>) item {
if (arrayObjectClassString && ![item isKindOfClass:NSClassFromString(arrayObjectClassString)]) {
[NSException raise:@"YOU MADE A MISTAKE" format:@"you tried to insert a %@ but I can only accept %@", [item class], arrayObjectClassString];
} else {
if (!sortWithThisComparator) {
[self.items addObject:item];
} else {
NSInteger newItemIndex = [self.items indexOfObject:item inSortedRange:NSMakeRange(0, [self.items count]) options:NSBinarySearchingFirstEqual usingComparator:sortWithThisComparator];
[self.items insertObject:item atIndex:newItemIndex];
}
}
}
- (void) clearAllItems {
[self.items removeAllObjects];
}
- (void) printItems {
NSLog(@"here are printed items %@", self.items);
}
#pragma mark Thread-safe multiple-point thread-safe access
- (void) setItems:(NSMutableArray<id<NSCoding,CKArchivingItem>> *)itemsVal {
if (!threadingQueueString) {
itemsAccessQueue = dispatch_queue_create(kCKGeneralizedItemsDefaultTheadName, DISPATCH_QUEUE_CONCURRENT);
}
dispatch_barrier_async(self.itemsAccessQueue, ^{
items = itemsVal;
});
}
- (NSMutableArray <id<NSCoding, CKArchivingItem>> *) getItems {
if (!threadingQueueString) {
itemsAccessQueue = dispatch_queue_create(kCKGeneralizedItemsDefaultTheadName, DISPATCH_QUEUE_CONCURRENT);
}
__block NSMutableArray <id<NSCoding, CKArchivingItem>> *copyItems;
dispatch_sync(self.itemsAccessQueue, ^{
copyItems = [items mutableCopy];
});
return copyItems;
}
- (void) setThreadingQueueString:(NSString *)threadingQueueStringVal {
static dispatch_once_t once;
dispatch_once(&once, ^{
threadingQueueString = threadingQueueStringVal;
});
}
#pragma mark Between-session persistence in user defaults
// this can also be modified to store the NSData in a file
- (void) retrieveItemsFromArchive {
if (!archiveString) {
[self setArchiveName:kCKGeneralizedItemsDefaultArchivingName];
}
NSString *defaultsString = self.userDefaultsString;
if (!defaultsString) defaultsString = kCKGeneralizedItemsDefaultNSUserDefaultsSuite;
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:defaultsString];
NSArray *retrievedItems = [NSKeyedUnarchiver unarchiveObjectWithData: [defaults objectForKey:archiveString]];
if (retrievedItems) {
self.items = [retrievedItems mutableCopy];
}
}
- (void) archiveItems {
if (!archiveString) {
[self setArchiveName:kCKGeneralizedItemsDefaultArchivingName];
}
NSString *defaultsString = self.userDefaultsString;
if (!defaultsString) defaultsString = kCKGeneralizedItemsDefaultNSUserDefaultsSuite;
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:defaultsString];
[defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:self.items] forKey:archiveString];
[defaults synchronize];
NSLog(@"here is what got archived %@", [NSKeyedUnarchiver unarchiveObjectWithData: [defaults objectForKey:archiveString]]);
}
- (void) setArchiveName: (NSString *)archiveStringVal {
static dispatch_once_t once;
dispatch_once(&once, ^{
archiveString = archiveStringVal;
});
}
#pragma mark Uploading to remote API
- (void) uploadItems {
// this should be entirely defined by the sub-classer
[NSException raise:@"uploadItems has not been properly defined" format:@"This class method must be overridden."];
}
@end
Here's a sample of what you could do:
[[CKGeneralSingletonArray sharedManager] retrieveItemsFromArchive]; [[CKGenericSingletonSubclass sharedManager] printItems]; [[CKGeneralSingletonArray sharedManager] setArrayObjectClassString:@"NSString"]; CKGeneralizedItemSubclass *subclass = [[CKGeneralizedItemSubclass alloc] init]; subclass.firstName = @"Doofus"; subclass.lastName = @"Potter"; subclass.age = @(11); CKGeneralizedItemSubclass *subclass2 = [[CKGeneralizedItemSubclass alloc] init]; subclass2.firstName = @"Draco"; subclass2.lastName = @"Malfoy"; subclass.birthday = [[NSDate date] dateByAddingTimeInterval:-1*24*60*60*50]; subclass2.age = @(11); [[CKGenericSingletonSubclass sharedManager] insertItem:subclass]; // this will throw an exception because the array singleton has been set to only accept NSStrings [[CKGenericSingletonSubclass sharedManager] insertItem:subclass2]; [[CKGenericSingletonSubclass sharedManager] printItems];
CKGeneralizedItem
even work with methods that are set to always return empty non-mutable arrays? And secondly, what exactly is the point of adding this extra layer over the top ofNSUserDefaults
? – nhgrif Feb 12 at 13:05