How To Synchronize Core Data with a Web Service – Part 2

This is a post by iOS Tutorial Team Member Chris Wagner, an enthusiast in software engineering always trying to stay ahead of the curve. You can also find him on Google+. Welcome back to our 2-part tutorial series on how to synchronize core data with a web service! Just to refresh your memory, here’s what […] By Chris Wagner.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 2 of this article. Click here to view the first page.

Delete records on server when deleted locally

Now try deleting a record (swipe to delete) and then press the refresh button.

Whoa, what’s going on here? No, unfortunately aliens are not responsible for this behaviour! The issue is that you are not tracking when an object is deleted locally and sending that information to the remote service. First you need to add another syncStatus option open SDSyncEngine.h and update your enum to reflect the following:

typedef enum {
    SDObjectSynced = 0,
    SDObjectCreated,
    SDObjectDeleted,
} SDObjectSyncStatus;

Next you need to add a new method SDAFParseAPIClient which will process the deletion on the remote service.

Add the following method declaration to SDAFParseAPIClient.h:

- (NSMutableURLRequest *)DELETERequestForClass:(NSString *)className forObjectWithId:(NSString *)objectId;

Now implement the method in SDAFParseAPIClient.m:

- (NSMutableURLRequest *)DELETERequestForClass:(NSString *)className forObjectWithId:(NSString *)objectId {
    NSMutableURLRequest *request = nil;
    request = [self requestWithMethod:@"DELETE" path:[NSString stringWithFormat:@"classes/%@/%@", className, objectId] parameters:nil];
    return request;
}

Next you need to flag records as deleted when the user deletes them. Open SDDateTableViewController.m and update -tableView:commitEditingStyle:forRowAtIndexPath: with the following implementation:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        NSManagedObject *date = [self.dates objectAtIndex:indexPath.row];
        [self.managedObjectContext performBlockAndWait:^{
            // 1
            if ([[date valueForKey:@"objectId"] isEqualToString:@""] || [date valueForKey:@"objectId"] == nil) {
                [self.managedObjectContext deleteObject:date];
            } else {
                [date setValue:[NSNumber numberWithInt:SDObjectDeleted] forKey:@"syncStatus"];
            }
            NSError *error = nil;
            BOOL saved = [self.managedObjectContext save:&error];
            if (!saved) {
                NSLog(@"Error saving main context: %@", error);
            }
            
            [[SDCoreDataController sharedInstance] saveMasterContext];
            [self loadRecordsFromCoreData];
            [self.tableView reloadData];
        }];
    }
}

Take a close look at the comment mark “1”. This line was removed:

[self.managedObjectContext deleteObject:date];

And this line was added:

if ([[date valueForKey:@"objectId"] isEqualToString:@""] || [date valueForKey:@"objectId"] == nil) {
    [self.managedObjectContext deleteObject:date];
} else {
    [date setValue:[NSNumber numberWithInt:SDObjectDeleted] forKey:@"syncStatus"];
}

You are no longer just deleting the record from Core Data. In the new model, if the record does NOT have an objectId (meaning it does not exist on the server) the record is immediately deleted as it was before. Otherwise you set the syncStatus to SDObjectDeleted. This is so that the record is still around when it is time to send the request to the server to have it deleted.

This poses a new problem though. (Can you see it yourself, before you read on any further?)

The deleted records still appear in the list! (And no, this one isn’t due to aliens either.) This will undoubtedly confuse the user, and they will likely try to delete it over and over again. You must next update your SDDateTableViewController to not show records whose syncStatus is set to SDObjectDeleted.

Add one line in your -loadRecordsFromCoreData method:

- (void)loadRecordsFromCoreData {
    [self.managedObjectContext performBlockAndWait:^{
        [self.managedObjectContext reset];
        NSError *error = nil;
        NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:self.entityName];
        [request setSortDescriptors:[NSArray arrayWithObject:
                                     [NSSortDescriptor sortDescriptorWithKey:@"date" ascending:YES]]];
        // 1
        [request setPredicate:[NSPredicate predicateWithFormat:@"syncStatus != %d", SDObjectDeleted]];
        self.dates = [self.managedObjectContext executeFetchRequest:request error:&error];
    }];
}

The line after the comment “1” sets a predicate on the NSFetchRequest to ignore records whose syncStatus is equal to SDObjectDeleted. (Phew! That wasn’t so hard to fix. Those aliens will have to try a little harder next time).

