iOSでABAddressBookを使って連絡先情報を取得する

先日、大きな連絡先をリリースしたけど、iOSで連絡先の情報を一括で取得するのが以外に難しく書籍も探してみるもあまり情報が無くて困ったわけですよ。

情報が無いならブログ書くしかないと言うわけで書いてみます。

今回のサンプルで作ったプロジェクトはtochi/ContactSample · GitHubからどうぞ。

サンプルコードの簡単な説明。

連絡先から連絡先情報を取得する

iOSで連絡先を取得するにはAddressBook.frameworkを使います。

そして、厄介なのがプログラムから連絡先にアクセスして全ての連絡先を取得するにはObjective-CライクなメソッドではなくてC言語?ライクなメソッド経由でアクセスする必要があります。

まずは1行目のABAddressBookCreateで連絡先のインスタンス作って、2行目のABAddressBookCopyArrayOfAllPeopleで連絡先から全ての連絡先情報を取得します。

ABAddressBookRef addressBook = ABAddressBookCreate();
CFArrayRef records = ABAddressBookCopyArrayOfAllPeople(addressBook);

で取得した連絡先情報をfor文でグルグル回しながら、3行目のCFArrayGetValueAtIndexで連絡先を1件づつ取り出します。

for (int i = 0; i < CFArrayGetCount(records); i++) {
  @autoreleasepool {
    ABRecordRef person = CFArrayGetValueAtIndex(records, i);
  }
}

取り出した連絡先からkABPersonFirstNamePropertyとかkABPersonLastNamePropertyとかのプロパティを使って名前やら電話番号やらメールアドレスを取り出します。

その他のプロパティはAppleのABPerson Referenceを参照のこと。

NSString *firstName = (__bridge_transfer NSString *)ABRecordCopyValue(person, kABPersonFirstNameProperty);

あとはCoreDataで保存してあげればOKなんですが、iOS標準の連絡先で変更、削除、追加された場合のことを考慮しなければいけません。 そこで、ABRecordGetRecordIDという連絡先で持っている一意のキーを取得してCoreData側でも保持してあげます。 で、このIDをKeyにしてCoreData側を毎回検索して、あれば更新、なければ新規で連絡先を更新します。

NSNumber *recordId = [NSNumber numberWithInteger:ABRecordGetRecordID(person)];

ただこれだけだと、連絡先を削除した場合に反映されないわけですよ。 そこで、苦肉の策としてDeleteFlagを持たせて更新の時にDeleteFlagを更新して。 最後にDeleteFlagが更新されていないデータ1〜5行目で取得して、10〜17行目でグルグル回してCoreDataからデータを削除しています。

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@”Contact”];
request.predicate = [NSPredicate predicateWithFormat:@”deleteFlag == %@”, [NSNumber numberWithBool:deleteFlag]];
request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@”recordId” ascending:YES]];
error = nil;
NSArray *contacts = [self.managedObjectContext executeFetchRequest:request error:&error];
if (error != nil) {
  NSLog(@”Error:%@”, error);
  abort();
}
for (Contact *contact in contacts) {
  [self.managedObjectContext deleteObject:contact];
}
error = nil;
if ([self.managedObjectContext save:&error] == NO) {
  NSLog(@”Error:%@”, error);
  abort();
}

なんだかスマートじゃ無いんですけどね。。。

連絡先の更新状態を受け取る

で実はABAddressBookには便利な機能があって、iOS標準の連絡先では連絡先が追加・更新・削除された時に知らせてくれる機能があるんだよっと、アノiTunesの総合ランキングで上位にランクインした特殊文字@haranicleさんが教えてくれたので試してみました。そして、ここで再度ハマります。。。

ABAddressBookRegisterExternalChangeCallbackというメソッドに更新された際に呼び出されるメソッドを登録して上げます。

ABAddressBookRegisterExternalChangeCallback(_addressBook, _addressBookChanged, (__bridge_retained void *)self);

第1引数にはAddressBookのインスタンスを、第2引数には呼び出されたいコールバックメソッドを、第3引数には呼び出されたいメソッドを実装しているクラスを。

で、ここで注意点1:このコールバックメソッドは連絡先で更新されたあとに、自アプリが起動されたタイミングで呼び出されます。 更新されたらすぐに呼び出されるわけじゃないよ。

注意点2:第1引数に渡すAddressBookのインスタンスがリリース(解放)されるとコールバックメソッドは呼び出されない。NSNotificationCenterみたいにOSに登録する感じでは無くてあくまでも自アプリ内のAddressBookのインスタンスがコールバックメソッドを保持している感じです。(ここでハマりました) しかも、今回のサンプルでは連絡先情報の更新処理をGCDを使って別スレッドでやっているんですけど、AddressBookのインスタンスはスレッド間で渡せない。。。(ここでもハマりました。。。リファレンスに書いてあるのでちゃんと読めと。) なので、第1引数に渡すAddressBookのインスタンスはインスタンス変数としなければなりません。

注意点3:第3引数に渡すクラスはARCがONの場合にはbridgeで変換が必要です。

あとはコールバックメソッド内で連絡先の更新処理を読んであげれば連絡先が更新された時だけ更新処理が走るようになります。 ただし、コールバックメソッドは更新されたことを知らせてはくれるけど、どの連絡先が更新・追加・削除されたかは教えてくれません。(多分) ただし、ここは疑問なんだけどコールバックメソッドを一度このタイミングで取り消して再度登録してあげないと上手く動かなかった。。。なぜ? そして、連絡先を1件しか更新していないのに、コールバックメソッドが複数回(3回)呼ばれるのはなぜ?

void _addressBookChanged (ABAddressBookRef addressBook, CFDictionaryRef info, void *context) {
  ABAddressBookUnregisterExternalChangeCallback(addressBook, _addressBookChanged, context);
  ViewController *viewController = (__bridge_transfer ViewController *)context;
  [viewController _updateContact];
}

と言うことで不明点を残したままですが何となく動きました。

きっと有識者の方がこの疑問点にも答えてくれるはず。w

不明点

  1. 連絡先を1件しか登録していないのに、なぜコールバックメソッドが複数回呼ばれるのか?
  2. コールバックメソッドの登録・解除を行わないと行けないのはなぜ?
  3. 更新された連絡先がどれなのかを知る方法は無いの?