29 | ```
30 | Getting an instance of the addressbook.
31 |
32 | ```objectivec
33 | RHAddressBook *ab = [[[RHAddressBook alloc] init] autorelease];
34 | ```
35 | Support for iOS6+ authorization
36 |
37 | ```objectivec
38 | //query current status, pre iOS6 always returns Authorized
39 | if ([RHAddressBook authorizationStatus] == RHAuthorizationStatusNotDetermined){
40 |
41 | //request authorization
42 | [ab requestAuthorizationWithCompletion:^(bool granted, NSError *error) {
43 | [abViewController setAddressBook:ab];
44 | }];
45 | }
46 | ```
47 | Registering for addressbook changes
48 |
49 | ```objectivec
50 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(addressBookChanged:) name:RHAddressBookExternalChangeNotification object:nil];
51 | ```
52 | Getting sources.
53 |
54 | ```objectivec
55 | NSArray *sources = [ab sources];
56 | RHSource *defaultSource = [ab defaultSource];
57 | ```
58 | Getting a list of groups.
59 |
60 | ```objectivec
61 | NSArray *groups = [ab groups];
62 | long numberOfGroups = [ab numberOfGroups];
63 | NSArray *groupsInSource = [ab groupsInSource:defaultSource];
64 | RHGroup *lastGroup = [groups lastObject];
65 | ```
66 | Getting a list of people.
67 |
68 | ```objectivec
69 | NSArray *allPeople = [ab people];
70 | long numberOfPeople = [ab numberOfPeople];
71 | NSArray *allPeopleSorted = [ab peopleOrderedByUsersPreference];
72 | NSArray *allFreds = [ab peopleWithName:@"Fred"];
73 | NSArray *allFredsInLastGroup = [lastGroup peopleWithName:@"Fred"];
74 | RHPerson *person = [allPeople lastObject];
75 | ```
76 | Getting basic properties on on a person.
77 |
78 | ```objectivec
79 | NSString *department = [person department];
80 | UIImage *thumbnail = [person thumbnail];
81 | BOOL isCompany = [person isOrganization];
82 | ```
83 | Setting basic properties on a person.
84 |
85 | ```objectivec
86 | person.name = @"Freddie";
87 | [person setImage:[UIImage imageNames:@"hahaha.jpg"]];
88 | person.kind = kABPersonKindOrganization;
89 | [person save];
90 | ```
91 | Getting MultiValue properties on a person.
92 |
93 | ```objectivec
94 | RHMultiDictionaryValue *addressesMultiValue = [person addresses];
95 | NSString *firstAddressLabel = [RHPerson localizedLabel:[addressesMultiValue labelAtIndex]]; //eg Home
96 | NSDictionary *firstAddress = [addressesMultiValue valueAtIndex:0];
97 | ```
98 | Setting MultiValue properties on a person.
99 |
100 | ```objectivec
101 | RHMultiStringValue *phoneMultiValue = [person phoneNumbers];
102 | RHMutableMultiStringValue *mutablePhoneMultiValue = [[phoneMultiValue mutableCopy] autorelease];
103 | if (! mutablePhoneMultiValue) mutablePhoneMultiValue = [[[RHMutableMultiStringValue alloc] initWithType:kABMultiStringPropertyType] autorelease];
104 |
105 | //RHPersonPhoneIPhoneLabel casts kABPersonPhoneIPhoneLabel to the correct toll free bridged type, see RHPersonLabels.h
106 | mutablePhoneMultiValue addValue:@"+14086655555" withLabel:RHPersonPhoneIPhoneLabel];
107 | person.phonenumbers = mutablePhoneMultiValue;
108 | [person save];
109 | ```
110 | Creating a new person.
111 |
112 | ```objectivec
113 | RHPerson *newPerson = [[ab newPersonInDefaultSource] autorelease]; //added to ab
114 | RHPerson *newPerson2 = [[[RHPerson newPersonInSource:[ab defaultSource]] autorelease]; //not added to ab
115 | [ab addPerson:newPerson2];
116 | NSError* error = nil;
117 | if (![ab save:&error]) NSLog(@"error saving: %@", error);
118 | ```
119 | Getting an RHPerson object for an ABRecordRef for editing. (note: RHPerson might not be associated with the same addressbook as the original ABRecordRef)
120 |
121 | ```objectivec
122 | ABRecordRef personRef = ...;
123 | RHPerson *person = [ab personForRecordRef:personRef];
124 | if(person){
125 | person.firstName = @"Paul";
126 | person.lastName = @"Frank";
127 | [person save];
128 | }
129 | ```
130 | Presenting / editing an RHPerson instance in a ABPersonViewController.
131 |
132 | ```objectivec
133 | ABPersonViewController *personViewController = [[[ABPersonViewController alloc] init] autorelease];
134 |
135 | //setup (tell the view controller to use our underlying address book instance, so our person object is directly updated on our behalf)
136 | [person.addressBook performAddressBookAction:^(ABAddressBookRef addressBookRef) {
137 | personViewController.addressBook =addressBookRef;
138 | } waitUntilDone:YES];
139 |
140 | personViewController.displayedPerson = person.recordRef;
141 | personViewController.allowsEditing = YES;
142 |
143 | [self.navigationController pushViewController:personViewController animated:YES];
144 | ```
145 | Background geocoding
146 |
147 | ```objectivec
148 | if ([RHAddressBook isGeocodingSupported){
149 | [RHAddressBook setPreemptiveGeocodingEnabled:YES]; //class method
150 | }
151 | float progress = [_addressBook preemptiveGeocodingProgress]; // 0.0f - 1.0f
152 | ```
153 | Geocoding results for a person.
154 |
155 | ```objectivec
156 | CLLocation *location = [person locationForAddressID:0];
157 | CLPlacemark *placemark = [person placemarkForAddressID:0];
158 | ```
159 |
160 | Finding people within distance of a location.
161 |
162 | ```objectivec
163 | NSArray *inRangePeople = [ab peopleWithinDistance:5000 ofLocation:location];
164 | NSLog(@"people:%@", inRangePeople);
165 | ```
166 |
167 | Saving. (all of the below are equivalent)
168 |
169 | ```objectivec
170 | BOOL changes = [ab hasUnsavedChanges];
171 | BOOL result = [ab save];
172 | BOOL result =[source save];
173 | BOOL result =[group save];
174 | BOOL result =[person save];
175 | ```
176 | Reverting changes on objects. (reverts the entire addressbook instance, not just the object revert is called on.)
177 |
178 | ```objectivec
179 | [ab revert];
180 | [source revert];
181 | [group revert];
182 | [person revert];
183 | ```
184 | Remember, save often in order to avoid painful save conflicts.
185 |
186 | ## Installing
187 | For instructions on how to get started using this static library see [Using Static iOS Libraries](http://rheard.com/blog/using-static-ios-libraries/) at [rheard.com](http://rheard.com).
188 |
189 | ## Licence
190 | Released under the Modified BSD License.
191 | (Attribution Required)
192 |
193 | RHAddressBook
194 |
195 | Copyright (c) 2011-2012 Richard Heard. All rights reserved.
196 |
197 | Redistribution and use in source and binary forms, with or without
198 | modification, are permitted provided that the following conditions
199 | are met:
200 | 1. Redistributions of source code must retain the above copyright
201 | notice, this list of conditions and the following disclaimer.
202 | 2. Redistributions in binary form must reproduce the above copyright
203 | notice, this list of conditions and the following disclaimer in the
204 | documentation and/or other materials provided with the distribution.
205 | 3. The name of the author may not be used to endorse or promote products
206 | derived from this software without specific prior written permission.
207 |
208 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
209 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
210 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
211 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
212 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
213 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
214 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
215 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
216 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
217 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
218 |
219 |
220 |
221 | ### iOS Version Support (Executive Summary: Supports iOS 4+, tested on iOS 4.0 - 7.1)
222 | This Framework code runs and compiles on and has been tested all the way back to iOS 4.0.
223 |
224 | Unit tests are in place that run on all versions between 4.0 and 7.1.
225 |
226 | Various methods are not available when linking against older SDKs and will return nil when running on older os versions.
227 | eg. Geocoding is only supported on iOS 5+. You should always use the +[RHAddressBook isGeocodingAvailable] method to check whether geocoding is available before attempting to access geocode information. Methods will however, if available safely return nil / empty arrays.
228 |
--------------------------------------------------------------------------------
/RHAddressBook/RHAddressBookGeoResult.m:
--------------------------------------------------------------------------------
1 | //
2 | // RHAddressBookGeoResult.m
3 | // RHAddressBook
4 | //
5 | // Created by Richard Heard on 12/11/11.
6 | // Copyright (c) 2011 Richard Heard. All rights reserved.
7 | //
8 | // Redistribution and use in source and binary forms, with or without
9 | // modification, are permitted provided that the following conditions
10 | // are met:
11 | // 1. Redistributions of source code must retain the above copyright
12 | // notice, this list of conditions and the following disclaimer.
13 | // 2. Redistributions in binary form must reproduce the above copyright
14 | // notice, this list of conditions and the following disclaimer in the
15 | // documentation and/or other materials provided with the distribution.
16 | // 3. The name of the author may not be used to endorse or promote products
17 | // derived from this software without specific prior written permission.
18 | //
19 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 | // IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 | //
30 |
31 | #import "RHAddressBookGeoResult.h"
32 |
33 | #if RH_AB_INCLUDE_GEOCODING
34 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
35 |
36 | #import //for hashing functions
37 | #import //for geo
38 | #import "RHAddressBook.h" // for logging
39 | #import "RHAddressBookSharedServices.h" // for isGeocodingSupported
40 |
41 | @interface RHAddressBookGeoResult ()
42 | @property (readwrite, retain) CLPlacemark *placemark;
43 | @property (readwrite, assign) BOOL resultNotFound;
44 | @end
45 |
46 | @implementation RHAddressBookGeoResult {
47 |
48 | CLGeocoder *_geocoder; //only a valid pointer while performing a geo operation
49 | }
50 |
51 | @synthesize placemark=_placemark;
52 | @synthesize personID=_personID;
53 | @synthesize addressID=_addressID;
54 | @synthesize addressHash=_addressHash;
55 | @synthesize resultNotFound=_resultNotFound;
56 |
57 | -(CLLocation*)location{
58 | return _placemark.location;
59 | }
60 |
61 | -(instancetype)init {
62 | [NSException raise:NSInvalidArgumentException format:@"Unable to create a GeoResult without a personID and addressID."];
63 | return nil;
64 | }
65 |
66 | -(instancetype)initWithPersonID:(ABRecordID)personID addressID:(ABMultiValueIdentifier)addressID {
67 | self = [super init];
68 | if (self) {
69 | _personID = personID;
70 | _addressID = addressID;
71 |
72 | //compute address hash and store it
73 | _addressHash = arc_retain([RHAddressBookGeoResult hashForDictionary:[self associatedAddressDictionary]]);
74 | }
75 | return self;
76 | }
77 |
78 |
79 | -(instancetype)initWithCoder:(NSCoder *)coder {
80 | self = [super init];
81 | if (self) {
82 | _placemark = arc_retain([coder decodeObjectForKey:@"placemark"]);
83 | _personID = [coder decodeInt32ForKey:@"personID"];
84 | _addressID = [coder decodeInt32ForKey:@"addressID"];
85 | _addressHash = arc_retain([coder decodeObjectForKey:@"addressHash"]);
86 | _resultNotFound = [coder decodeBoolForKey:@"resultNotFound"];
87 | }
88 | return self;
89 | }
90 |
91 | -(void)encodeWithCoder:(NSCoder *)coder{
92 | [coder encodeObject:_placemark forKey:@"placemark"];
93 | [coder encodeInt32:_personID forKey:@"personID"];
94 | [coder encodeInt32:_addressID forKey:@"addressID"];
95 | [coder encodeObject:_addressHash forKey:@"addressHash"];
96 | [coder encodeBool:_resultNotFound forKey:@"resultNotFound"];
97 | }
98 |
99 |
100 |
101 | -(BOOL)isValid{
102 | BOOL valid = NO;
103 |
104 | NSDictionary *address = [self associatedAddressDictionary];
105 | if (address){
106 | NSString *newHash = [RHAddressBookGeoResult hashForDictionary:address];
107 | if ([newHash isEqualToString:self.addressHash]){
108 | valid = YES;
109 | }
110 | }
111 |
112 | return valid;
113 | }
114 |
115 |
116 | -(NSDictionary*)associatedAddressDictionary{
117 |
118 | NSDictionary *result = nil;
119 | ABAddressBookRef addressBookRef = NULL;
120 |
121 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
122 | if (ABAddressBookCreateWithOptions != NULL){
123 |
124 | CFErrorRef errorRef = NULL;
125 | addressBookRef = ABAddressBookCreateWithOptions(nil, &errorRef);
126 |
127 | if (!addressBookRef){
128 | //bail
129 | RHErrorLog(@"Error: Failed to get -[RHAddressBookGeoResult associatedAddressDictionary]. Underlying ABAddressBookCreateWithOptions() failed with error: %@", errorRef);
130 | if (errorRef) CFRelease(errorRef);
131 |
132 | return nil;
133 | }
134 |
135 | } else {
136 | #endif //end iOS6+
137 |
138 | #pragma clang diagnostic push
139 | #pragma clang diagnostic ignored "-Wdeprecated-declarations"
140 | addressBookRef = ABAddressBookCreate();
141 | #pragma clang diagnostic pop
142 |
143 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
144 | }
145 | #endif //end iOS6+
146 |
147 | ABRecordRef person = ABAddressBookGetPersonWithRecordID(addressBookRef, self.personID);
148 | if (person){
149 | ABMultiValueRef addresses = ABRecordCopyValue(person, kABPersonAddressProperty);
150 | if (ABMultiValueGetCount(addresses) > 0){
151 |
152 | CFIndex index = ABMultiValueGetIndexForIdentifier(addresses, self.addressID);
153 | if (index != -1){
154 | CFDictionaryRef address = ABMultiValueCopyValueAtIndex(addresses, index);
155 | if (address){
156 |
157 | result = [[NSDictionary alloc] initWithDictionary:(__bridge NSDictionary*)address];
158 | CFRelease(address);
159 | }
160 | } else {
161 | //invalid addressID
162 | RHLog(@"got a -1 address index for %@", self);
163 | }
164 | }
165 | //cleanup
166 | if (addresses) CFRelease(addresses);
167 | }
168 | if (addressBookRef) CFRelease(addressBookRef);
169 |
170 | return arc_autorelease(result);
171 | }
172 |
173 |
174 | #pragma mark - geocode
175 | -(void)geocodeAssociatedAddressDictionary{
176 |
177 | //if geocoding is not supported, do nothing
178 | if (![RHAddressBookSharedServices isGeocodingSupported]) return;
179 |
180 | //don't do anything if our address is no longer valid
181 | if (! [self isValid]){
182 | RHLog(@"%@ is no longer valid. Skipping Geocode Op.", self);
183 | return;
184 | }
185 |
186 | //geocode currently in progress.. nothing to do
187 | if (_geocoder){
188 | return;
189 | }
190 |
191 | dispatch_async(dispatch_get_main_queue(), ^{
192 | _geocoder = [[CLGeocoder alloc] init];
193 |
194 | NSDictionary *addressDict = [self associatedAddressDictionary];
195 |
196 | RHLog(@"beginning geocode for :%@", addressDict);
197 |
198 | [_geocoder geocodeAddressDictionary:addressDict completionHandler:^(NSArray *placemarks, NSError *error) {
199 | if ([placemarks count]){
200 | self.placemark = [placemarks objectAtIndex:0];
201 | self.resultNotFound = NO;
202 |
203 | RHLog(@"geocode found for :%@", self);
204 |
205 | } else {
206 | if (error.code == kCLErrorNetwork){
207 | //network error, offline
208 | RHLog(@"geocode not found for: %@. A network error occurred: %@.", self, error);
209 | } else {
210 | //we are interested in:
211 | //kCLErrorGeocodeFoundNoResult
212 | //kCLErrorGeocodeFoundPartialResult
213 | //kCLErrorGeocodeCanceled
214 | self.resultNotFound = YES;
215 | RHLog(@"geocode not found for: %@ error: %@", self, error);
216 | }
217 | }
218 |
219 | // we no longer need the geocoder, release it.
220 | arc_release_nil(_geocoder);
221 |
222 | dispatch_async(dispatch_get_main_queue(), ^{
223 | //send our notification RHAddressBookPersonAddressGeocodeCompleted
224 | NSDictionary *info = [NSDictionary dictionaryWithObjectsAndKeys:
225 | [NSNumber numberWithInteger:self.personID], @"personID",
226 | [NSNumber numberWithInteger:self.addressID], @"addressID",
227 | nil];
228 | [[NSNotificationCenter defaultCenter] postNotificationName:RHAddressBookPersonAddressGeocodeCompleted object:nil userInfo:info];
229 |
230 | });
231 | }];
232 |
233 | });
234 |
235 | }
236 |
237 | -(void)dealloc{
238 | arc_release_nil(_placemark);
239 | arc_release_nil(_addressHash);
240 | arc_super_dealloc();
241 | }
242 |
243 | #pragma mark - hashing
244 | +(NSString*)hashForDictionary:(NSDictionary*)dict{
245 | return [RHAddressBookGeoResult hashForString:[dict description]];
246 | }
247 |
248 | +(NSString*)hashForString:(NSString*)string{
249 | if (! string) return nil;
250 | if (!CC_MD5) return nil; //availability check
251 |
252 | //md5 hash the string
253 | const char *str = [string UTF8String];
254 | unsigned char outBuffer[CC_MD5_DIGEST_LENGTH];
255 | CC_MD5(str, strlen(str), outBuffer);
256 |
257 | NSMutableString *hash = [NSMutableString string];
258 | for(int i = 0; i
32 | #import
33 |
34 | //enable framework debug logging (by default, enabled if DEBUG is defined, change FALSE to TRUE to enable always)
35 | #ifndef RH_AB_ENABLE_DEBUG_LOGGING
36 | #define RH_AB_ENABLE_DEBUG_LOGGING ( defined(DEBUG) || FALSE )
37 | #endif
38 |
39 | //include geocoding support in RHAddressbook. (0 == NO; 1 == YES;)
40 | #ifndef RH_AB_INCLUDE_GEOCODING
41 | #define RH_AB_INCLUDE_GEOCODING 0
42 | #endif
43 |
44 | //support building with older sdks that don't define NS_DESIGNATED_INITIALIZER
45 | #ifndef NS_DESIGNATED_INITIALIZER
46 | #if __has_attribute(objc_designated_initializer)
47 | #define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
48 | #else
49 | #define NS_DESIGNATED_INITIALIZER
50 | #endif
51 | #endif
52 |
53 | @class RHRecord;
54 | @class RHSource;
55 |
56 | @class RHPerson;
57 | @class RHGroup;
58 |
59 | @class CLLocation;
60 | @class CLPlacemark;
61 |
62 | //Notification fired when the address book is changed externally
63 | extern NSString * const RHAddressBookExternalChangeNotification;
64 |
65 | #if RH_AB_INCLUDE_GEOCODING
66 | //notification fired when a person and address pair has been geocoded (info dict contains personID and addressID as [NSNumber integerValue])
67 | extern NSString * const RHAddressBookPersonAddressGeocodeCompleted;
68 | #endif
69 |
70 | //authorization status enum.
71 | typedef NS_ENUM(NSUInteger, RHAuthorizationStatus) {
72 | RHAuthorizationStatusNotDetermined = 0,
73 | RHAuthorizationStatusRestricted,
74 | RHAuthorizationStatusDenied,
75 | RHAuthorizationStatusAuthorized
76 | };
77 |
78 | @interface RHAddressBook : NSObject
79 |
80 | -(instancetype)init NS_DESIGNATED_INITIALIZER; //create an instance of the addressbook (iOS6+ may return nil, signifying an access error. Error is logged to console)
81 |
82 | +(RHAuthorizationStatus)authorizationStatus; // pre iOS6+ will always return RHAuthorizationStatusAuthorized
83 | -(void)requestAuthorizationWithCompletion:(void (^)(bool granted, NSError* error))completion; //completion block is always called, you only need to call authorize if ([RHAddressBook authorizatonStatus] != RHAuthorizationStatusAuthorized). Pre, iOS6 completion block is always called with granted=YES. The block is called on an arbitrary queue, so dispatch_async to the main queue for any UI updates.
84 |
85 | //any access to the underlying ABAddressBook should be done inside this block wrapper below.
86 | //from the addressbook programming guide... Important: Instances of ABAddressBookRef cannot be used by multiple threads. Each thread must make its own instance by calling ABAddressBookCreate.
87 | -(void)performAddressBookAction:(void (^)(ABAddressBookRef addressBookRef))actionBlock waitUntilDone:(BOOL)wait;
88 |
89 | //access
90 | @property (nonatomic, readonly, copy) NSArray *sources;
91 | @property (nonatomic, readonly, retain) RHSource *defaultSource;
92 | -(RHSource*)sourceForABRecordRef:(ABRecordRef)sourceRef; //returns nil if ref not found in the current ab, eg unsaved record from another ab. if the passed recordRef does not belong to the current addressbook, the returned person objects underlying personRef will differ from the passed in value. This is required in-order to maintain thread safety for the underlying AddressBook instance.
93 | -(RHSource*)sourceForABRecordID:(ABRecordID)sourceID; //returns nil if not found in the current ab, eg unsaved record from another ab.
94 |
95 | @property (nonatomic, readonly, copy) NSArray *groups;
96 | @property (nonatomic, readonly) NSUInteger numberOfGroups;
97 | -(NSArray*)groupsInSource:(RHSource*)source;
98 | -(RHGroup*)groupForABRecordRef:(ABRecordRef)groupRef; //returns nil if ref not found in the current ab, eg unsaved record from another ab. if the passed recordRef does not belong to the current addressbook, the returned person objects underlying personRef will differ from the passed in value. This is required in-order to maintain thread safety for the underlying AddressBook instance.
99 | -(RHGroup*)groupForABRecordID:(ABRecordID)groupID; //returns nil if not found in the current ab, eg unsaved record from another ab.
100 |
101 | @property (nonatomic, readonly, copy) NSArray *people;
102 | @property (nonatomic, readonly) NSUInteger numberOfPeople;
103 | -(NSArray*)peopleOrderedBySortOrdering:(ABPersonSortOrdering)ordering;
104 | @property (nonatomic, readonly, copy) NSArray *peopleOrderedByUsersPreference; //preferred
105 | @property (nonatomic, readonly, copy) NSArray *peopleOrderedByFirstName;
106 | @property (nonatomic, readonly, copy) NSArray *peopleOrderedByLastName;
107 |
108 | -(NSArray*)peopleWithName:(NSString*)name;
109 | -(NSArray*)peopleWithEmail:(NSString*)email;
110 | -(RHPerson*)personForABRecordRef:(ABRecordRef)personRef; //returns nil if ref not found in the current ab, eg unsaved record from another ab. if the passed recordRef does not belong to the current addressbook, the returned person objects underlying personRef will differ from the passed in value. This is required in-order to maintain thread safety for the underlying AddressBook instance.
111 | -(RHPerson*)personForABRecordID:(ABRecordID)personID; //returns nil if not found in the current ab, eg unsaved record from another ab.
112 |
113 |
114 | //add
115 |
116 | //convenience people methods (return a +1 retain count object and are automatically added to the current addressBook)
117 | -(RHPerson*)newPersonInDefaultSource; //returns nil on error (eg read only source)
118 | -(RHPerson*)newPersonInSource:(RHSource*)source;
119 |
120 | //add a person to the current address book instance (this will thrown an exception if the RHPerson object belongs to another ab, eg by being been added to another ab, or created with a source object that was not from the current addressbook)
121 | -(BOOL)addPerson:(RHPerson*)person;
122 | -(BOOL)addPerson:(RHPerson*)person error:(NSError**)error;
123 |
124 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
125 | //add people from vCard to the current addressbook (iOS5+ : pre iOS5 these methods are no-ops)
126 | -(NSArray*)addPeopleFromVCardRepresentationToDefaultSource:(NSData*)representation; //returns an array of newly created RHPerson objects, nil on error
127 | -(NSArray*)addPeopleFromVCardRepresentation:(NSData*)representation toSource:(RHSource*)source;
128 | -(NSData*)vCardRepresentationForPeople:(NSArray*)people;
129 |
130 | #endif //end iOS5+
131 |
132 | //convenience group methods (return a +1 retain count object and are automatically added to the current addressBook)
133 | -(RHGroup*)newGroupInDefaultSource; //returns nil on error (eg read only source or does not support groups ex. exchange)
134 | -(RHGroup*)newGroupInSource:(RHSource*)source;
135 |
136 | //add a group to the current address book instance (this will thrown an exception if the RHGroup object belongs to another ab, eg by being been added to another ab, or created with a source object that was not from the current addressbook)
137 | -(BOOL)addGroup:(RHGroup*)group;
138 | -(BOOL)addGroup:(RHGroup *)group error:(NSError**)error;
139 |
140 | //remove
141 | -(BOOL)removePerson:(RHPerson*)person;
142 | -(BOOL)removePerson:(RHPerson*)person error:(NSError**)error;
143 |
144 | -(BOOL)removeGroup:(RHGroup*)group;
145 | -(BOOL)removeGroup:(RHGroup*)group error:(NSError**)error;
146 |
147 |
148 | //save
149 | -(BOOL)save;
150 | -(BOOL)saveWithError:(NSError**)error;
151 | @property (nonatomic, readonly) BOOL hasUnsavedChanges;
152 | -(void)revert;
153 |
154 |
155 | //user prefs
156 | +(ABPersonSortOrdering)sortOrdering;
157 | +(BOOL)orderByFirstName; // YES if first name ordering is preferred
158 | +(BOOL)orderByLastName; // YES if last name ordering is preferred
159 |
160 | +(ABPersonCompositeNameFormat)compositeNameFormat;
161 | +(BOOL)compositeNameFormatFirstNameFirst; // YES if first name comes before last name
162 | +(BOOL)compositeNameFormatLastNameFirst; // YES if last name comes before first name
163 |
164 |
165 | #if RH_AB_INCLUDE_GEOCODING
166 | //if geocoding is currently supported (runtime & compile-time check safe)
167 | +(BOOL)isGeocodingSupported;
168 |
169 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
170 |
171 | //geocoding (if geocoding is not available or background processing is disabled, only results already processed will be returned)
172 |
173 | //class methods to enable / disable geocoding
174 | +(BOOL)isPreemptiveGeocodingEnabled; //defaults to YES
175 | +(void)setPreemptiveGeocodingEnabled:(BOOL)enabled; //Geocoding starts on first instantiation of the AB class, therefore this is a class method, allowing you to set it to false before the first AB instance is created.
176 | @property (nonatomic, readonly) float preemptiveGeocodingProgress; // returns percentage range 0.0f - 1.0f
177 |
178 | //forward
179 | -(CLPlacemark*)placemarkForPerson:(RHPerson*)person addressID:(ABMultiValueIdentifier)addressID;
180 | -(CLLocation*)locationForPerson:(RHPerson*)person addressID:(ABMultiValueIdentifier)addressID;
181 |
182 | //reverse
183 | -(NSArray*)peopleWithinDistance:(double)distance ofLocation:(CLLocation*)location; //distance in meters
184 | -(RHPerson*)personClosestToLocation:(CLLocation*)location;
185 | -(RHPerson*)personClosestToLocation:(CLLocation*)location distanceOut:(double*)distanceOut; //distance in meters
186 |
187 | #endif //end iOS5+
188 | #endif //end Geocoding
189 |
190 | @end
191 |
192 |
193 | //define the debug logging macros
194 |
195 | #if RH_AB_ENABLE_DEBUG_LOGGING
196 | #define RHLog(format, ...) NSLog( @"%s:%i %@ ", __PRETTY_FUNCTION__, __LINE__, [NSString stringWithFormat: format, ##__VA_ARGS__])
197 | #else
198 | #define RHLog(format, ...)
199 | #endif
200 |
201 | #define RHErrorLog(format, ...) NSLog( @"%s:%i %@ ", __PRETTY_FUNCTION__, __LINE__, [NSString stringWithFormat: format, ##__VA_ARGS__])
202 |
203 |
--------------------------------------------------------------------------------
/RHAddressBook/RHPerson.h:
--------------------------------------------------------------------------------
1 | //
2 | // RHPerson.h
3 | // RHAddressBook
4 | //
5 | // Created by Richard Heard on 14/11/11.
6 | // Copyright (c) 2011 Richard Heard. All rights reserved.
7 | //
8 | // Redistribution and use in source and binary forms, with or without
9 | // modification, are permitted provided that the following conditions
10 | // are met:
11 | // 1. Redistributions of source code must retain the above copyright
12 | // notice, this list of conditions and the following disclaimer.
13 | // 2. Redistributions in binary form must reproduce the above copyright
14 | // notice, this list of conditions and the following disclaimer in the
15 | // documentation and/or other materials provided with the distribution.
16 | // 3. The name of the author may not be used to endorse or promote products
17 | // derived from this software without specific prior written permission.
18 | //
19 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 | // IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 | //
30 |
31 | #import "RHRecord.h"
32 |
33 | #import
34 | #import "RHMultiValue.h"
35 | #import "RHPersonLabels.h"
36 |
37 | @class RHPerson;
38 | @class RHSource;
39 |
40 | @class CLPlacemark;
41 | @class CLLocation;
42 |
43 | // To create a new empty instance of a person either use -[RHAddressBook newPersonInDefaultSource] or the below newPersonInSource: method
44 | // If you have an existing ABPersonRef that you want to wrap with an RHPerson object, use the personForABRecordRef: method on RHAddressBook or the below forwarding wrapper method.
45 |
46 | @interface RHPerson : RHRecord
47 |
48 | //once a person object is created using a given source object from an ab instance, its not safe to use that object with any other instance of the addressbook.
49 | //you can always access the persons associated addressbook object using @property (readonly) RHAddressBook* addressBook;
50 | //the addressbook instance is guaranteed to stay alive until its last associated object is dealloc'd.
51 | //these methods do not automatically add the new object to the source.addressBook, if you want it added you will need do add it yourself. -[RHAddressBook addPerson:];
52 | +(instancetype)newPersonInSource:(RHSource*)source;
53 | -(instancetype)initWithSource:(RHSource*)source;
54 |
55 | //look up an RHPerson instance for an existing ABRecordRef in a particular addressbook; if the current recordRef does not belong to the given addressbook, the person objects underlying personRef will differ from the passed in value. This is required in-order to maintain thread safety for the underlying AddressBook instance.
56 | +(RHPerson*)personForABRecordRef:(ABRecordRef)personRef inAddressBook:(RHAddressBook*)addressBook; //equivalent to -[RHAddressBook personForABRecordRef:];
57 | +(RHPerson*)personForABRecordID:(ABRecordID)personID inAddressBook:(RHAddressBook*)addressBook; //equivalent to -[RHAddressBook personForABRecordID:];
58 |
59 |
60 | //localised property and labels (class methods)
61 | +(NSString*)localizedPropertyName:(ABPropertyID)propertyID; //properties eg:kABPersonFirstNameProperty (ABPersonCopyLocalizedPropertyName)
62 | +(NSString*)localizedLabel:(NSString*)label; //labels eg: kABWorkLabel (ABAddressBookCopyLocalizedLabel)
63 |
64 |
65 | //person is from given source
66 | @property (nonatomic, readonly, weak) RHSource *inSource;
67 |
68 | //linked people (ie other cards that represent the same person in other sources)
69 | @property (nonatomic, readonly, copy) NSArray *linkedPeople;
70 |
71 | //image
72 |
73 | #if __IPHONE_OS_VERSION_MAX_ALLOWED < 40100
74 | //iOS4.1 added ABPersonImageFormat, however later versions of the headers think it was added in 4.0
75 | //running on 4.0 we will always return the full size image
76 | typedef enum {
77 | kABPersonImageFormatThumbnail = 0, // the square thumbnail
78 | kABPersonImageFormatOriginalSize = 2 // the original image as set by ABPersonSetImageData
79 | } ABPersonImageFormat;
80 | #endif
81 |
82 |
83 | @property (nonatomic, readonly) BOOL hasImage;
84 | @property (nonatomic, readonly, copy) UIImage *thumbnail;
85 | @property (nonatomic, readonly, copy) UIImage *originalImage;
86 | -(UIImage*)imageWithFormat:(ABPersonImageFormat)imageFormat;
87 | @property (nonatomic, readonly, copy) NSData *thumbnailData;
88 | @property (nonatomic, readonly, copy) NSData *originalImageData;
89 | -(NSData*)imageDataWithFormat:(ABPersonImageFormat)imageFormat;
90 | -(BOOL)setImage:(UIImage*)image;
91 | -(BOOL)removeImage;
92 |
93 | //personal properties
94 | @property (nonatomic, copy, readonly) NSString *name; // alias for compositeName
95 | @property (nonatomic, copy) NSString *firstName; // kABPersonFirstNameProperty
96 | @property (nonatomic, copy) NSString *lastName; // kABPersonLastNameProperty
97 | @property (nonatomic, copy) NSString *middleName; // kABPersonMiddleNameProperty
98 | @property (nonatomic, copy) NSString *prefix; // kABPersonPrefixProperty
99 | @property (nonatomic, copy) NSString *suffix; // kABPersonSuffixProperty
100 | @property (nonatomic, copy) NSString *nickname; // kABPersonNicknameProperty
101 |
102 | @property (nonatomic, copy) NSString *firstNamePhonetic; // kABPersonFirstNamePhoneticProperty
103 | @property (nonatomic, copy) NSString *lastNamePhonetic; // kABPersonLastNamePhoneticProperty
104 | @property (nonatomic, copy) NSString *middleNamePhonetic; // kABPersonMiddleNamePhoneticProperty
105 |
106 | @property (nonatomic, copy) NSString *organization; // kABPersonOrganizationProperty
107 | @property (nonatomic, copy) NSString *jobTitle; // kABPersonJobTitleProperty
108 | @property (nonatomic, copy) NSString *department; // kABPersonDepartmentProperty
109 |
110 | @property (nonatomic, copy) RHMultiStringValue *emails; // kABPersonEmailProperty - (Multi String)
111 | @property (nonatomic, copy) NSDate *birthday; // kABPersonBirthdayProperty
112 | @property (nonatomic, copy) NSString *note; // kABPersonNoteProperty
113 |
114 | @property (nonatomic, copy, readonly) NSDate *created; // kABPersonCreationDateProperty
115 | @property (nonatomic, copy, readonly) NSDate *modified; // kABPersonModificationDateProperty
116 |
117 | // (For more info on the keys and values for MultiValue objects check out )
118 | // (Also check out RHPersonLabels.h, it casts a bunch of CF labels into their toll free bridged counterparts for ease of use with this class )
119 |
120 | //Addresses
121 | @property (nonatomic, copy) RHMultiDictionaryValue *addresses; // kABPersonAddressProperty - (Multi Dictionary) dictionary keys are ( kABPersonAddressStreetKey, kABPersonAddressCityKey, kABPersonAddressStateKey, kABPersonAddressZIPKey, kABPersonAddressCountryKey, kABPersonAddressCountryCodeKey )
122 |
123 |
124 | //Dates
125 | @property (nonatomic, copy) RHMultiDateTimeValue *dates; // kABPersonDateProperty - (Multi Date) possible predefined labels ( kABPersonAnniversaryLabel )
126 |
127 | //Kind
128 | @property (nonatomic, copy) NSNumber *kind; // kABPersonKindProperty (Integer) possible values include (kABPersonKindPerson, kABPersonKindOrganization)
129 | -(BOOL)isOrganization; // if person == kABPersonKindOrganization
130 | -(BOOL)isPerson; // if person == kABPersonKindPerson
131 |
132 | //Phone numbers
133 | @property (nonatomic, copy) RHMultiStringValue *phoneNumbers; // kABPersonPhoneProperty (Multi String) possible labels are ( kABPersonPhoneMobileLabel, kABPersonPhoneIPhoneLabel, kABPersonPhoneMainLabel, kABPersonPhoneHomeFAXLabel, kABPersonPhoneWorkFAXLabel, kABPersonPhoneOtherFAXLabel, kABPersonPhonePagerLabel )
134 |
135 |
136 | //IM
137 | @property (nonatomic, copy) RHMultiDictionaryValue *instantMessageServices; // kABPersonInstantMessageProperty - (Multi Dictionary) dictionary keys are ( kABPersonInstantMessageServiceKey, kABPersonInstantMessageUsernameKey ) possible services are ( kABPersonInstantMessageServiceYahoo, kABPersonInstantMessageServiceJabber, kABPersonInstantMessageServiceMSN, kABPersonInstantMessageServiceICQ, kABPersonInstantMessageServiceAIM, kABPersonInstantMessageServiceQQ, kABPersonInstantMessageServiceGoogleTalk, kABPersonInstantMessageServiceSkype, kABPersonInstantMessageServiceFacebook, kABPersonInstantMessageServiceGaduGadu )
138 |
139 |
140 | //URLs
141 | @property (nonatomic, copy) RHMultiStringValue *urls; // kABPersonURLProperty - (Multi String) possible labels are ( kABPersonHomePageLabel )
142 |
143 |
144 | //Related Names (Relationships)
145 | @property (nonatomic, copy) RHMultiStringValue *relatedNames; // kABPersonRelatedNamesProperty - (Multi String) possible labels are ( kABPersonFatherLabel, kABPersonMotherLabel, kABPersonParentLabel, kABPersonBrotherLabel, kABPersonSisterLabel, kABPersonChildLabel, kABPersonFriendLabel, kABPersonSpouseLabel, kABPersonPartnerLabel, kABPersonAssistantLabel, kABPersonManagerLabel )
146 |
147 |
148 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
149 |
150 | //Social Profile (iOS5 +)
151 | @property (nonatomic, copy) RHMultiDictionaryValue *socialProfiles; // kABPersonSocialProfileProperty - (Multi Dictionary) possible dictionary keys are ( kABPersonSocialProfileURLKey, kABPersonSocialProfileServiceKey, kABPersonSocialProfileUsernameKey, kABPersonSocialProfileUserIdentifierKey )
152 | // possible kABPersonSocialProfileServiceKey values ( kABPersonSocialProfileServiceTwitter, kABPersonSocialProfileServiceGameCenter, kABPersonSocialProfileService Facebook, kABPersonSocialProfileServiceMyspace, kABPersonSocialProfileServiceLinkedIn, kABPersonSocialProfileServiceFlickr )
153 |
154 | //vCard formatting (iOS5 +)
155 | -(NSData*)vCardRepresentation; //the current persons vCard representation
156 | +(NSData*)vCardRepresentationForPeople:(NSArray*)people; //array of RHPerson Objects.
157 |
158 | //geocoding
159 | #if RH_AB_INCLUDE_GEOCODING
160 | -(CLPlacemark*)placemarkForAddressID:(ABMultiValueIdentifier)addressID;
161 | -(CLLocation*)locationForAddressID:(ABMultiValueIdentifier)addressID;
162 | #endif //end Geocoding
163 |
164 | #endif //end iOS5+
165 |
166 | //remove person from addressBook
167 | -(BOOL)remove;
168 | @property (nonatomic, readonly) BOOL hasBeenRemoved; // we check to see if ABAddressBookGetPersonWithRecordID() returns NULL for self.recordID; This is the recommended approach from the AB docs.
169 |
170 |
171 | //composite name format for this explicit record
172 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
173 | -(ABPersonCompositeNameFormat)compositeNameFormat; // at runtime, if you are running on a pre ios 7 device, we return the default system preference
174 | #endif //end iOS7+
175 |
176 | @end
177 |
--------------------------------------------------------------------------------
/RHAddressBook/RHRecord.m:
--------------------------------------------------------------------------------
1 | //
2 | // RHRecord.m
3 | // RHAddressBook
4 | //
5 | // Created by Richard Heard on 11/11/11.
6 | // Copyright (c) 2011 Richard Heard. All rights reserved.
7 | //
8 | // Redistribution and use in source and binary forms, with or without
9 | // modification, are permitted provided that the following conditions
10 | // are met:
11 | // 1. Redistributions of source code must retain the above copyright
12 | // notice, this list of conditions and the following disclaimer.
13 | // 2. Redistributions in binary form must reproduce the above copyright
14 | // notice, this list of conditions and the following disclaimer in the
15 | // documentation and/or other materials provided with the distribution.
16 | // 3. The name of the author may not be used to endorse or promote products
17 | // derived from this software without specific prior written permission.
18 | //
19 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 | // IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 | //
30 |
31 | #import "RHRecord.h"
32 | #import "RHRecord_Private.h"
33 |
34 | #import "RHAddressBook.h"
35 | #import "RHAddressBook_private.h"
36 | #import "RHMultiValue.h"
37 |
38 | @implementation RHRecord
39 |
40 | -(instancetype)initWithAddressBook:(RHAddressBook*)addressBook recordRef:(ABRecordRef)recordRef{
41 | self = [super init];
42 | if (self) {
43 |
44 | if (!recordRef){
45 | arc_release_nil(self);
46 | return nil;
47 | }
48 |
49 | _addressBook = arc_retain(addressBook);
50 | _recordRef = CFRetain(recordRef);
51 |
52 | //check in so we can be added to the weak link cache
53 | if (_addressBook){
54 | [_addressBook _recordCheckIn:self];
55 | }
56 | }
57 | return self;
58 | }
59 | #pragma mark - thread safe action block
60 | -(void)performRecordAction:(void (^)(ABRecordRef recordRef))actionBlock waitUntilDone:(BOOL)wait{
61 | //if we have an address book perform it on that thread
62 | if (_addressBook){
63 | if (_recordRef) CFRetain(_recordRef);
64 | [_addressBook performAddressBookAction:^(ABAddressBookRef addressBookRef) {
65 | actionBlock(_recordRef);
66 | if (_recordRef) CFRelease(_recordRef);
67 | } waitUntilDone:wait];
68 | } else {
69 | //otherwise, a user created object... just use current thread.
70 | actionBlock(_recordRef);
71 | }
72 |
73 | }
74 |
75 |
76 |
77 | #pragma mark - properties
78 |
79 | @synthesize addressBook=_addressBook;
80 | @synthesize recordRef=_recordRef;
81 |
82 | -(ABRecordID)recordID{
83 |
84 | __block ABRecordID recordID = kABPropertyInvalidID;
85 |
86 | [self performRecordAction:^(ABRecordRef recordRef) {
87 | recordID = ABRecordGetRecordID(recordRef);
88 | } waitUntilDone:YES];
89 |
90 | return recordID;
91 | }
92 |
93 | -(ABRecordType)recordType{
94 |
95 | __block ABRecordType recordType = -1;
96 |
97 | [self performRecordAction:^(ABRecordRef recordRef) {
98 | recordType = ABRecordGetRecordType(recordRef);
99 | } waitUntilDone:YES];
100 |
101 | return recordType;
102 | }
103 |
104 | -(NSString*)compositeName{
105 | __block CFStringRef compositeNameRef = NULL;
106 |
107 | [self performRecordAction:^(ABRecordRef recordRef) {
108 | compositeNameRef = ABRecordCopyCompositeName(recordRef);
109 | } waitUntilDone:YES];
110 |
111 | NSString* compositeName = [(__bridge NSString*)compositeNameRef copy];
112 | if (compositeNameRef) CFRelease(compositeNameRef);
113 |
114 | return arc_autorelease(compositeName);
115 | }
116 |
117 |
118 | #pragma mark - generic getter/setter/remover
119 | -(id)getBasicValueForPropertyID:(ABPropertyID)propertyID{
120 | if (!_recordRef) return nil; //no record ref
121 | if (propertyID == kABPropertyInvalidID) return nil; //invalid
122 |
123 | __block CFTypeRef value = NULL;
124 |
125 | [self performRecordAction:^(ABRecordRef recordRef) {
126 | value = ABRecordCopyValue(recordRef, propertyID);
127 | } waitUntilDone:YES];
128 |
129 | id result = [(__bridge id)value copy];
130 | if (value) CFRelease(value);
131 |
132 | return arc_autorelease(result);
133 | }
134 |
135 |
136 | -(BOOL)setBasicValue:(CFTypeRef)value forPropertyID:(ABPropertyID)propertyID error:(NSError**)error{
137 | if (!_recordRef) return false; //no record ref
138 | if (propertyID == kABPropertyInvalidID) return false; //invalid
139 | if (value == NULL) return [self unsetBasicValueForPropertyID:propertyID error:error]; //allow NULL to unset the property
140 |
141 | __block CFErrorRef cfError = NULL;
142 | __block BOOL result;
143 | [self performRecordAction:^(ABRecordRef recordRef) {
144 | result = ABRecordSetValue(recordRef, propertyID, value, &cfError);
145 | } waitUntilDone:YES];
146 |
147 | if (!result){
148 | if (error && cfError) *error = (NSError*)ARCBridgingRelease(CFRetain(cfError));
149 | if (cfError) CFRelease(cfError);
150 | }
151 | return result;
152 | }
153 |
154 | -(BOOL)unsetBasicValueForPropertyID:(ABPropertyID)propertyID error:(NSError**)error{
155 | if (!_recordRef) return false; //no record ref
156 | if (propertyID == kABPropertyInvalidID) return false; //invalid
157 |
158 | __block CFErrorRef cfError = NULL;
159 | __block BOOL result;
160 | [self performRecordAction:^(ABRecordRef recordRef) {
161 | result = ABRecordRemoveValue(recordRef, propertyID, &cfError);
162 | } waitUntilDone:YES];
163 |
164 | if (!result){
165 | if (error && cfError) *error = (NSError*)ARCBridgingRelease(CFRetain(cfError));
166 | if (cfError) CFRelease(cfError);
167 | }
168 | return result;
169 | }
170 |
171 |
172 | #pragma mark - generic multi value getter/setter/remover
173 | -(RHMultiValue*)getMultiValueForPropertyID:(ABPropertyID)propertyID{
174 | if (!_recordRef) return nil; //no record ref
175 | if (propertyID == kABPropertyInvalidID) return nil; //invalid
176 |
177 | __block ABMultiValueRef valueRef = NULL;
178 |
179 | [self performRecordAction:^(ABRecordRef recordRef) {
180 | valueRef = ABRecordCopyValue(recordRef, propertyID);
181 | } waitUntilDone:YES];
182 |
183 | RHMultiValue *multiValue = nil;
184 | if (valueRef){
185 | multiValue = [[RHMultiValue alloc] initWithMultiValueRef:valueRef];
186 | CFRelease(valueRef);
187 | }
188 | return arc_autorelease(multiValue);
189 | }
190 |
191 | -(BOOL)setMultiValue:(RHMultiValue*)multiValue forPropertyID:(ABPropertyID)propertyID error:(NSError**)error{
192 | if (multiValue == NULL) return [self unsetMultiValueForPropertyID:propertyID error:error]; //allow NULL to unset the property
193 | return [self setBasicValue:multiValue.multiValueRef forPropertyID:propertyID error:error];
194 | }
195 |
196 | -(BOOL)unsetMultiValueForPropertyID:(ABPropertyID)propertyID error:(NSError**)error{
197 | //this should just be able to be forwarded
198 | return [self unsetBasicValueForPropertyID:propertyID error:error];
199 | }
200 |
201 |
202 | #pragma mark - forward
203 | -(BOOL)save{
204 | return [_addressBook save];
205 | }
206 |
207 | //renamed method shim
208 | -(BOOL)save:(NSError**)error{
209 | RHErrorLog(@"RHAddressBook: The save: method has been renamed to saveWithError: You should update your sources appropriately.");
210 | return [self saveWithError:error];
211 | }
212 |
213 | -(BOOL)saveWithError:(NSError**)error{
214 | return [_addressBook saveWithError:error];
215 | }
216 | -(BOOL)hasUnsavedChanges{
217 | return [_addressBook hasUnsavedChanges];
218 | }
219 | -(void)revert{
220 | [_addressBook revert];
221 | }
222 |
223 |
224 | #pragma mark - cleanup
225 |
226 | //unfortunately ensuring dealloc occurs on our _addressBook queue is not available under ARC.
227 | #if ARC_IS_NOT_ENABLED
228 | -(oneway void)release{
229 | //ensure dealloc occurs on our ABs addressBookQueue
230 | //we do this to guarantee that we are removed from the weak cache before someone else ends up with us.
231 | if (_addressBook && !rh_dispatch_is_current_queue_for_addressbook(_addressBook)){
232 | dispatch_async(_addressBook.addressBookQueue, ^{
233 | [self release];
234 | });
235 | } else {
236 | [super release];
237 | }
238 | }
239 | #endif
240 |
241 | -(void)dealloc {
242 |
243 | //check out so we can be removed from the weak link lookup cache
244 | if (_addressBook){
245 | [_addressBook _recordCheckOut:self];
246 | }
247 |
248 | arc_release_nil(_addressBook);
249 | if (_recordRef) CFRelease(_recordRef);
250 | _recordRef = NULL;
251 | arc_super_dealloc();
252 | }
253 |
254 | #pragma mark - misc
255 | -(NSString*)description{
256 | return [NSString stringWithFormat:@"<%@: %p> name:%@", NSStringFromClass([self class]), self, self.compositeName];
257 | }
258 |
259 | +(NSString*)descriptionForRecordType:(ABRecordType)type{
260 | switch (type) {
261 | case kABPersonType: return @"kABPersonType - Person Record Type";
262 | case kABGroupType: return @"kABGroupType - Group Record Type";
263 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
264 | case kABSourceType: return @"kABSourceType - Source Record Type";
265 | #endif
266 | default: return @"Unknown Property Type";
267 | }
268 | }
269 |
270 | +(NSString*)descriptionForPropertyType:(ABPropertyType)type{
271 | switch (type) {
272 | case kABInvalidPropertyType: return @"kABInvalidPropertyType - Invalid Property Type";
273 | case kABStringPropertyType: return @"kABStringPropertyType - String Property Type";
274 | case kABIntegerPropertyType: return @"kABIntegerPropertyType - Integer Property Type";
275 | case kABRealPropertyType: return @"kABRealPropertyType - Real Property Type";
276 | case kABDateTimePropertyType: return @"kABDateTimePropertyType - Date Time Property Type";
277 | case kABDictionaryPropertyType: return @"kABDictionaryPropertyType - Dictionary Property Type";
278 |
279 | case kABMultiStringPropertyType: return @"kABMultiStringPropertyType - Multi String Property Type";
280 | case kABMultiIntegerPropertyType: return @"kABMultiIntegerPropertyType - Multi Integer Property Type";
281 | case kABMultiRealPropertyType: return @"kABMultiRealPropertyType - Multi Real Property Type";
282 | case kABMultiDateTimePropertyType: return @"kABMultiDateTimePropertyType - Multi Date Time Property Type";
283 | case kABMultiDictionaryPropertyType: return @"kABMultiDictionaryPropertyType - Multi Dictionary Property Type";
284 |
285 | default: return @"Unknown Property Type";
286 | }
287 | }
288 |
289 |
290 | @end
291 |
--------------------------------------------------------------------------------
/RHAddressBookTester/RHAddressBookViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // RHAddressBookViewController.m
3 | // RHAddressBook
4 | //
5 | // Created by Richard Heard on 20/02/12.
6 | // Copyright (c) 2012 Richard Heard. All rights reserved.
7 | //
8 |
9 | #import "RHAddressBookViewController.h"
10 | #import "RHGroupViewController.h"
11 |
12 | #import
13 |
14 | @interface RHAddressBookViewController ()
15 |
16 | -(void)configureCell:(UITableViewCell*)cell forInfoAtRow:(NSInteger)row;
17 | -(void)configureCell:(UITableViewCell*)cell forLocationAtRow:(NSInteger)row;
18 | -(void)configureCell:(UITableViewCell*)cell forSourceAtRow:(NSInteger)row;
19 | -(void)configureCell:(UITableViewCell*)cell forGroupAtRow:(NSInteger)row;
20 | -(void)configureCell:(UITableViewCell*)cell forPersonAtRow:(NSInteger)row;
21 |
22 | -(void)addNewGroup;
23 | -(void)addNewPerson;
24 |
25 | -(void)addressBookChanged:(NSNotification*)notification;
26 |
27 |
28 | @end
29 |
30 | @implementation RHAddressBookViewController
31 |
32 | @synthesize addressBook=_addressBook;
33 |
34 | - (instancetype)initWithAddressBook:(RHAddressBook *)addressBook {
35 | self = [super initWithStyle:UITableViewStyleGrouped];
36 | if (self) {
37 | // Custom initialization
38 | self.title = NSLocalizedString(@"RHAddressBook", nil);
39 | _addressBook = [addressBook retain];
40 | }
41 | return self;
42 | }
43 |
44 | #define RN(x) [x release]; x = nil;
45 | - (void)dealloc{
46 | RN(_addressBook);
47 | RN(_sources);
48 | RN(_groups);
49 | RN(_people);
50 |
51 | [[NSNotificationCenter defaultCenter] removeObserver:self]; //for the ab externally changed notifications
52 |
53 | [super dealloc];
54 | }
55 | - (void)viewDidLoad {
56 | [super viewDidLoad];
57 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(addressBookChanged:) name:RHAddressBookExternalChangeNotification object:nil];
58 | self.navigationItem.rightBarButtonItem = self.editButtonItem;
59 |
60 | self.tableView.allowsSelectionDuringEditing = YES;
61 |
62 | }
63 |
64 | - (void)viewDidUnload {
65 | [super viewDidUnload];
66 |
67 | //discard our cached values
68 | RN(_sources);
69 | RN(_groups);
70 | RN(_people);
71 |
72 | [[NSNotificationCenter defaultCenter] removeObserver:self name:RHAddressBookExternalChangeNotification object:nil];
73 |
74 | }
75 |
76 | - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
77 | return (interfaceOrientation == UIInterfaceOrientationPortrait);
78 | }
79 |
80 | - (void)setEditing:(BOOL)editing animated:(BOOL)animated {
81 | [super setEditing:editing animated:animated];
82 | [self.tableView setEditing:editing animated:animated];
83 |
84 | NSArray* paths = [NSArray arrayWithObjects:
85 | [NSIndexPath indexPathForRow:[_groups count] inSection:kRHAddressBookViewControllerGroupsSection],
86 | [NSIndexPath indexPathForRow:[_people count] inSection:kRHAddressBookViewControllerPeopleSection],
87 | nil];
88 |
89 | if(editing) {
90 | [self.tableView insertRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationFade];
91 | } else {
92 | [self.tableView deleteRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationFade];
93 | }
94 | }
95 |
96 | #pragma mark - Table view data source
97 |
98 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
99 | return kRHAddressBookViewControllerNumberOfSections;
100 | }
101 |
102 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
103 | if (section == kRHAddressBookViewControllerInfoSection){
104 | return kRHAddressBookViewControllerInfoCellsCount;
105 | } else if (section == kRHAddressBookViewControllerLocationSection){
106 | return kRHAddressBookViewControllerLocationCellsCount;
107 | } else if (section == kRHAddressBookViewControllerSourcesSection){
108 | [_sources release];
109 | _sources = [[_addressBook sources] mutableCopy];
110 | return [_sources count];
111 | } else if (section == kRHAddressBookViewControllerGroupsSection){
112 | [_groups release];
113 | _groups = [[_addressBook groups] mutableCopy];
114 | return [_groups count] + self.tableView.editing; //to allow for + button
115 | } else if (section == kRHAddressBookViewControllerPeopleSection){
116 | [_people release];
117 | _people = [[_addressBook peopleOrderedByUsersPreference] mutableCopy];
118 | return [_people count] + self.tableView.editing; //to allow for + button
119 | }
120 | return 0;
121 | }
122 |
123 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
124 | static NSString *cellIdentifier = @"RHAddressBookViewControllerCell";
125 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
126 | if (!cell){
127 | cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellIdentifier] autorelease];
128 | }
129 | //reset
130 | cell.textLabel.text = nil;
131 |
132 | switch (indexPath.section) {
133 | case kRHAddressBookViewControllerInfoSection: [self configureCell:cell forInfoAtRow:indexPath.row]; break;
134 | case kRHAddressBookViewControllerLocationSection:[self configureCell:cell forLocationAtRow:indexPath.row]; break;
135 | case kRHAddressBookViewControllerSourcesSection: [self configureCell:cell forSourceAtRow:indexPath.row]; break;
136 | case kRHAddressBookViewControllerGroupsSection: [self configureCell:cell forGroupAtRow:indexPath.row]; break;
137 | case kRHAddressBookViewControllerPeopleSection: [self configureCell:cell forPersonAtRow:indexPath.row]; break;
138 | }
139 |
140 | return cell;
141 | }
142 |
143 | -(NSString*)titleForSection:(NSInteger)section {
144 | switch (section) {
145 | case kRHAddressBookViewControllerInfoSection: return NSLocalizedString(@"Info", nil);
146 | case kRHAddressBookViewControllerSourcesSection: return NSLocalizedString(@"Sources", nil);
147 | case kRHAddressBookViewControllerGroupsSection: return NSLocalizedString(@"Groups", nil);
148 | case kRHAddressBookViewControllerPeopleSection: return NSLocalizedString(@"People", nil);
149 | case kRHAddressBookViewControllerLocationSection: return NSLocalizedString(@"Location", nil);
150 |
151 | default: return nil;
152 | }
153 | }
154 | -(NSString*)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
155 | return [self titleForSection:section];
156 | }
157 |
158 | // Override to support conditional editing of the table view.
159 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
160 |
161 | //groups and people can be edited
162 | if (indexPath.section == kRHAddressBookViewControllerGroupsSection) return YES;
163 | if (indexPath.section == kRHAddressBookViewControllerPeopleSection) return YES;
164 |
165 | return NO;
166 | }
167 |
168 | - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
169 | if (indexPath.section == kRHAddressBookViewControllerGroupsSection && indexPath.row >= [_groups count]) return UITableViewCellEditingStyleInsert;
170 | if (indexPath.section == kRHAddressBookViewControllerPeopleSection && indexPath.row >= [_people count]) return UITableViewCellEditingStyleInsert;
171 |
172 | return UITableViewCellEditingStyleDelete;
173 | }
174 |
175 | // Override to support editing the table view.
176 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
177 | if (editingStyle == UITableViewCellEditingStyleDelete) {
178 | if (indexPath.section == kRHAddressBookViewControllerGroupsSection) {
179 | RHGroup *group = [[_addressBook groups] objectAtIndex:indexPath.row];
180 | [group remove];
181 | [_addressBook save];
182 | } else if (indexPath.section == kRHAddressBookViewControllerPeopleSection) {
183 | RHPerson *person = [[_addressBook peopleOrderedByUsersPreference] objectAtIndex:indexPath.row];
184 | [person remove];
185 | [_addressBook save];
186 | }
187 |
188 | [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
189 | }
190 | else if (editingStyle == UITableViewCellEditingStyleInsert) {
191 | // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
192 | if (indexPath.section == kRHAddressBookViewControllerGroupsSection) {
193 | [self addNewGroup];
194 | } else if (indexPath.section == kRHAddressBookViewControllerPeopleSection) {
195 | [self addNewPerson];
196 | }
197 | }
198 | }
199 |
200 |
201 | #pragma mark - Table view delegate
202 |
203 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
204 | UIViewController *pushController = nil;
205 |
206 | if (indexPath.section == kRHAddressBookViewControllerSourcesSection && indexPath.row < [_sources count]){
207 |
208 | } else if (indexPath.section == kRHAddressBookViewControllerGroupsSection && indexPath.row < [_groups count]) {
209 | RHGroup *group = [_groups objectAtIndex:indexPath.row];
210 | pushController = [[RHGroupViewController alloc] initWithGroup:group];
211 |
212 | } else if (indexPath.section == kRHAddressBookViewControllerPeopleSection && indexPath.row < [_people count]) {
213 |
214 | //TODO: push our own viewer view, for now just use the AB default one.
215 | RHPerson *person = [_people objectAtIndex:indexPath.row];
216 |
217 | ABPersonViewController *personViewController = [[[ABPersonViewController alloc] init] autorelease];
218 |
219 | //setup (tell the view controller to use our underlying address book instance, so our person object is directly updated)
220 | [person.addressBook performAddressBookAction:^(ABAddressBookRef addressBookRef) {
221 | personViewController.addressBook =addressBookRef;
222 | } waitUntilDone:YES];
223 |
224 | personViewController.displayedPerson = person.recordRef;
225 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
226 | personViewController.allowsActions = YES;
227 | #endif
228 | personViewController.allowsEditing = YES;
229 |
230 |
231 | [self.navigationController pushViewController:personViewController animated:YES];
232 |
233 | } else if (indexPath.section == kRHAddressBookViewControllerLocationSection){
234 | //toggle location
235 | #if RH_AB_INCLUDE_GEOCODING
236 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
237 | [RHAddressBook setPreemptiveGeocodingEnabled:![RHAddressBook isPreemptiveGeocodingEnabled]];
238 | #endif
239 | #endif //end Geocoding
240 |
241 | [[self.tableView cellForRowAtIndexPath:indexPath] setSelected:NO];
242 | [self.tableView reloadData];
243 |
244 | } else if (indexPath.section == kRHAddressBookViewControllerGroupsSection) { //fall through to creation
245 | [self addNewGroup];
246 | } else if (indexPath.section == kRHAddressBookViewControllerPeopleSection) {
247 | [self addNewPerson];
248 | }
249 |
250 | if (pushController){
251 | [self.navigationController pushViewController:pushController animated:YES];
252 | [pushController release];
253 | }
254 |
255 | }
256 |
257 | #pragma mark - cell config
258 |
259 | -(void)configureCell:(UITableViewCell*)cell forInfoAtRow:(NSInteger)row{
260 | cell.textLabel.text = nil;
261 | cell.accessoryType = UITableViewCellAccessoryNone;
262 | cell.selectionStyle = UITableViewCellSelectionStyleNone;
263 | switch (row) {
264 |
265 | case 0: cell.textLabel.text = [NSString stringWithFormat:@"sortOrdering = %i", [RHAddressBook sortOrdering]]; break;
266 | case 1: cell.textLabel.text = [NSString stringWithFormat:@"compositeNameFormat = %i", [RHAddressBook compositeNameFormat]]; break;
267 | default: cell.textLabel.text = NSLocalizedString(@"-", nil);
268 | }
269 |
270 | }
271 |
272 | -(void)configureCell:(UITableViewCell*)cell forLocationAtRow:(NSInteger)row{
273 | cell.textLabel.text = nil;
274 | cell.accessoryType = UITableViewCellAccessoryNone;
275 | cell.selectionStyle = UITableViewCellSelectionStyleNone;
276 |
277 | switch (row) {
278 |
279 | #if RH_AB_INCLUDE_GEOCODING
280 | case 0: cell.textLabel.text = [NSString stringWithFormat:@"GeocodingSupported = %i", [RHAddressBook isGeocodingSupported]]; break;
281 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
282 | case 2: cell.textLabel.text = [NSString stringWithFormat:@"GeocodingProgress = %f", [_addressBook preemptiveGeocodingProgress]]; break;
283 | case 1: cell.textLabel.text = [NSString stringWithFormat:@"GeocodingEnabled = %i", [RHAddressBook isPreemptiveGeocodingEnabled]]; break;
284 | case 3: cell.textLabel.text = @"Toggle Geocoding"; cell.selectionStyle = UITableViewCellSelectionStyleBlue; break;
285 | #endif
286 | default: cell.textLabel.text = NSLocalizedString(@"-", nil);
287 | #else
288 | default: cell.textLabel.text = NSLocalizedString(@"No Geo Support", nil);
289 | #endif //end Geocoding
290 | }
291 |
292 | }
293 |
294 | -(void)configureCell:(UITableViewCell*)cell forSourceAtRow:(NSInteger)row{
295 | RHSource *source = [_sources objectAtIndex:row];
296 |
297 | if ([source isEqual:[_addressBook defaultSource]]){
298 | cell.textLabel.text = NSLocalizedString(@"Default Source", nil);
299 |
300 | } else {
301 | cell.textLabel.text = source.compositeName;
302 | }
303 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
304 |
305 | }
306 | -(void)configureCell:(UITableViewCell*)cell forGroupAtRow:(NSInteger)row{
307 | if (row < [_groups count]){
308 | RHGroup *group = [_groups objectAtIndex:row];
309 | cell.textLabel.text = [NSString stringWithFormat:NSLocalizedString(@"%@ - %lu Members",nil), group.compositeName, (unsigned long)[[group members] count]];
310 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
311 |
312 | } else {
313 | //assume adding a new row
314 | cell.textLabel.text = NSLocalizedString(@"Add New Group...", nil);
315 | cell.accessoryType = UITableViewCellAccessoryNone;
316 | }
317 |
318 | }
319 | -(void)configureCell:(UITableViewCell*)cell forPersonAtRow:(NSInteger)row{
320 | if (row < [_people count]){
321 | RHPerson *person = [_people objectAtIndex:row];
322 | cell.textLabel.text = person.compositeName;
323 | cell.imageView.image = person.thumbnail;
324 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
325 |
326 | } else {
327 | //assume adding a new row
328 | cell.textLabel.text = NSLocalizedString(@"Add New Person...", nil);
329 | cell.accessoryType = UITableViewCellAccessoryNone;
330 | }
331 | }
332 |
333 | #pragma mark - add new objects
334 | -(void)addNewGroup{
335 | RHGroup *group = [_addressBook newGroupInDefaultSource];
336 | group.name = NSLocalizedString(@"New Group", nil);
337 | [_addressBook save];
338 | [_groups addObject:group];
339 | [group release];
340 | }
341 |
342 | -(void)addNewPerson{
343 | RHPerson *person = [_addressBook newPersonInDefaultSource];
344 | person.firstName = NSLocalizedString(@"New Person", nil);
345 | [_addressBook save];
346 | [_people addObject:person];
347 | [person release];
348 | }
349 |
350 | #pragma mark - addressBookChangedNotification
351 | -(void)addressBookChanged:(NSNotification*)notification{
352 | [_addressBook revert]; //so we pick up the remove changes
353 | [self.tableView reloadData];
354 | }
355 |
356 | @end
357 |
358 |
359 |
--------------------------------------------------------------------------------
/RHAddressBook/RHAddressBookSharedServices.m:
--------------------------------------------------------------------------------
1 | //
2 | // RHAddressBookSharedServices.m
3 | // RHAddressBook
4 | //
5 | // Created by Richard Heard on 11/11/11.
6 | // Copyright (c) 2011 Richard Heard. All rights reserved.
7 | //
8 | // Redistribution and use in source and binary forms, with or without
9 | // modification, are permitted provided that the following conditions
10 | // are met:
11 | // 1. Redistributions of source code must retain the above copyright
12 | // notice, this list of conditions and the following disclaimer.
13 | // 2. Redistributions in binary form must reproduce the above copyright
14 | // notice, this list of conditions and the following disclaimer in the
15 | // documentation and/or other materials provided with the distribution.
16 | // 3. The name of the author may not be used to endorse or promote products
17 | // derived from this software without specific prior written permission.
18 | //
19 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 | // IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 | //
30 |
31 | #import "RHAddressBookSharedServices.h"
32 |
33 | #if RH_AB_INCLUDE_GEOCODING
34 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
35 | #import "RHAddressBookGeoResult.h"
36 | #endif //end iOS5+
37 | #endif //end Geocoding
38 |
39 | #import "NSThread+RHBlockAdditions.h"
40 | #import "RHAddressBook.h"
41 | #import "RHAddressBookThreadMain.h"
42 |
43 | #import
44 | #import
45 |
46 | #define PROCESS_ADDRESS_EVERY_SECONDS 5.0 //seconds between each geocode
47 |
48 | //private
49 | @interface RHAddressBookSharedServices ()
50 |
51 | #if RH_AB_INCLUDE_GEOCODING
52 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
53 |
54 | //cache
55 | -(void)loadCache;
56 | -(void)writeCache;
57 | -(void)purgeCache;
58 | -(void)rebuildCache;
59 | -(NSString*)cacheFilePath;
60 |
61 | //geocoding
62 | -(RHAddressBookGeoResult*)cacheEntryForPersonID:(ABRecordID)pid addressID:(ABPropertyID)aid;
63 | -(void)processAddressesMissingLocationInfo;
64 | -(void)processTimerFire;
65 |
66 | #endif //end iOS5+
67 | #endif //end Geocoding
68 |
69 |
70 | //addressbook notifications
71 | -(void)registerForAddressBookChanges;
72 | -(void)deregisterForAddressBookChanges;
73 | void RHAddressBookExternalChangeCallback (ABAddressBookRef addressBook, CFDictionaryRef info, void *context );
74 |
75 |
76 | @end
77 |
78 | @implementation RHAddressBookSharedServices {
79 | //we have our own instance of the address book
80 | ABAddressBookRef _addressBook;
81 | NSThread *_addressBookThread; //perform all address book operations on this thread. (AB is not thread safe. :()
82 |
83 | #if RH_AB_INCLUDE_GEOCODING
84 | NSMutableArray *_cache; //array of RHAddressBookGeoResult objects
85 | NSTimer *_timer;
86 | #endif //end Geocoding
87 |
88 | }
89 |
90 | #pragma mark - singleton
91 | static __strong RHAddressBookSharedServices *_sharedInstance = nil;
92 |
93 | +(id)sharedInstance{
94 | if (_sharedInstance) return _sharedInstance; //for performance reasons, check outside @synchronized
95 |
96 | @synchronized([self class]){
97 | if (!_sharedInstance){
98 | _sharedInstance = [[super allocWithZone:NULL] init];
99 | }
100 | }
101 |
102 | return _sharedInstance;
103 | }
104 |
105 | +(id)allocWithZone:(NSZone *)zone{
106 | return arc_retain([self sharedInstance]);
107 | }
108 |
109 | -(instancetype)init {
110 |
111 | self = [super init];
112 | if (self) {
113 |
114 | //because NSThread retains its target, we use a placeholder object that contains the threads main method
115 | RHAddressBookThreadMain *threadMain = arc_autorelease([[RHAddressBookThreadMain alloc] init]);
116 | _addressBookThread = [[NSThread alloc] initWithTarget:threadMain selector:@selector(threadMain:) object:nil];
117 | [_addressBookThread setName:[NSString stringWithFormat:@"RHAddressBookSharedServicesThread for %p", self]];
118 | [_addressBookThread start];
119 |
120 |
121 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
122 | if (ABAddressBookCreateWithOptions != NULL){
123 | __block CFErrorRef errorRef = NULL;
124 | [_addressBookThread rh_performBlock:^{
125 | _addressBook = ABAddressBookCreateWithOptions(nil, &errorRef);
126 | }];
127 |
128 | if (!_addressBook){
129 | //bail
130 | RHErrorLog(@"Error: Failed to create RHAddressBookSharedServices instance. Underlying ABAddressBookCreateWithOptions() failed with error: %@", errorRef);
131 | if (errorRef) CFRelease(errorRef);
132 |
133 | arc_release_nil(self);
134 |
135 | return nil;
136 | }
137 |
138 | } else {
139 | #endif //end iOS6+
140 |
141 | #pragma clang diagnostic push
142 | #pragma clang diagnostic ignored "-Wdeprecated-declarations"
143 | [_addressBookThread rh_performBlock:^{
144 | _addressBook = ABAddressBookCreate();
145 | }];
146 | #pragma clang diagnostic pop
147 |
148 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 60000
149 | }
150 | #endif //end iOS6+
151 |
152 |
153 | #if RH_AB_INCLUDE_GEOCODING
154 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
155 | if ([RHAddressBookSharedServices isGeocodingSupported]){
156 | [self loadCache];
157 | [self rebuildCache];
158 | }
159 | #endif //end iOS5+
160 | #endif //end Geocoding
161 |
162 | [self registerForAddressBookChanges];
163 |
164 | }
165 | return self;
166 | }
167 |
168 | -(id)copyWithZone:(NSZone *)zone{
169 | return self;
170 | }
171 |
172 | #pragma mark - cleanup
173 | -(void)dealloc {
174 | //do stuff (even though we are a singleton)
175 | [self deregisterForAddressBookChanges];
176 |
177 | if (_addressBook) { CFRelease(_addressBook); _addressBook = NULL; }
178 |
179 | [_addressBookThread cancel];
180 | arc_release_nil(_addressBookThread);
181 |
182 | #if RH_AB_INCLUDE_GEOCODING
183 | arc_release_nil(_cache);
184 | arc_release_nil(_timer);
185 | #endif //end Geocoding
186 |
187 | arc_super_dealloc();
188 | }
189 |
190 | #if RH_AB_INCLUDE_GEOCODING
191 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
192 |
193 | #pragma mark - cache management
194 | -(void)loadCache{
195 | RHLog(@"");
196 | arc_release(_cache);
197 | _cache = arc_retain([NSKeyedUnarchiver unarchiveObjectWithFile:[self cacheFilePath]]);
198 |
199 | //if unarchive failed or on first run
200 | if (!_cache) _cache = [[NSMutableArray alloc] init];
201 |
202 | }
203 |
204 | -(void)writeCache{
205 | RHLog(@"");
206 | [NSKeyedArchiver archiveRootObject:_cache toFile:[self cacheFilePath]];
207 |
208 | }
209 |
210 | -(void)purgeCache{
211 | RHLog(@"");
212 | [[NSFileManager defaultManager] removeItemAtPath:[self cacheFilePath] error:nil];
213 | [self loadCache];
214 | }
215 |
216 | //creates a new cache array, pulling over all existing values from the old cache array that are useable
217 | -(void)rebuildCache{
218 | if (![[NSThread currentThread] isEqual:_addressBookThread]){
219 | [self performSelector:_cmd onThread:_addressBookThread withObject:nil waitUntilDone:YES];
220 | return;
221 | }
222 | RHLog(@"");
223 |
224 | NSMutableArray *newCache = [NSMutableArray array];
225 |
226 | //make sure the address book instance is up to date
227 | ABAddressBookRevert(_addressBook);
228 |
229 | CFArrayRef people = ABAddressBookCopyArrayOfAllPeople(_addressBook);
230 |
231 | if (people){
232 | for (CFIndex i = 0; i < CFArrayGetCount(people); i++) {
233 |
234 | ABRecordRef person = CFArrayGetValueAtIndex(people, i);
235 |
236 | if (person){
237 |
238 | ABRecordID personID = ABRecordGetRecordID(person);
239 | ABMultiValueRef addresses = ABRecordCopyValue(person, kABPersonAddressProperty);
240 |
241 | if (addresses){
242 | for (CFIndex i = 0; i < ABMultiValueGetCount(addresses); i++) {
243 |
244 | ABPropertyID addressID = ABMultiValueGetIdentifierAtIndex(addresses, i);
245 | CFDictionaryRef addressDict = ABMultiValueCopyValueAtIndex(addresses, i);
246 | //======================================================================
247 |
248 | //see if we have a valid, old entry
249 | RHAddressBookGeoResult* old = [self cacheEntryForPersonID:personID addressID:addressID];
250 |
251 | if (old && [old isValid]){
252 | //yes
253 | [newCache addObject:old]; // just add it and be done.
254 | } else {
255 | // not valid, create a new entry
256 | RHAddressBookGeoResult* new = [[RHAddressBookGeoResult alloc] initWithPersonID:personID addressID:addressID];
257 | [newCache addObject:new];
258 | arc_release(new);
259 | }
260 |
261 | //======================================================================
262 | if (addressDict) CFRelease(addressDict);
263 | }
264 |
265 | CFRelease(addresses);
266 | } //addresses
267 | } //person
268 | }
269 |
270 | CFRelease(people);
271 | } //people
272 |
273 | //swap old cache with the new
274 | arc_release(_cache);
275 | _cache = arc_retain(newCache);
276 |
277 | [self processAddressesMissingLocationInfo];
278 | [self writeCache]; //get it to disk asap
279 |
280 | }
281 |
282 |
283 | -(RHAddressBookGeoResult*)cacheEntryForPersonID:(ABRecordID)pid addressID:(ABPropertyID)aid{
284 | for (RHAddressBookGeoResult *entry in _cache) {
285 | if (entry.personID == pid && entry.addressID == aid){
286 | return arc_autorelease(arc_retain(entry));
287 | }
288 | }
289 |
290 | return nil;
291 | }
292 |
293 | -(NSString*)cacheFilePath{
294 |
295 | //cache
296 | NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
297 |
298 | NSString *applicationID = [[[NSBundle mainBundle] infoDictionary] valueForKey:(NSString *)kCFBundleIdentifierKey];
299 | path = [path stringByAppendingPathComponent:applicationID];
300 |
301 | path = [path stringByAppendingPathComponent:@"RHAddressBookGeoCache.cache"];
302 |
303 | return path;
304 | }
305 |
306 |
307 | #pragma mark - Geocoding Process
308 | -(void)processAddressesMissingLocationInfo{
309 |
310 | //don't do any geocoding if its not available (iOS 5+ only)
311 | if (![RHAddressBookSharedServices isGeocodingSupported]) return;
312 |
313 | //if disabled, do nothing
314 | if (![self.class isPreemptiveGeocodingEnabled]) return;
315 |
316 |
317 | if (!_timer){
318 | _timer = arc_retain([NSTimer scheduledTimerWithTimeInterval:PROCESS_ADDRESS_EVERY_SECONDS target:self selector:@selector(processTimerFire) userInfo:nil repeats:YES]);
319 | }
320 | }
321 |
322 | -(void)processTimerFire{
323 |
324 | //if we are offline, the geocode fails with a specific error
325 | // in that instance we don't set the resultNotFound flag, so next time around we will re-attempt the particular address.
326 | //TODO: we really should handle this better, with our shared services class observing some form of reachability and pausing / resuming the timer.
327 |
328 | //write the cache periodically, not just at the end... incase we... you know..... yea.....
329 | [self writeCache];
330 |
331 | //if we have been disabled, stop working
332 | if (![self.class isPreemptiveGeocodingEnabled]){
333 | [_timer invalidate];
334 | arc_release_nil(_timer);
335 | RHLog(@"Location Lookup has been disabled.");
336 | return;
337 | }
338 |
339 | //look for next unprocessed entry
340 | for (RHAddressBookGeoResult *entry in _cache) {
341 | if (!entry.location && !entry.resultNotFound){
342 | //needs processing
343 | [entry geocodeAssociatedAddressDictionary]; //if this is called and the entry is already geocoding, its just a no-op and so is an easy way for us to bail
344 | return;
345 | }
346 | }
347 |
348 | //we are done, all addresses processed
349 | [self writeCache];
350 | [_timer invalidate];
351 | arc_release_nil(_timer);
352 |
353 | RHLog(@"Location Lookup Processing done.");
354 |
355 | }
356 |
357 | #pragma mark - Geocode Lookup
358 | //forward
359 | -(CLPlacemark*)placemarkForPersonID:(ABRecordID)personID addressID:(ABMultiValueIdentifier)addressID{
360 | RHAddressBookGeoResult *cacheEntry = [self cacheEntryForPersonID:personID addressID:addressID];
361 | if (cacheEntry && !cacheEntry.placemark && !cacheEntry.resultNotFound && [self.class isGeocodingSupported] && !_timer) {
362 | //lets force a geocode for this one address
363 | [cacheEntry geocodeAssociatedAddressDictionary];
364 | }
365 | return [cacheEntry placemark];
366 | }
367 |
368 | -(CLLocation*)locationForPersonID:(ABRecordID)personID addressID:(ABMultiValueIdentifier)addressID{
369 | return [[self placemarkForPersonID:personID addressID:addressID] location];
370 | }
371 |
372 | //reverse
373 | -(NSArray*)geoResultsWithinDistance:(CLLocationDistance)distance ofLocation:(CLLocation*)location{
374 | NSMutableArray *results = [[NSMutableArray alloc] init];
375 |
376 | for (RHAddressBookGeoResult *entry in _cache) {
377 | if (entry.location) {
378 | CLLocationDistance tmpDistance = [entry.location distanceFromLocation:location];
379 | if (tmpDistance < distance) {
380 | //within radius
381 | [results addObject:entry];
382 | }
383 | }
384 | }
385 |
386 | return arc_autorelease(results);
387 | }
388 |
389 | -(RHAddressBookGeoResult*)geoResultClosestToLocation:(CLLocation*)location{
390 | return [self geoResultClosestToLocation:location distanceOut:nil];
391 | }
392 |
393 | -(RHAddressBookGeoResult*)geoResultClosestToLocation:(CLLocation*)location distanceOut:(CLLocationDistance*)distanceOut{
394 |
395 | CLLocationDistance distance = DBL_MAX;
396 | RHAddressBookGeoResult *result = nil;
397 |
398 | for (RHAddressBookGeoResult *entry in _cache) {
399 | if (entry.location) {
400 | CLLocationDistance tmpDistance = [entry.location distanceFromLocation:location];
401 | if (tmpDistance < distance) {
402 | //closer point
403 | result = entry;
404 | distance = tmpDistance;
405 | }
406 | }
407 | }
408 |
409 | if (distanceOut) *distanceOut = distance;
410 | return result;
411 | }
412 |
413 | #endif //end iOS5+
414 |
415 | #pragma mark - geocoding settings
416 | NSString static * RHAddressBookSharedServicesPreemptiveGeocodingEnabled = @"RHAddressBookSharedServicesPreemptiveGeocodingEnabled";
417 |
418 | +(BOOL)isPreemptiveGeocodingEnabled{
419 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
420 | if ([RHAddressBookSharedServices isGeocodingSupported]){
421 | return [[NSUserDefaults standardUserDefaults] boolForKey:RHAddressBookSharedServicesPreemptiveGeocodingEnabled];
422 | }
423 | #endif //end iOS5+
424 | return NO;
425 | }
426 |
427 | +(void)setPreemptiveGeocodingEnabled:(BOOL)enabled{
428 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
429 | if ([RHAddressBookSharedServices isGeocodingSupported]){
430 | [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:RHAddressBookSharedServicesPreemptiveGeocodingEnabled];
431 | //for the disabled->enabled case
432 | if (_sharedInstance)[_sharedInstance processAddressesMissingLocationInfo];
433 | }
434 | #endif //end iOS5+
435 |
436 | }
437 |
438 | -(float)preemptiveGeocodingProgress{
439 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
440 | if ([RHAddressBookSharedServices isGeocodingSupported]){
441 | NSInteger incomplete = 0;
442 | for (RHAddressBookGeoResult *entry in _cache) {
443 | if (!entry.location && !entry.resultNotFound){
444 | incomplete++;
445 | }
446 | }
447 |
448 | if ([_cache count] == 0) return 1.0f;
449 |
450 | return 1.0f - ((float)incomplete / (float)[_cache count]);
451 | }
452 | #endif //end iOS5+
453 |
454 | return 0.0f;
455 | }
456 |
457 |
458 | +(BOOL)isGeocodingSupported{
459 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
460 | //the response to selector check is required because iOS4 actually has a private CLGeocoder class.
461 | return ([CLGeocoder class] && [CLGeocoder instancesRespondToSelector:@selector(geocodeAddressDictionary:completionHandler:)]);
462 | #endif //end iOS5+
463 | return NO; //if not compiled with Geocoding, return false, always
464 | }
465 |
466 | #endif //end Geocoding
467 |
468 |
469 | #pragma mark - addressbook changes
470 |
471 | -(void)registerForAddressBookChanges{
472 | if (![[NSThread currentThread] isEqual:_addressBookThread]){
473 | [self performSelector:_cmd onThread:_addressBookThread withObject:nil waitUntilDone:YES];
474 | return;
475 | }
476 |
477 | ABAddressBookRegisterExternalChangeCallback(_addressBook, RHAddressBookExternalChangeCallback, (__bridge void *)(self)); //use the context as a pointer to self
478 |
479 | }
480 |
481 | -(void)deregisterForAddressBookChanges{
482 | if (![[NSThread currentThread] isEqual:_addressBookThread]){
483 | [self performSelector:_cmd onThread:_addressBookThread withObject:nil waitUntilDone:YES];
484 | return;
485 | }
486 |
487 | // when unregistering a callback both the callback and the context
488 | // need to match the ones that were registered.
489 | if (_addressBook){
490 | ABAddressBookUnregisterExternalChangeCallback(_addressBook, RHAddressBookExternalChangeCallback, (__bridge void *)(self));
491 | }
492 |
493 | }
494 |
495 | void RHAddressBookExternalChangeCallback (ABAddressBookRef addressBook, CFDictionaryRef info, void *context ){
496 |
497 | #if RH_AB_INCLUDE_GEOCODING
498 | RHLog(@"AddressBook changed externally. Rebuilding RHABGeoCache");
499 |
500 | #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 50000
501 | if ([RHAddressBookSharedServices isGeocodingSupported]){
502 | [(__bridge RHAddressBookSharedServices*)context rebuildCache]; //use the context as a pointer to self
503 | }
504 | #endif //end iOS5+
505 | #endif //end Geocoding
506 |
507 | //post external change notification for public clients, on the main thread
508 | dispatch_async(dispatch_get_main_queue(), ^{
509 | [[NSNotificationCenter defaultCenter] postNotificationName:RHAddressBookExternalChangeNotification object:nil];
510 | });
511 | }
512 |
513 |
514 |
515 |
516 | @end
517 |
--------------------------------------------------------------------------------