Now build and run the App! Attempt to delete a record; the deleted records should no longer keep reappearing in your list when you press the refresh button.

However, there’s still one problem remaining. Can you tell what you’ve neglected to do?

Take a look at Parse — the records will still exist! (Don’t even try blaming those aliens again!) You must now modify the sync engine to use your new method in SDAFParseAPIClient.

Beneath -postLocalObjectsToServer, add the following method

- (void)deleteObjectsOnServer {
    NSMutableArray *operations = [NSMutableArray array];    
    //
    // Iterate over all registered classes to sync
    // 
    for (NSString *className in self.registeredClassesToSync) {
        //
        // Fetch all records from Core Data whose syncStatus is equal to SDObjectDeleted
        //
        NSArray *objectsToDelete = [self managedObjectsForClass:className withSyncStatus:SDObjectDeleted];
        //
        // Iterate over all fetched records from Core Data
        //
        for (NSManagedObject *objectToDelete in objectsToDelete) {
            //
            // Create a request for each record
            // 
            NSMutableURLRequest *request = [[SDAFParseAPIClient sharedClient] 
                                            DELETERequestForClass:className 
                                            forObjectWithId:[objectToDelete valueForKey:@"objectId"]];
            
            AFHTTPRequestOperation *operation = [[SDAFParseAPIClient sharedClient] HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
                NSLog(@"Success deletion: %@", responseObject);
                //
                // In the operations completion block delete the NSManagedObject from Core data locally since it has been 
                // deleted on the server
                //
                [[[SDCoreDataController sharedInstance] backgroundManagedObjectContext] deleteObject:objectToDelete];
            } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                NSLog(@"Failed to delete: %@", error);
            }];
            
            //
            // Add each operation to the operations array
            // 
            [operations addObject:operation];
        }
    }
    
    [[SDAFParseAPIClient sharedClient] enqueueBatchOfHTTPRequestOperations:operations progressBlock:^(NSUInteger numberOfCompletedOperations, NSUInteger totalNumberOfOperations) {
        
    } completionBlock:^(NSArray *operations) {
        if ([operations count] > 0) {
            //
            // Save the background context after all operations have completed
            //
            [[SDCoreDataController sharedInstance] saveBackgroundContext];
        }
        
        //
        // Execute the sync completed operations
        // 
        [self executeSyncCompletedOperations];
    }];
}

Now that this method is the last part of the sync engine’s flow you must update your -postLocalObjectsToServer method by replacing:

[self executeSyncCompletedOperations];

with:

[self deleteObjectsOnServer];

And that’s it! Congratulations – you’re done — despite that annoying alien interference! ;] Now all records created or deleted on your remote service will show up or disappear in your app. The reverse is true as well — all records created or deleted locally will be reflected on the remote service.

Where to go from here?

Here is the final example project from this tutorial series.

Even though you’ve got a pretty well-rounded app, there’s still a few ways to improve it. The next natural progression would be to add the ability for users to edit records locally; as well, any edits made on the local service should sync with the App.

There’s also the blaring omission of adding images within the App. Depending on the remote service, you could do this in a variety of ways. This is mostly an implementation detail, but is definitely something that users would want. To give you a starting point, the same POST strategy described above for pushing records to the server should be applicable.

Synchronization is an incredibly difficult task in the mobile iOS world. Depending on the amount of data that needs to be synced, the strategy outlined in this tutorial may not be optimal. Although efficiency was touched on throughout this tutorial, it’s usually best to avoid “premature optimization” – tuning your strategy to your app’s needs will always be better than cutting and pasting a canned solution.

While extending your app, if you find that memory footprints are increasing beyond acceptable levels, you should look into using autorelease pools in the processing loops. A good tip is to use Instruments to help you find the points where memory usage is high. You may also find that you should not invoke the synchronization process too often or too soon, or you may need to prevent application usage during the initial sync if you are expecting a lot of data to be coming in.

I hope that this tutorial helps you in determining the best strategy for synchronization in your apps. I also hope that I was able to make it as generic as possible so that you can use it in your real world applications.

If you have any questions or comments on this tutorial, please join the forum discussion below!


This is a post by iOS Tutorial Team Member Chris Wagner, an enthusiast in software engineering always trying to stay ahead of the curve. You can also find him on .

Chris Wagner

Contributors

Chris Wagner

Author

Over 300 content creators. Join our team.