tl;dr; There is a neat little library for handling
NSUserDefaults over here.
For a project I've recently worked on, I've created a settings class which mirrors its properties into
NSUserDefaults. I did so by overriding the getters and setters and directly forwarding the calls to
NSUserDefaults. After adding one or the other property I've discovered that it's cumbersome to add new properties. You need to define a new static key variable for the
NSUserDefaults, you need to override getter and setter and eventually add support for Key-Value Observing. So I had the idea write a little library for all this. Objective-C makes heavy use of those
@dynamic properties in CoreData. Couchbase's iOS library does the same for their model objects. So I figured there is a way without using any private SDK to achieve the same.
-> The idea for
FFUserDefaults was born.
What should it do
I haven't defined many requirements. What I wanted to create is a class which can be subclassed and which dynamically implements
@dynamic properties at runtime. Also, those properties must be Key-Value observable. And one should be able to use properties of a primitive data type as well.
Dynamically resolve methods
First step was to find out how to resolve methods dynamically. This Question on StackOverflow showed me the basic approach. I then found this Site in Apple's documentation which shows how the attributes of a property look like and what different types there are. So the next step was to put the pieces together. I've created a model class which represents a property and the
FFUserDefaults class which reads all its properties on demand. At the beginning I've only created IMPs for object properties. I've thought that I could get the primitive ones for free by dynamically creating the IMPs for them. But I was wrong and I ended up writing getter and setter for each and every primtive data type I wanted to support:
One of the features I wanted to have in
FFUserDefaults is to support KVO. The properties should be observable. For that I need to observe
NSUserDefaults which isn't really a problem. The first tests without any observing options specified ran through without any problems. But once I created the example project, I added
NSKeyValueObservingOptionNew and suddenly I got crashes caused by
valueForUndefinedKey:. After a bit of playing around it turned out that I needed to implement
setValue:forKey: as well. And I couldn't even just call my methods as the
setValue:forKey: methods both have
id as argument and the getters and setters need the primitive types. My next thought was to get rid of all my getters and setters and just use
setValue:forKey:. But then I couldn't use my
@dynamic properties anymore.
KVO seems broken
While adding KVO support I've noticed that my tests eventually wouldn't run through. In fact, they just stopped without any reason. No crash, no (visible) endless loops, they just stopped. By setting breakpoints I've noticed that it seemed to have something to do with calling
valueForKey: on an array (which then should call
valueForKey: on all its objects and return an array with those values). I was able to step right to that call but no further. I couldn't even step into the method. The moment this specific line was executed nothing happened anymore. Now, the funny part about this was that this only happened when the KVO test was run. All other tests were just fine. With a bit of trial and error debugging I found out, that calling
valueForKey: on an array while setting an observed property ends up in nirvana. I haven't tried calling
valueForKey: on objects other than arrays. It seems like a bug to me but it would need further investigation before filing a radar with Apple.
It eventually works
In the end I got everything working. Dynamically adding methods to a class is something completely new to me. I haven't done this before. And I'm sure there's a lot to improve in
FFUserDefaults. But it's working and that's what is important in the first place.
You can find the library on Github over here. Any feedback and/or pull requests are welcome!
This library isn't yet ported to Swift as I'm not aware of a way to achieve the same in Swift. But as soon as this is possible I'll be working on a Swift version.