macosaddressbookabaddressbookosx-elcapitanabperson

How to observe ABPersonView changes for ABPerson


I know that ABPersonView is not KVO complaint. My issue is that despite declared property of ABPersonView being retained every time I access the property I get different object. Am I doing something wrong or is this correct that every time there was a change in ABPersonView I have to update model with new ABPerson object? Using El Capitan GM.

ABPersonView:

@property (readwrite, retain) ABPerson *person;
// An ABPerson record for display.
// Raises if person originates from ABAddressBook's +sharedAddressBook.
// Person must be exist in an ABAddressBook created and manipulated on the main thread only.
// When person is nil, displays an empty selection state. 

Code:

#import "AppDelegate.h"
@import AddressBook;
static void * ABPersonVCContext = &ABPersonVCContext;

@interface AppDelegate ()

@property (weak) IBOutlet NSWindow *window;
@property (strong) ABPerson *person;
@property (strong) ABPersonView *personView;
@property (strong) ABAddressBook *book;
@property (assign, getter=isEditing) BOOL editing;
@property NSTimer *timer;
@end

@implementation AppDelegate

- (instancetype)init {
  self = [super init];
  if (self) {
    _book = [[ABAddressBook alloc] init];
    NSString *vCardRepresentation = @"BEGIN:VCARD\r\nVERSION:3.0\r\nN:AA;BB;;;\r\nFN:\r\nEND:VCARD\r\n";
    NSData *vCardData = [vCardRepresentation dataUsingEncoding:NSUTF8StringEncoding];
    _person = [[ABPerson alloc] initWithVCardRepresentation:vCardData];
    [_book addRecord:_person];
    [self addObserver:self forKeyPath:@"editing"
              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
              context:ABPersonVCContext];
#ifdef DEBUG
    NSLog(@"%s %d %s", __FILE__, __LINE__, __PRETTY_FUNCTION__);
    NSLog(@"%@",_person);
#endif
  }
  return self;
}

- (void)awakeFromNib
{
  self.personView = [[ABPersonView alloc] initWithFrame:self.window.contentView.frame];
  self.personView.person = self.person;
  [self.window.contentView addSubview:self.personView];
  self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(reverseEditing) userInfo:NULL repeats:YES];
  [self.timer fire];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
  if (context == ABPersonVCContext) {
    if ([keyPath isEqualTo:@"editing"]) {
#ifdef DEBUG
      NSLog(@"%s %d %s", __FILE__, __LINE__, __PRETTY_FUNCTION__);
      NSLog(@"%@",self.personView.person);
#endif
    }
  } else {
    @try {
      [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
    @catch (NSException *exception) {
      ;
    }
    @finally {
      ;
    }
  }
}

- (void)reverseEditing
{
  self.editing = !self.editing;
}

@end

EDIT: The new object comes from different addressBook instance:

(lldb) po [newPerson addressBook]
<ABAddressBook: 0x6080000d50e0>

(lldb) po self.book
<ABAddressBook: 0x6080000c4130>

(lldb) po [self.person addressBook]
<ABAddressBook: 0x6080000c4130>

EDIT2: Even registering for notifications does not help because different object is being modified.

NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(changeOccured:) name:kABDatabaseChangedNotification object:nil];
[nc addObserver:self selector:@selector(changeOccured:) name:kABDatabaseChangedExternallyNotification object:nil];

Solution

  • Unfortunately every call to person property of personView triggers ABPersonViewAPIAdapter that converts CNContact to ABPerson. So if one doesn't want to use CNContact on El Capitan he has to propagate edited ABPerson back to the model object.

    One can try following code (hope this will save some time to someone)

      NSLog(@"%@",[self.personView performSelector:@selector(addressBook) withObject:nil]);
      NSLog(@"%@",[self.personView performSelector:@selector(_APIAdapter) withObject:nil]);
      NSLog(@"%@",[self.personView performSelector:@selector(_contact) withObject:nil]);