4

In my project I have a settings class with properties with custom setters that access NSUserDefaults to make everything simpler. The idea is that Settings class has

@property NSString *name

which has custom getter that gets the name value from NSUserDefaults and a setter that saves the new value there. In this way throughout the whole project I interact with the Settings class only to manage user defined preferences. The thing is that it seems way too repetitive to write all the getters and setters (I have about 50 properties), and would like to create one setter and one getter that would work for all variables. My only issue is how to get hold of the name of the variable within the setter.

The final question then is: is it possible to find out within a getter or setter for which property is the function being called?

If you have some other approach I would appreciate it too but considering that I would like to keep all the NSUserDefaults stuff in one class, I can't think of an alternative that doesnt include writing 50 getters and setters.

Thanks!

shim
  • 9,289
  • 12
  • 69
  • 108
Denis Balko
  • 1,566
  • 2
  • 15
  • 30

6 Answers6

4

Another approach could be this. No properties, just key value subscript.

@interface DBObject : NSObject<NSCoding>
+ (instancetype)sharedObject;
@end

@interface NSObject(SubScription) 
- (id)objectForKeyedSubscript:(id)key;
- (void)setObject:(id)obj forKeyedSubscript:(id <NSCopying>)key;
@end

On the implementation file:

+ (instancetype)sharedObject {
   static DBObject *sharedObject = nil;
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
     sharedObject = [[DBObject alloc] init];
   });
   return sharedObject;
}

- (id)objectForKeyedSubscript:(id)key {
   return [[NSUserDefaults standardUserDefaults] objectForKey:key];
 }

- (void)setObject:(id)obj forKeyedSubscript:(id <NSCopying>)key {
   [[NSUserDefaults standardUserDefaults] setObject:obj forKeyedSubscript:key];
   [[NSUserDefaults standardUserDefaults] synchronize];
 }

Now, you can use it like this:

 // You saved it in NSUserDefaults
[DBObject sharedObject][@"name"] = @"John"; 

// You retrieve it from NSUserDefaults
NSLog(@"Name is: %@", [DBObject sharedObject][@"name"]); 

I this this is the best approach and is what i will use in the future.

Harald
  • 422
  • 5
  • 10
  • Sorry Herald for the late reply, I only noticed your answer now. You are quite correct that this would work, but it doesn't improve much upon standard NSUserDefaults. I still have to get a shared object (like standardDefaults), and still have to provide a key that doesn't check spelling. Eventually [[NSUserDefaults standardDefaults] setObject:"John" forKey:"name"]; isn't that much different from [DBObject sharedObject][@"name"] = @"John"; It is a bit shorter and looks nicer, so I agree that it is an improvement, just not enough to be worth rewriting all the code getting preferences. – Denis Balko Jun 23 '17 at 08:36
3

The setter and getter in this case is simple, you can do like this:

- (void)setName:(NSString *)name {
   [[NSUserDefaults standardUserDefaults] setObject:name forKey:@"name"];
   [[NSUserDefaults standardUserDefaults] synchronize];
}

- (NSString *)name {
   return [[NSUserDefaults standardUserDefaults] objectForKey:@"name"];
}

If you want to use a simple approach for all properties:

- (id)objectForKey:(NSString *)key {
   return [[NSUserDefaults standardUserDefaults] objectForKey:key];
}
- (void)setObject:(id)object forKey:(NSString *)key {
   [[NSUserDefaults standardUserDefaults] setObject:object forKey:key];
   [[NSUserDefaults standardUserDefaults] synchronize];
}

Instead of creating many properties, create many keys, each key is something you want to save or retrieve. Example of keys:

static NSString *const kName = @"name";
static NSString *const kLastName = @"lastName";
Harald
  • 422
  • 5
  • 10
  • You are right this would work and I actually had something very similar to your first version but the problem is once you have many properties, you would have 2xmany functions so if for any reason I need to change it, it would be a pain to maintain. That is why I'm looking for a general option so that one getter and one setter work for all properties (something that people above managed to achieve one way or another). Keys is another option indeed, but it is nowhere as clean which, with the amount of code I have, is a problem. It is, however, definitely a viable alternative approach. Thanks! – Denis Balko Jul 26 '16 at 22:18
2

I found your question very interesting and I said to myself "Challenge accepted!".

I've created this project on Github.

Basically, all you have to do is subclass the VBSettings class and then declare de properties, like this:

@interface MySettings : VBSettings

@property (strong, nonatomic) NSString *hello;

@end

The value of "hello" will be saved to NSUserDefaults with the key "hello". Example of usage:

MySettings settings = [[MySettings alloc] init];
settings.hello = "World!"; //The value is saved in NSUserDefaults
NSLog(@"%@", settings.hello); //The value is restored from NSUserDefaults.
vbgd
  • 1,937
  • 1
  • 13
  • 18
  • Thank you for this, it looks great and achieves exactly the purpose I need. I would just like to point out that this could be pretty useful to other people in which case perhaps you could add 2 more methods: clearAll and registerDefaults:@{}. I definitely intend on implementing both into the subclass for when the user logs out and defaults to have in AppDelegate easily visible. Again, thanks! – Denis Balko Jul 26 '16 at 22:11
  • Thank you! I appreciate the feedback. I really intent to improve the project in the near future including those two methods you mentioned. – vbgd Jul 26 '16 at 22:15
  • And I forgot, "Challenge Completed!" ;) – Denis Balko Jul 26 '16 at 22:18
