Get only new/modified phone contacts from iPhone - ios

I want to import all phone contacts into app first time only and save on server.
Second time wants to imports only new, modified and deleted contacts into an app and sync with contacts saved on the server which should be done according to created/modified date of contacts.
From iOS 9, Apple won't allow to get create/modified date using Contacts Framework
How can I achieve with better approach?

You can save contacts identifier and import date in local storage like SQLite. And when you receive contacts update notification, you can send saved date of respective updated contact and also update date in SQLite with received notification timestamp(date).

You should add observer for change contact list notification like this:
NotificationCenter.default.addObserver(self,selector: #selector(self.addressBookDidChange(_:)), name: NSNotification.Name.CNContactStoreDidChange,object: nil)
And then
#objc func addressBookDidChange(_ notification: Notification){
print(notification.object as Any, notification.userInfo as Any)
//remove observer so far to prevent double method calling when making operations with contacts
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.CNContactStoreDidChange, object: nil)
//processing
//at some point return observer
NotificationCenter.default.addObserver(self,selector: #selector(ContactsListVC.addressBookDidChange(_:)), name: NSNotification.Name.CNContactStoreDidChange,object: nil)
}

There's an undocumented API in CNContactStore - enumeratorForChangeHistoryFetchRequest:error:
I tried to test this method using the following code:
-(void)testChangeHistoryRequest {
CNContactStore *store = [[CNContactStore alloc] init];
[self requestContactsPermissions:store completion:^(BOOL granted) {
if (!granted)
return;
NSData *storeToken = [store currentHistoryToken];
NSLog(#"testChangeHistoryRequest: store token st start - %#", storeToken);
NSError *error;
CNChangeHistoryFetchRequest *req = [[CNChangeHistoryFetchRequest alloc] init];
[req setAdditionalContactKeyDescriptors:#[CNContactGivenNameKey, CNContactFamilyNameKey]];
[req setMutableObjects:YES];
[req setShouldUnifyResults:YES];
[req setStartingToken:storeToken];
CNFetchResult<NSEnumerator<CNChangeHistoryEvent*>*>* res = [store enumeratorForChangeHistoryFetchRequest:req error:&error];
if (res && res.value) {
NSData *token = [res currentHistoryToken];
NSLog(#"token - %#", token);
for (CNChangeHistoryEvent *e in res.value)
NSLog(#"%# - %#", [e class], e);
NSLog(#"token at end - %#", token);
}
}];
}
What I got is that store.currentHistoryToken never changes - it starts and ends with nil value. Also during the iteration, res.currentHistoryToken is always nil.
I also tried to initialize [req setStartingToken:storeToken]; with arbitrary data, but this changed nothing (and didn't even fail).
My guess is that this enumeration is not fully implemented.

Related

Azure Mobile Services - Duplicate item after synchronization

I am using Azure Mobile Service as a backend for an iOS application. I have set up everything to work with offline sync which allows me to view, add, or modify data even when there is no network connection. I am running into a problem when I add a new object into a table. The add works well locally but when I synchronize data it creates a duplicate item on the local database with a slightly different objectId. The created item is not duplicated on the server side.
Here's how I am setup. By the way, thanks to #TheBasicMind for posting this model.
Here's a link to his explanation of the model: enter link description here
Here's what I do to setup the sync context and sync table:
// Initialize the Mobile Service client with your URL and key
MSClient *client = self.hpc.client;
NSManagedObjectContext *context = self.hpc.syncContext;
MSCoreDataStore *store = [[MSCoreDataStore alloc] initWithManagedObjectContext:context];
client.syncContext = [[MSSyncContext alloc] initWithDelegate:syncDelegate dataSource:store callback:nil];
// Add a Mobile Service filter to enable the busy indicator
self.client = [client clientWithFilter:self];
// Create an MSSyncTable instance to allow us to work with the Athlete table
self.syncAthleteTable = [self.client syncTableWithName:#"Athlete"];
Here's how I add a record for the moment:
NSDictionary *newItem = #{#"firstname": firstname, #"lastname": lastname, #"laterality" : laterality};
[self.athletesService addItem:newItem completion:^{
NSLog(#"New athlete added");
}];
-(void)addItem:(NSDictionary *)item completion:(CompletionBlock)completion
{
// Insert the item into the Athlete table
[self.syncAthleteTable insert:item completion:^(NSDictionary *result, NSError *error)
{
[self logErrorIfNotNil:error];
// Let the caller know that we finished
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}];
}
The add works as expected and it is added in a UITableView as I have an NSFetchedResultsController listening on my Main Context.
Here's where the problem occurs. When I synchronize data with the server using this function:
-(void)syncData:(CompletionBlock)completion
{
// push all changes in the sync context, then pull new data
[self.client.syncContext pushWithCompletion:^(NSError *error) {
[self logErrorIfNotNil:error];
[self pullData:completion];
}];
}
-(void)pullData:(CompletionBlock)completion
{
MSQuery *query = [self.syncAthleteTable query];
// Pulls data from the remote server into the local table.
// We're pulling all items and filtering in the view
// query ID is used for incremental sync
[self.syncAthleteTable pullWithQuery:query queryId:#"allAthletes" completion:^(NSError *error) {
[self logErrorIfNotNil:error];
[self refreshDataOnSuccess:completion];
}];
}
- (void) refreshDataOnSuccess:(CompletionBlock)completion
{
MSQuery *query = [self.syncAthleteTable query];
[query readWithCompletion:^(MSQueryResult *results, NSError *error) {
[self logErrorIfNotNil:error];
NSLog(#"Data that pulled from local store: ");
for ( NSDictionary *dict in results.items ) {
NSLog(#"%# %#", [dict objectForKey:#"firstname"], [dict objectForKey:#"lastname"] );
}
// Let the caller know that we finished
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}];
}
After the synchronization the NSFetchedResultsChangeInsert is called a second time for the same record with a slightly different objectID. Here's an example of the first and second objectIDs:
tD7ADE77E-0ED0-4055-BAF6-B6CF8A6960AE9
tD7ADE77E-0ED0-4055-BAF6-B6CF8A6960AE11
I am stuck here.
Any help is highly appreciated. Thank you!
In the past, when I've seen this happen, its because the "id" field the client is sending was being changed or ignored by the server logic.
Locally the store finds the object in core data using that field, so a change to it could result in the client SDK thinking it needs to insert a new object and not update an existing one.
One easy way to confirm this, is by using the tableOperation:complete: method on the data delegate and comparing the "id" column between the item originally and that being returned by operation execute.

CKFetchNotificationChangesOperation returning old notifications

I'm working on a CloudKit-based app that uses CKSubscription notifications to keep track of changes to a public database. Whenever the app receives a push notification I check the notification queue with CKFetchNotificationChangesOperation and mark each notification read after processing it:
__block NSMutableArray *notificationIds = [NSMutableArray new];
CKFetchNotificationChangesOperation *operation = [[CKFetchNotificationChangesOperation alloc] initWithPreviousServerChangeToken:self.serverChangeToken];
operation.notificationChangedBlock = ^(CKNotification *notification) {
[notificationIds addObject:notification.notificationID];
[self processRemoteNotification:notification withCompletionHandler:completionHandler];
};
__weak CKFetchNotificationChangesOperation *operationLocal = operation;
operation.fetchNotificationChangesCompletionBlock = ^(CKServerChangeToken *serverChangeToken, NSError *operationError) {
if (operationError) {
NSLog(#"Unable to fetch queued notifications: %#", operationError);
}
else {
self.serverChangeToken = serverChangeToken;
completionHandler(UIBackgroundFetchResultNewData);
// Mark the processed notifications as read so they're not delivered again if the token gets reset.
CKMarkNotificationsReadOperation *markReadOperation = [[CKMarkNotificationsReadOperation alloc] initWithNotificationIDsToMarkRead:[notificationIds copy]];
[notificationIds removeAllObjects];
markReadOperation.markNotificationsReadCompletionBlock = ^(NSArray *notificationIDsMarkedRead, NSError *operationError) {
if (operationError) {
NSLog(#"Unable to mark notifications read: %#", operationError);
}
else {
NSLog(#"%lu notifications marked read.", (unsigned long)[notificationIDsMarkedRead count]);
}
};
[[CKContainer defaultContainer] addOperation:markReadOperation];
if (operationLocal.moreComing) {
NSLog(#"Fetching more");
[self checkNotificationQueueWithCompletionHandler:completionHandler];
}
}
};
[[CKContainer defaultContainer] addOperation:operation];
As I understand it marking a notification read will keep it from showing up in future queue fetches, even if the server change token is reset to nil. Instead I'm getting a lot of old notifications in every fetch with a non-nil change token when there should only be 1 or 2 new ones. I can detect the old ones from the notificationType flag, but I'm concerned that they're showing up at all. Am I missing a step somewhere?
I know this is a bit old, but I was running into the same issue. I think I figured it out (at least for my case).
In my code, I was doing the same as you: that is, adding all the notificationIDs to an array and using that in my CKMarkNotificationsReadOperation, and was also getting all the notifications returned each time (although, as you noted, with a type of "ReadNotification").
I changed my code so that I was only adding "new" notifications to my array, and not the "ReadNotification" items, and sending those. That fixed it.
It seems that sending a notification back to the server to be marked as read, even if it already has been marked as such, will cause it to be returned again as "ReadNotification."
I hope this helps someone.
The documentation isn't very clear it should say: "Marking a notification as read prevents it from being returned by subsequent fetch operations"...as a query notification type. Further clarification it should say the notifications will instead be returned as read type.
If it wasn't returned at all then other devices that missed the push wouldn't know that something has changed!

Downloading data, creating core data object and then using those to make next request

I am struggling to come up with an elegant solution to a problem involving multiple network requests. If you need further information than what has been provided then please don't hesitate to ask.
I need to download data from a server, create core data objects from it and then use information from those objects to download the next set of data. So I am transversing a hierarchy.
So for example:
I make my first request to the server and pull down the Regions which is made up of 4 objects (North, South, East, West). I am saving these to core data.
Once that is done (not sure best way to track this) I then need to do a fetch request on the region entity to get back those 4 objects. Each region contains a number of counties which I need to request from the server. So I loop through the regions and make a network request for each region.
I loop through each returned dictionary (one for each region) to create each county.
Here is my code to download the regions and county:
+ (void)downloadRegions
{
NSString *search = #"organisation";
NetworkHandler *networkHandler = [[NetworkHandler alloc] init];
[networkHandler downloadData:search];
}
+ (void)downloadCounty
{
NSManagedObjectContext *context = [DatabaseHandler sharedHandler].managedObjectContext;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Region" inManagedObjectContext:context];
[request setEntity:entity];
//NSPredicate *searchFilter = [NSPredicate predicateWithFormat:#"attribute = %#", searchingFor];
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
NSLog(#"%#", results);
NSString *search = #"organisation?code=";
for (Region *region in results) {
NSString *s = [search stringByAppendingString:[NSString stringWithFormat:#"%#", region.code]];
NetworkHandler *networkHandler = [[NetworkHandler alloc] init];
[networkHandler downloadData:s];
}
}
Both of the above methods call:
- (void)downloadData:(NSString *)searchUrl
{
NSString *apiURL = [kBaseURL stringByAppendingPathComponent:#"api"];
NSString *finalURL = [apiURL stringByAppendingPathComponent:searchUrl];
self.dataTask = [self.session dataTaskWithURL:[NSURL URLWithString:finalURL]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
//self.jsonData[searchUrl] = json;
[self createObjectInDatabase:json andSearchURL:searchUrl];
NSLog(#"This has been reached");
}];
[self.dataTask resume];
}
- (void)createObjectInDatabase:(id)data andSearchURL:(NSString *)searchURL
{
if ([searchURL isEqual:#"organisation"]) {
[Region createNewRegionWithData:data inManagedObjectContext:[DatabaseHandler sharedHandler].managedObjectContext];
}
else {
[LAT createNewLATWithData:data inManagedObjectContext:[DatabaseHandler sharedHandler].managedObjectContext];
}
}
I am not sure if I am doing this the best way. In regards to making the request, creating the object and then making the next request.
My biggest issue is knowing when to make the next request. i.e - knowing when the download has completed and all the core data objects have been created successfully before making a request for those objects and using them in the next request. I am currently making the second request manually but need it to be done automatically.
I hope that is clear. I am finding it hard to explain :-). Thanks in advance.
What if you passed a string into downloadData: that the completion handler then used to post a notification as it was finishing? When you receive each notification, you know which step of the process to go to next.

Force app to wait for method completion (data download)

I'm working with an app that requests data from an OAuth2.0 protected server. When I use the GTM OAuth Library to retrieve data, the program continues to run while the data is being downloaded in the background. I need some sort of mechanism to either force my application to wait until the didFinishWithData selector is called,or I need a way to notify my ViewController of the download's completion, so I can then utilize the data immediately.
I've tried conditional blocks, but those aren't doing it for me. I've also tried polling the object whose data I'm interested in, but if I do that, the data never seems to download. I've heard I can somehow utilize the Notification Center to accomplish this task, so I'll look more into that while I'm waiting for replies here.
Here is basically what is going on:
-(void) getAlert{
// Define the URL of the API module we'd like to utilize.
NSURL *url = [[NSURL alloc] initWithString:#"https://access.active911.com/interface/open_api/api/alerts"];
// Constructs a an HTTP request object to send to the server in order to obtain data.
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setValue:#"1" forHTTPHeaderField:#"alert_days"];
// This fetcher sends the request along with the authentication header in a recognizable manner.
GTMHTTPFetcher *fetcher = [[GTMHTTPFetcher alloc] initWithRequest:request];
// Attach the OAuth credentials for the fetcher's use.
[fetcher setAuthorizer:auth];
// Execute the operation.
[fetcher waitForCompletionWithTimeout:10];
NSLog(#"About to get alert");
[fetcher beginFetchWithDelegate:self didFinishSelector:#selector(responseHandler:finishedWithData:finishedWithError:)];
NSLog(#"got alert");
}
-(void)responseHandler:(id)valueNotUsed finishedWithData:(NSData *)data finishedWithError:(NSError *)error{
// Retrieve the server data in a usable object
// All that's being done here is conversion to an NSDictionary
// followed by the creation of subdictionaries from that dictionary
// until our final value can be picked directly out of the resulting dict
NSData *jsonData = [[NSData alloc] initWithData:data];
NSError *dictError;
NSDictionary* json = [NSJSONSerialization
JSONObjectWithData:jsonData //1
options:kNilOptions
error:&dictError];
NSDictionary *token = [json objectForKeyedSubscript:#"message"];
NSArray *alerts = [token objectForKeyedSubscript:#"alerts"];
NSDictionary *alertData = alerts[0];
mapCode = [alertData objectForKeyedSubscript:#"map_code"];
NSString *city = [alertData objectForKeyedSubscript:#"city"];
NSLog(#"Map code: '%#' with city '%#' and access token %#", mapCode, city, accessToken);
}
And I need to pass the mapCode to my view controller.
Thanks for the help!
First off, please rethink about having the UI halt while you fetch results from the server. This can create an extremely bad UX for the app and only should be done if absolutely necessary.
Second, does your responseHandler method work? And do you only need mapCode in the VC that responseHandler is in?
If so, you don't even need to use Notifications. Simply do:
-(void)responseHandler:(id)valueNotUsed finishedWithData:(NSData *)data finishedWithError:(NSError *)error{
...
...
mapCode = [alertData objectForKeyedSubscript:#"map_code"];
[self updateVCWithMapCode:mapCode];
}
That will call the method after the response has been received. Passing it explicitly too so you don't need to have mapCode be a property as well.

thread error with Core Data

I am developing an app that communicates with a server, and saves to core data.
in appDelegate, I started my singleton object and called useDocument to initialize UIManagedDocument:
- (void)useDocument
{
if (![[NSFileManager defaultManager] fileExistsAtPath:self.database.fileURL.path]){
[self.database saveToURL:self.database.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:nil];
} else if (self.database.documentState == UIDocumentStateClosed){
[self.database openWithCompletionHandler:nil];
}
}
heres the code upon receiving data from server:
- (void)downloadCompletedWithData: (NSData *)data item: (TPDownloadItem *)finishedItem;
{
// parse data and update core
NSError *error;
id dataObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
if (dataObject && !error){
dispatch_async(dispatch_get_main_queue(), ^{
[User createUserWithInfo:dataObject inManagedContext:self.database.managedObjectContext];
});
}
}
heres the code that writes to core data:
+ (id)createUserWithInfo: (NSDictionary *)userInfo inManagedContext: (NSManagedObjectContext *)aContext
{
NSError *error;
User *aUser = [NSEntityDescription insertNewObjectForEntityForName:ENTITY_NAME inManagedObjectContext:aContext];
NSString *userID = [userInfo objectForKey:USER_ID];
aUser.userID = userID;
aUser.effective = [NSNumber numberWithBool:[[userInfo objectForKey:USER_EFFECTIVE] boolValue]];
aUser.job = [userInfo objectForKey:USER_JOB];
aUser.gender = [userInfo objectForKey:USER_GENDER];
return aUser;
}
The problem is that whenever core data tries to auto save (waited for about 30 sec) the app crashes with the following error:
I tried to call "useDocument" and "createUser" in the main thread using GCD, also override UIDocument "contentsForType" to make sure it happens right after autosave, document state is normal before it saves, and persistence store file is also created.
Does anyone encounter similar situation? any help is very much appreciated!!
managed context is not thread safe, but you can have as many managed contexts as you like, that might have something to do with this. Perhaps remove the dispatch async, you don't need it as you are doing the code on main thread.
I make a background managed context in a data manager class, where I do all my writes and when I need to, I listen do a notification name MANAGEDCONTEXTDIDSAVE and reload if I need to.
Works like a charm.
the problem is sort of fixed, I had to turn off and reset my phone to get it to work...
but still don't know why this could happen

Resources