Message Sent To Deallocated Instance... Sent During #synthesize? - objective-c

I've been using code from Raphael Cruzeiro's PDF Annotator, and have discovered a number of memory leaks (ARC is off, and will stay off to support older devices). After patching most of them up, I'm down to the last couple, and they have me stumped. So in a class called PDFDocument, he has properties for a CGPDFPageRef, CGPDFDocument, and a custom annotation class #synthesize'd. I had to pepper his dealloc method with releases and eliminate some dangling pointers, which works well except for one small problem: After about 3 complete retain-release cycles, it crashes at the #synthesize line for his annotation object... I've never seen a SIGABRT because of a deallocated object sent during #synthesize, so naturally have no idea how to fix it. If I remove the release code in dealloc, it leaks, but if I leave it in, it crashes. Here's the code for the PDFDocument class:
//.h
#import <Foundation/Foundation.h>
#class Annotation;
#interface PDFDocument : NSObject {
Annotation *_annotation;
}
- (id)initWithDocument:(NSString *)documentPath;
- (NSInteger) pageCount;
- (void) loadPage:(NSInteger)number;
- (BOOL)save;
#property (nonatomic, retain) NSString *name;
#property (nonatomic, retain) NSString *hash;
#property (readwrite, nonatomic, assign) CGPDFDocumentRef document;
#property (readwrite, nonatomic, assign) CGPDFPageRef page;
#property (nonatomic, retain) NSString *version;
#property (nonatomic, assign) BOOL dirty;
#property (nonatomic, retain) Annotation *annotation;
#end
//.m
#import "PDFDocument.h"
#import "Annotation.h"
#import "HashExtensions.h"
#import "DocumentDeserializer.h"
#import "DocumentSerializer.h"
#implementation PDFDocument
#synthesize document;
#synthesize page;
#synthesize annotation = _annotation; //after 3rd cycle, it crashes here.
#synthesize name;
#synthesize hash;
#synthesize dirty;
#synthesize version;
- (id)initWithDocument:(NSString *)documentPath
{
if((self = [super init]) != NULL) {
self.name = [documentPath lastPathComponent];
if ([self.name isEqualToString:#"Musette.pdf"] || [self.name isEqualToString:#"Minore.pdf"] || [self.name isEqualToString:#"Cantata.pdf"] || [self.name isEqualToString:#"Finalé.pdf"])
{
CFURLRef ref = CFBundleCopyResourceURL(CFBundleGetMainBundle(), (CFStringRef)self.name, NULL, NULL);
self.document = CGPDFDocumentCreateWithURL(ref);
self.page = CGPDFDocumentGetPage(document, 1);
self.version = #"1.0";
DocumentDeserializer *deserializer = [[[DocumentDeserializer alloc] init] autorelease];
self.annotation = [deserializer readAnnotation:[[(NSURL*)ref absoluteString] stringByDeletingPathExtension]];
CFRelease(ref);
}
else {
CFURLRef pdfURL = (CFURLRef)[[NSURL alloc] initFileURLWithPath:documentPath];
self.document = CGPDFDocumentCreateWithURL(pdfURL);
self.page = CGPDFDocumentGetPage(document, 1);
self.version = #"1.0";
DocumentDeserializer *deserializer = [[[DocumentDeserializer alloc] init] autorelease];
self.annotation = [deserializer readAnnotation:[[(NSURL*)pdfURL absoluteString] stringByDeletingPathExtension]];
CFRelease(pdfURL);
CGPDFPageRelease(self.page);
}
}
return self;
}
- (NSInteger)pageCount
{
return CGPDFDocumentGetNumberOfPages(self.document);
}
- (void)loadPage:(NSInteger)number
{
self.page = CGPDFDocumentGetPage(document, number);
}
- (BOOL)save
{
DocumentSerializer *serializer = [[[DocumentSerializer alloc] init] autorelease];
[serializer serialize:self];
self.dirty = NO;
return !self.dirty;
}
- (void)dealloc
{
CGPDFDocumentRelease(self.document);
if (self.annotation != nil && _annotation != nil) {
[_annotation release];
self.annotation = nil;
} //my attempt to prevent the object from being over-released
self.document = nil;
self.name = nil;
[super dealloc];
}
#end
Then I ran it through Instruments to find zombie objects, and sure enough, Instruments found a deallocated object being sent a message at the exact same #synthesize line!
Does anyone have any idea what's going on and how to fix it?

This bit looks very wrong:
if (self.annotation != nil && _annotation != nil) {
[_annotation release];
self.annotation = nil;
}
Firstly, why are you checking self.annotation and _annotation for nil-ness. That's effectively doing the same check twice.
Secondly, you're using direct ivar access to release _annotation and then the setter for annotation will be releasing _annotation again and setting _annotation = nil. Effectively it's doing this:
if (self.annotation != nil && _annotation != nil) {
[_annotation release];
[_annotation release];
_annotation = [nil retain];
}
Which as you can see, is going to over-release _annotation.
Also, seriously, just use ARC. ARC is (mainly) compile time and has nothing to do with the device or OS version it's running on. The only bit that's not supported on pre iOS 5 is auto nil-ed weak pointers. But that really shouldn't be a problem as that's totally new in Lion / iOS 5 anyway.

Related

Notification when any property changes [duplicate]

Put simply, is there a way to receive a general notification when any property in an Objective-C class is changed? I know I can use KVO to monitor particular property changes, but I have the need to call a particular method whenever any setProperty: message is sent to my class. I want to be able to receive a generic notification without any concern about which property in particular was modified.
If it helps to clarify why I want to do this, I am making use of some fast table scrolling code found here: http://blog.atebits.com/2008/12/fast-scrolling-in-tweetie-with-uitableview/
Part of the process of accomplishing this is that whenever a property in a table view cell is modified, [ self setNeedsDisplay ] needs to be called. I'd rather not have to override the setter methods for every property in my class just to make this call.
As Chuck notes, you can create a dependent key, or of course you can directly observe all the properties (which is less work than overloading the setters).
Using the Objective-C runtime, if you exclusively use properties, you can automate this process using class_copyPropertyList(). But I'd probably only do this if this problem comes up a bit for you. If you only have one instance of this problem, it's probably easier and safer and more maintainable just to directly observe the list of properties unless you feel like working in the ObjC runtime.
Here's an example built off of Chuck and Rob's suggestions:
DrakeObject.h
#interface DrakeObject : NSObject
#property (nonatomic, strong) NSNumber *age;
#property (nonatomic, strong) NSNumber *money;
#property (nonatomic, strong) NSString *startPosition;
#property (nonatomic, strong) NSString *currentPosition;
#property (nonatomic, strong, readonly) id propertiesChanged;
#end
DrakeObject.m
#implementation DrakeObject
- (instancetype)init {
self = [super init];
if (self) {
self.age = #25;
self.money = #25000000;
self.startPosition = #"bottom";
self.currentPosition = #"here";
}
return self;
}
- (id)propertiesChanged {
return nil;
}
+(NSSet *)keyPathsForValuesAffectingPropertiesChanged {
return [NSSet setWithObjects:#"age", #"money", #"startPosition", #"currentPosition", nil];
}
observing propertiesChanged will let us know anytime a property has changed.
[self.drakeObject addObserver:self
forKeyPath:#"propertiesChanged"
options:NSKeyValueObservingOptionNew
context:nil];
Not exactly. You can create a dependent key that depends on every property you wish to expose and then observe that. That's about as close as you'll get, I think.
Here an example of code. I have a general object and dother object. Dother object has to save his state on change each property.
#import <Foundation/Foundation.h>
#interface GeneralObject : NSObject
+ (instancetype)instanceWithDictionary:(NSDictionary *)aDictionary;
- (instancetype)initWithDictionary:(NSDictionary *)aDictionary;
- (NSDictionary *)dictionaryValue;
- (NSArray *)allPropertyNames;
#end
implementation
#import "GeneralObject.h"
#import <objc/runtime.h>
#implementation GeneralObject
#pragma mark - Public
+ (instancetype)instanceWithDictionary:(NSDictionary *)aDictionary {
return [[self alloc] initWithDictionary:aDictionary];
}
- (instancetype)initWithDictionary:(NSDictionary *)aDictionary {
aDictionary = [aDictionary clean];
for (NSString* propName in [self allPropertyNames]) {
[self setValue:aDictionary[propName] forKey:propName];
}
return self;
}
- (NSDictionary *)dictionaryValue {
NSMutableDictionary *result = [NSMutableDictionary dictionary];
NSArray *propertyNames = [self allPropertyNames];
id object;
for (NSString *key in propertyNames) {
object = [self valueForKey:key];
if (object) {
[result setObject:object forKey:key];
}
}
return result;
}
- (NSArray *)allPropertyNames {
unsigned count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
NSMutableArray *array = [NSMutableArray array];
unsigned i;
for (i = 0; i < count; i++) {
objc_property_t property = properties[i];
NSString *name = [NSString stringWithUTF8String:property_getName(property)];
[array addObject:name];
}
free(properties);
return array;
}
#end
and after all we have dother class, which should save his state on each change of any property
#import "GeneralObject.h"
extern NSString *const kUserDefaultsUserKey;
#interface DotherObject : GeneralObject
#property (strong, nonatomic) NSString *firstName;
#property (strong, nonatomic) NSString *lastName;
#property (strong, nonatomic) NSString *email;
#end
and implementation
#import "DotherObject.h"
NSString *const kUserDefaultsUserKey = #"CurrentUserKey";
#implementation DotherObject
- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
if (self = [super initWithDictionary:dictionary]) {
for (NSString *key in [self allPropertyNames]) {
[self addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil];
}
}
return self;
}
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context {
NSDictionary *dict = [self dictionaryValue];
[[NSUserDefaults standardUserDefaults] setObject:dict forKey:kUserDefaultsUserKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSString *)description {
return [NSString stringWithFormat:#"%#; dict:\n%#", [super description], [self dictionaryValue]];
}
#end
Happy coding!

MKMapKit - EXC_BAD_ACCESS when looping through MKAnnotations

I've been stuck on this EXC_BAD_ACCESS error 2 days now. I have a reloadAnnotations method that removes all annotations before adding new annotations. Before removing the annotation this method should be checking to see if the new set contains the same location so it's not removed and re-added. But as soon as I try to trace out the current annotation title I get this error Thread 1: Program received signal: "EXC_BAD_ACCESS"
And when I view the annotation in the debugger the title property says "Invalid Summary". It must be caused by a value not being retained but I've tried everything and can't figure it out.
Why can't I log the annotation title to NSLog?
And why can't I compare each title and coords to other objects?
BrowseController.m
-(void)reloadAnnotations
{
NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:10];
for (id annotation in _mapView.annotations) {
if (annotation != _mapView.userLocation) {
//ParkAnnotation *pa = (ParkAnnotation *)annotation;
ParkAnnotation *pa = annotation;
NSLog(#"pa.title %#", pa.title); // Thread 1: Program received signal: "EXC_BAD_ACCESS"
[toRemove addObject:annotation];
}
}
// DON'T REMOVE IT IF IT'S ALREADY ON THE MAP!!!!!!
for(RKLocation *loc in locations)
{
CLLocationCoordinate2D location;
location.latitude = (double)[loc.lat doubleValue];
location.longitude = (double)[loc.lng doubleValue];
ParkAnnotation *parkAnnotation = [[ParkAnnotation alloc] initWithTitle:loc.name andCoordinate:location];
[_mapView addAnnotation:parkAnnotation];
}
[_mapView removeAnnotations:toRemove];
}
- (MKAnnotationView *)mapView:(MKMapView *)map viewForAnnotation:(id <MKAnnotation>)annotation
{
NSLog(#"BrowseViewController map viewForAnnotation");
MKPinAnnotationView *pin = (MKPinAnnotationView *)[_mapView dequeueReusableAnnotationViewWithIdentifier: #"anIdentifier"];
if (pin == nil){
pin = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier: #"anIdentifier"] autorelease];
pin.pinColor = MKPinAnnotationColorRed;
pin.animatesDrop = YES;
pin.canShowCallout = YES;
}
else{
pin.annotation = annotation;
}
return pin;
}
ParkAnnotation.h
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
#interface ParkAnnotation : NSObject <MKAnnotation> {
NSString *title;
CLLocationCoordinate2D coordinate;
}
#property (nonatomic, copy) NSString *title;
#property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
- (id)initWithTitle:(NSString *)ttl andCoordinate:(CLLocationCoordinate2D)c2d;
#end
ParkAnnotation.m (edited: see Wolfgangs comments below )
#import "ParkAnnotation.h"
#implementation ParkAnnotation
#synthesize title, coordinate;
- (id)initWithTitle:(NSString *)ttl andCoordinate:(CLLocationCoordinate2D)c2d {
self = [super init];
if (self) {
title = ttl;
coordinate = c2d;
}
return self;
}
- (void)dealloc {
[title release];
[super dealloc];
}
#end
Although you have declared title has a copy type property, it never is copied as you don't use the setter method and directly assigned. You are even releasing it without ownership. Change it like this,
title = [ttl copy];
The initializer in ParkAnnotation.m isn't written following ObjC conventions. The self variable is never set, the designated initializer of a class should follow the following pattern:
- (id)init
{
self = [super init];
if (self)
{
/* custom initialization here ... */
}
return self;
}
Since self is not set, the accessor methods used in the caller will fail; the container object (inside ParkAnnotation.m referenced with self) will be nil or some bogus value when trying to access a property inside the object from another class.

Memory leak with objective-c on alloc

When I use Instruments to find memory leaks, a leak is detected on
Horaires *jour;
jour= [[Horaires alloc] init]; // memory leak reported here by Instruments
self.lundi = jour;
[jour release];
and I don't know why there is a leak at this point.
Does anyone can help me? Here's the code.
// HorairesCollection.h
#import <Foundation/Foundation.h>
#import "Horaires.h"
#interface HorairesCollection : NSObject < NSCopying > {
Horaires *lundi;
}
#property (nonatomic, retain) Horaires *lundi;
-init;
-(void)dealloc;
#end
// HorairesCollection.m
#import "HorairesCollection.h"
#implementation HorairesCollection
#synthesize lundi;
-(id)copyWithZone:(NSZone *)zone{
HorairesCollection *another = [[HorairesCollection alloc] init];
another.lundi = [lundi copyWithZone: zone];
[another autorelease];
return another;
}
-init{
self = [super init];
Horaires *jour;
jour= [[Horaires alloc] init]; // memory leak reported here by Instruments
self.lundi = jour;
[jour release];
return self;
}
- (void)dealloc {
[lundi release];
[super dealloc];
}
#end
// Horaires.h
#import <Foundation/Foundation.h>
#interface Horaires : NSObject <NSCopying>{
BOOL ferme;
BOOL h24;
NSString *h1;
}
#property (nonatomic, assign) BOOL ferme;
#property (nonatomic, assign) BOOL h24;
#property (nonatomic, retain) NSString *h1;
-init;
-(id)copyWithZone:(NSZone *)zone;
-(void)dealloc;
#end
// Horaires.m
#import "Horaires.h"
#implementation Horaires
-(BOOL) ferme {
return ferme;
}
-(void)setFerme:(BOOL)bFerme{
ferme = bFerme;
if (ferme) {
self.h1 = #"";
self.h24 = NO;
}
}
-(BOOL) h24 {
return h24;
}
-(void)setH24:(BOOL)bH24{
h24 = bH24;
if (h24) {
self.h1 = #"";
self.ferme = NO;
}
}
-(NSString *) h1 {
return h1;
}
-(void)setH1:(NSString *)horaire{
[horaire retain];
[h1 release];
h1 = horaire;
if (![h1 isEqualToString:#""]) {
self.h24 = NO;
self.ferme = NO;
}
}
-(id)copyWithZone:(NSZone *)zone{
Horaires *another = [[Horaires alloc] init];
another.ferme = self.ferme;
another.h24 = self.h24;
another.h1 = self.h1;
[another autorelease];
return another;
}
-init{
self = [super init];
return self;
}
-(void)dealloc {
[h1 release];
[super dealloc];
}
#end
You've set your property to retain and you alloc and release the variable, so from what I can see the code is okay and Instruments has given you a false warning.
I think your copyWithZone: might have a leak, though. [lundi copyWithZone:] will retain a copy of lundi but you never release it. So you need an extra release, something like this:
-(id)copyWithZone:(NSZone *)zone{
DefibHoraires *another = [[DefibHoraires alloc] init];
Horaires* makeCopy = [lundi copyWithZone: zone];
another.lundi = makeCopy;
[makeCopy release];
return another;
}
This is because copy and alloc both return retained object instances and you need to manually release them when you're finished with them. You did that correctly for your alloc'd objects but not the copy.
That init method looks ok, although it should be implemented (and typed) as
-(id)init
{
if (self = [super init])
{
...
}
return self;
}
or a similar pattern.
Your copyWithZone implementations are wrong, they need to return a retained object, so do not autorelease the returned value. But you need to release your copy of lundi, because you are using the retaining setter.
-(id)copyWithZone:(NSZone *)zone{
DefibHoraires *another = [[DefibHoraires alloc] init];
Horaires *lundiCopy = [lundi copyWithZone:zone];
another.lundi = lundiCopy;
[lundiCopy release];
return another;
}
I don't know why you return an instance of DefibHoraires here, shouldn't it be a HorairesCollection?
Maybe the wrong copyWithZone: method is responsible for the reported leak (it's a leak anyway).
One further note: It's a good defensive rule to use (copy) for NSString properties instead of (retain) to remove side effects when passing NSMutableString instead.
I don't have an answer but I do have some general comments:
In copyWithZone: you should use allocWithZone: (passing the same zone as a parameter) to allocate the object you are going to return.
copyWithZone: should return a retained object. Don't autorelease it.
You are not supposed to use properties in init. Your init should look something like:
-init
{
self = [super init];
if (self != nil)
{
lundi = [[Horaires alloc] init]; // assign the ivar directly
}
return self;
}
In your copyWithZone: for HorairesCollection you have a leak. It should look like:
-(id)copyWithZone:(NSZone *)zone{
DefibHoraires *another = [[DefibHoraires allocWithZone: zone] init];
another.lundi = [[lundi copyWithZone: zone] autorelease];
return another;
}

incompatible Objective-c types warning

I have a user class that I use through the iPhone application, this is the init and initWithUser functions from my user class (A SUBCLASS OF NSobject), when I use the initWithUser function I get the warning described after the code. please advise.
// serialize.h
#import <Foundation/Foundation.h>
#protocol Serialize
// serialize the object to an xml string
-(NSString*)ToXML;
#end
// user.h
#import <Foundation/Foundation.h>
#import "Serialize.h"
#import "Contact.h"
#interface User : NSObject <Serialize> {
NSString *email;
NSString *firstName;
NSString *lastName;
NSString *userId;
NSString *userName;
NSString *password;
NSMutableArray *contactList;
}
#property (nonatomic,copy) NSString *email;
#property (nonatomic,copy) NSString *firstName;
#property (nonatomic,copy) NSString *lastName;
#property (nonatomic,copy) NSString *userId;
#property (nonatomic,copy) NSString *userName;
#property (nonatomic,copy) NSString *password;
#property (nonatomic, retain) NSMutableArray *contactList;
//-(id)init;
-(id)initWithUser:(User *)copyUser;
#end
// user.m
#import "user.h"
#implementation User
#synthesize email;
#synthesize firstName;
#synthesize lastName;
#synthesize userId;
#synthesize userName;
#synthesize password;
#synthesize contactList;
-(id)init
{
// call init in parent and assign to self
if( (self = [super init]) )
{
// do something specific
contactList = [[NSMutableArray alloc] init];
}
return self;
}
-(id)initWithUser:(User *)copyUser
{
if( (self = [self init]) ) {
email = copyUser.email;
firstName = copyUser.firstName;
lastName = copyUser.lastName;
userId = copyUser.userId;
userName = copyUser.userName;
password = copyUser.password;
// release contactList initialized in the init
[contactList release];
contactList = [copyUser.contactList mutableCopy];
}
return self;
}
- (void)dealloc
{
// TODO:
[contactList removeAllObjects];
[contactList release];
[super dealloc];
}
// implementation of serialize protocol
-(NSString*)ToXML
{
return #"";
}
and I use it in the main controller this way
- (void) registerNewUser {
RegistrationViewController *regController = [[RegistrationViewController alloc] init] ;
regController.newUser = [[User alloc] initWithUser:self.user];
[self.navigationController pushViewController:regController animated:YES];
[regController release];
}
the line
regController.newUser = [[User alloc] initWithUser:self.user];
gives me the following error, and its been driving me nuts for a couple of days:
incompatible Objective-c types 'struct User*', expected 'struct NSString *' when passing argument 1 of 'initWithUser:' from distinct Objective-c type
any help and guidance is appreciated
The problem is you have an ambiguous selector. Because alloc returns id, the call to initWithUser: has become ambiguous. NSUserDefaults also has an initWithUser: function which takes a string. The compiler thinks you're trying to use that one. Change the line to
regController.newUser = [(User*)[User alloc] initWithUser:self.user];
and everything should work as expected.
As mentioned in the comments, there are other problems with your implementation. In your initializer, reusing the -init is redundant and the assignments to ivars like email should be taking ownership of the data using -copy or -retain:
-(id)initWithUser:(User *)copyUser {
if((self = [super init])) {
// take ownership of the users data by copying or retaining:
email = [copyUser.email copy];
// ...
contactList = [copyUser.contactList mutableCopy];
}
return self;
}
In -dealloc, -removeAllObjects can be removed and the member data has to be released:
- (void)dealloc {
[email release];
// ...
[contactList release];
[super dealloc];
}
Note that you are also leaking the new User instance if newUser is a copy or retain property as there is a release missing:
User *user = [[User alloc] initWithUser:self.user];
regController.newUser = user;
[user release];

Observing a Change to ANY Class Property in Objective-C

Put simply, is there a way to receive a general notification when any property in an Objective-C class is changed? I know I can use KVO to monitor particular property changes, but I have the need to call a particular method whenever any setProperty: message is sent to my class. I want to be able to receive a generic notification without any concern about which property in particular was modified.
If it helps to clarify why I want to do this, I am making use of some fast table scrolling code found here: http://blog.atebits.com/2008/12/fast-scrolling-in-tweetie-with-uitableview/
Part of the process of accomplishing this is that whenever a property in a table view cell is modified, [ self setNeedsDisplay ] needs to be called. I'd rather not have to override the setter methods for every property in my class just to make this call.
As Chuck notes, you can create a dependent key, or of course you can directly observe all the properties (which is less work than overloading the setters).
Using the Objective-C runtime, if you exclusively use properties, you can automate this process using class_copyPropertyList(). But I'd probably only do this if this problem comes up a bit for you. If you only have one instance of this problem, it's probably easier and safer and more maintainable just to directly observe the list of properties unless you feel like working in the ObjC runtime.
Here's an example built off of Chuck and Rob's suggestions:
DrakeObject.h
#interface DrakeObject : NSObject
#property (nonatomic, strong) NSNumber *age;
#property (nonatomic, strong) NSNumber *money;
#property (nonatomic, strong) NSString *startPosition;
#property (nonatomic, strong) NSString *currentPosition;
#property (nonatomic, strong, readonly) id propertiesChanged;
#end
DrakeObject.m
#implementation DrakeObject
- (instancetype)init {
self = [super init];
if (self) {
self.age = #25;
self.money = #25000000;
self.startPosition = #"bottom";
self.currentPosition = #"here";
}
return self;
}
- (id)propertiesChanged {
return nil;
}
+(NSSet *)keyPathsForValuesAffectingPropertiesChanged {
return [NSSet setWithObjects:#"age", #"money", #"startPosition", #"currentPosition", nil];
}
observing propertiesChanged will let us know anytime a property has changed.
[self.drakeObject addObserver:self
forKeyPath:#"propertiesChanged"
options:NSKeyValueObservingOptionNew
context:nil];
Not exactly. You can create a dependent key that depends on every property you wish to expose and then observe that. That's about as close as you'll get, I think.
Here an example of code. I have a general object and dother object. Dother object has to save his state on change each property.
#import <Foundation/Foundation.h>
#interface GeneralObject : NSObject
+ (instancetype)instanceWithDictionary:(NSDictionary *)aDictionary;
- (instancetype)initWithDictionary:(NSDictionary *)aDictionary;
- (NSDictionary *)dictionaryValue;
- (NSArray *)allPropertyNames;
#end
implementation
#import "GeneralObject.h"
#import <objc/runtime.h>
#implementation GeneralObject
#pragma mark - Public
+ (instancetype)instanceWithDictionary:(NSDictionary *)aDictionary {
return [[self alloc] initWithDictionary:aDictionary];
}
- (instancetype)initWithDictionary:(NSDictionary *)aDictionary {
aDictionary = [aDictionary clean];
for (NSString* propName in [self allPropertyNames]) {
[self setValue:aDictionary[propName] forKey:propName];
}
return self;
}
- (NSDictionary *)dictionaryValue {
NSMutableDictionary *result = [NSMutableDictionary dictionary];
NSArray *propertyNames = [self allPropertyNames];
id object;
for (NSString *key in propertyNames) {
object = [self valueForKey:key];
if (object) {
[result setObject:object forKey:key];
}
}
return result;
}
- (NSArray *)allPropertyNames {
unsigned count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
NSMutableArray *array = [NSMutableArray array];
unsigned i;
for (i = 0; i < count; i++) {
objc_property_t property = properties[i];
NSString *name = [NSString stringWithUTF8String:property_getName(property)];
[array addObject:name];
}
free(properties);
return array;
}
#end
and after all we have dother class, which should save his state on each change of any property
#import "GeneralObject.h"
extern NSString *const kUserDefaultsUserKey;
#interface DotherObject : GeneralObject
#property (strong, nonatomic) NSString *firstName;
#property (strong, nonatomic) NSString *lastName;
#property (strong, nonatomic) NSString *email;
#end
and implementation
#import "DotherObject.h"
NSString *const kUserDefaultsUserKey = #"CurrentUserKey";
#implementation DotherObject
- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
if (self = [super initWithDictionary:dictionary]) {
for (NSString *key in [self allPropertyNames]) {
[self addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil];
}
}
return self;
}
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context {
NSDictionary *dict = [self dictionaryValue];
[[NSUserDefaults standardUserDefaults] setObject:dict forKey:kUserDefaultsUserKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSString *)description {
return [NSString stringWithFormat:#"%#; dict:\n%#", [super description], [self dictionaryValue]];
}
#end
Happy coding!