0

One possibility would be to use KVO to detect when your properties change.

E.g.:

@interface Settings : NSObject

@property NSString *one;
@property NSString *two;

@end

@implementation Settings

- (instancetype)init
{
    self = [super init];
    if (self) {
        [self addObserver:self forKeyPath:@"one" options:NSKeyValueObservingOptionNew context:NULL];
        [self addObserver:self forKeyPath:@"two" options:NSKeyValueObservingOptionNew context:NULL];
    }
    return self;
}

- (void)dealloc
{
    [self removeObserver:self forKeyPath:@"one"];
    [self removeObserver:self forKeyPath:@"two"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"Key: %@, Change: %@", keyPath, change);
}

@end

In a different class, use the standard property access:

Settings *settings = [[Settings alloc] init];
settings.one = @"something for one";

The Settings object logs:

Key: one, Change: { kind = 1; new = "something for one"; }

Phillip Mills
  • 30,888
  • 4
  • 42
  • 57
  • Thats a smart way around the problem, although I always had problems with KVO and would like to avoid if it at all possible. – Denis Balko Jul 26 '16 at 22:15
0

You could try to use dynamic Getter and Setter declarations as noted in this answer.

First create generic functions that you want all the properties to use:

- (id)_getter_
{
    return [[NSUserDefaults standardUserDefaults] objectForKey:NSStringFromSelector(_cmd)];
}

- (void)_setter_:(id)value 
{
    //This one's _cmd name has "set" in it and an upper case first character
    //This could take a little work to parse out the parameter name
    [[NSUserDefaults standardUserDefaults] setObject:object forKey:YourParsedOutKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

Then create the dynamic method generator:

+(void)synthesizeForwarder:(NSString*)getterName
{
    NSString*setterName=[NSString stringWithFormat:@"set%@%@:",
          [[getterName substringToIndex:1] uppercaseString],[getterName substringFromIndex:1]];
    Method getter=class_getInstanceMethod(self, @selector(_getter_));
    class_addMethod(self, NSSelectorFromString(getterName), 
                    method_getImplementation(getter), method_getTypeEncoding(getter));
    Method setter=class_getInstanceMethod(self, @selector(_setter_:));
    class_addMethod(self, NSSelectorFromString(setterName), 
                    method_getImplementation(setter), method_getTypeEncoding(setter));
}

Then set what strings you want to create dynamic getters and setters for:

+(void)load  
{
    for(NSString*selectorName in [NSArray arrayWithObjects:@"name", @"anything", @"else", @"you", @"want",nil]){
       [self synthesizeForwarder:selectorName];
    }
}

That will create getters and setters for any variable name that you add to the array. I'm not sure how well this will work when other classes try to call these methods, the compiler won't see them at compile time so may throw errors when you try to use them. I just combined 2 other StackOverflow questions into this one answer for your situation.

Dynamic Getters and Setters.

Get current Method name.

Community
  • 1
  • 1
Putz1103
  • 6,211
  • 1
  • 18
  • 25
  • Looks good but I worry about the combination of those two concepts. It certainly has the potential to work would have to be tested. Kudos for thinking of this though. – Denis Balko Jul 26 '16 at 22:14
0

As I understand it, you don't want the mental overhead of setObject:forKey: and objectForKey: method calls by the user of this class.

Here is how to get round it. I am leaving a lot of gaps for you to fill in.

  1. Declare the property in the header file, so that callers can use it:

    @property NSString *something;
    @property NSString *somethingElse;
    
  2. In the class file itself, declare that you are defining the properties, so that the compiler doesn't get upset:

    @dynamic something,somethingElse;
    
  3. In the class file, implement the methodSignatureForSelector function, like this:

    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {if (SelectorIsGetter(aSelector))
       return [NSMethodSignature signatureWithObjCTypes:"@@:"];
     if (SelectorIsSetter(aSelector))
       return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
     return nil;
     }
    

This will tell the system to call forwardInvocation: for these selectors, and will also tell it the shape of the call that is being made.

Your implementation of SelectorIsGetter and SelectorIsSetter is up to you. You will probably use NSStringFromSelector(aSelector) to get the selector's name, and then look it up in a table of names to see if it matches any of the names of the selectors you are implementing: in this case something and somethingElse for the getters and setSomething: and setSomethingElse: for the setters.

  1. In the class file, implement the forwardInvocation: function, like this:

    -(void)forwardInvocation:(NSInvocation *)anInvocation
    {if (SelectorIsGetter(anInvocation.selector))
       {NSString *s=[self objectForKey:NSStringFromSelector(anInvocation.selector)];
        [anInvocation setReturnValue:&s];
        return;
        };
     if (SelectorIsSetter(anInvocation.selector))
        {NSString *s;
         [anInvocation getArgument:&s atIndex:2];
         [self setObjectForKey:UnmangleName(NSStringFromSelector(anInvocation.selector))];
         return;
         };
     [super forwardInvocation:anInvocation];
     }
    

…where UnmangleName is a thoroughly tedious function that takes a string like "setSomething:" and turns it into a string like "something".

If you want to do more than just NSStrings, the extension is reasonably straightforward.

nugae
  • 499
  • 2
  • 5
  • That sounds like a reasonable approach although it is clearly quite a lot complex than subclassing the class provided by vladiulianbogdan. Nevertheless, it is a good idea and thank you for that, it certainly helps me to get a broader idea of a possible implementation. – Denis Balko Jul 26 '16 at 22:13