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.
Related
I am using Azure Mobile Service as a backend for an iOS app. 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 now into testing and I run into an error: "The item provided was not valid" when I try to synchronize data.
Here's what I am doing:
I add a new athlete to the syncTableWithName:#"Athlete" with this:
NSDictionary *newItem = #{#"firstname": #"Charles", #"lastname": #"Lambert", #"laterality" : #"Orthodox"};
[self.athletesService addItem:newItem completion:^{
NSLog(#"New athlete added");
}];
Here's the addItem function:
-(void)addItem:(NSDictionary *)item completion:(CompletionBlock)completion
{
// Insert the item into the Athlete table
[self.syncTable insert:item completion:^(NSDictionary *result, NSError *error)
{
[self logErrorIfNotNil:error];
// Let the caller know that we finished
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}];
}
For now everything is fine and the item is in the syncTable. The problem is when I try to synchronize with the Azure Mobile Service. Here's the syncData function I am calling:
-(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];
}];
}
The pushWithCompletion gets me the error: "The item provided was not valid." and same for the pullData function that gets called after:
-(void)pullData:(CompletionBlock)completion
{
MSQuery *query = [self.syncTable 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.syncTable pullWithQuery:query queryId:#"allAthletes" completion:^(NSError *error) {
[self logErrorIfNotNil:error];
// Let the caller know that we finished
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}];
}
I have tried inserting directly in the MSTable and that works fine. It's really when I am using the MSSyncTable that I run into this error. Although when I insert data manually in my database and that I synchronize my context I can fetch data and display within my UITableView.
Lookin forward to see what you guys think about this. Thanks a lot!
I just edited my question thanks to #phillipv.
When I add an item using NSDictionary just like I did I run into the error "The item provided was not valid". So I tried adding an item by first inserting it to my managedObjectContext and then calling:
NSDictionary *dict = [MSCoreDataStore tableItemFromManagedObject:newAthlete];
I then I get the error when I try to sync: "The item provided did not have a valid id."
I feel like I am experiencing a circle.. :S
#Charley14, you can work around the bug by adding the following handler.
- (void)tableOperation:(nonnull MSTableOperation *)operation onComplete:(nonnull MSSyncItemBlock)completion
{
NSMutableDictionary *rwItem = [NSMutableDictionary dictionaryWithDictionary:operation.item];
// Temporary workaround
[rwItem removeObjectsForKeys:#[ #"relationship1", #"relationship2"]];
operation.item = rwItem;
[operation executeWithCompletion:completion];
}
The tableOperation:onComplete: handler is simply removing keys that correspond to the relationships. You will have to replace 'relationship1', 'relationship2' in the code snippet with names of actual relationships in your application.
Once the bug (https://github.com/Azure/azure-mobile-services/issues/779) is fixed, this workaround can be removed.
This appears to be a bug in the iOS SDK, as the Many to One relationship is not supposed to be exposed in the object given to the operation during a Push call.
Created the following bug with more details on GitHub: https://github.com/Azure/azure-mobile-services/issues/779
The cause of the error message is due to the fact that the relationship is a NSSet on the object, and the NSJSONSerializer throws as it does not know how to convert that to JSON.
I am new with Parse and Ios-development.
I develop a ios-app that use Parse as backend.
I have got the main-function to work now, but i have a BIG problem.
I want to create a separate class for my API-handling to Parse. As i set it up now i have my parse-code directly in my view-controllers and as far as i know that not that nice coding.
But, the issue is to handle the background-jobs. Let say if i want to do a GET from the server, this can be done in a background-thread, just using "findObjectsInBackgroundWithBlock"
The problem is when i move this method to a separate API-class. Then my ViewController ask my API-class to get all the objects an the API-class will return it as soon its done. It will nor run in the background, i cant return a NSMutableArray with objects to the viewController until the fetch is done.
I have thinking that i maybe can get the data from parse synchronously in my API-class by using [query findObjects:&error] , as long as i figure out how to create my get-method in the API-class to run asynchronously.
I have try to create my API-method as a asynchronously method using blocks but will not run in background on a separate thread. (I am new to blocks an dont evan no if thats the correct way to crate a method that will run in a separate thread when using it)
Here is my API-method (Class: APIClient)
+ (void) GETAllShoppingGroups:(void (^) (NSMutableArray*))completionBlock{
//Create a mutable array (nil)
NSMutableArray *shoppingGroupsArray = nil;
//Create query for class ShoppingGroupe
PFQuery *query = [ShoppingGroupe query];
//Filter - find only the groups the current user is related to
[query whereKey:#"members" equalTo:[PFUser currentUser]];
//Sort Decending
[query orderByDescending:#"createdAt"];
//Tell Parse to also send the real member-objects and not only id
[query includeKey:#"members"];
//Send request of query to Parse with a "error-pointer"and fetch in a temp-array
NSError *error = nil;
NSArray *tempArray = [NSArray arrayWithArray:[query findObjects:&error]];
//Check for success
if (!tempArray) {
NSLog(#"%#", error);
NSLog(#"ERROR: %#", [error userInfo][#"error"]);
return completionBlock(shoppingGroupsArray);
} else {
//Seccess
shoppingGroupsArray = tempArray.mutableCopy;
completionBlock(shoppingGroupsArray);
}
}
Here is my ViewController Class (Class: ShoppingGruopViewController)
- (void) getAllObjects{
//Init array if nil
if (!self.shoppingGroupeArray) {
self.shoppingGroupeArray = [[NSMutableArray alloc]init];
}
//Remove old objects
[self.shoppingGroupeArray removeAllObjects];
//Get objects
[APIClient GETAllShoppingGroups:^(NSMutableArray* completionBlock){
if (completionBlock) {
[self.shoppingGroupeArray addObjectsFromArray:completionBlock]; }
[self.tableView reloadData];
}];
}
When I upload any data onto the Parse cloud, it stores the row at the top of the table. So every time a new row is added it gets stored at the top.
However, when I retrieve all the rows, the data is retrieved bottom up approach.
So let's say initially cloud is empty.
Push a
Cloud Looks like : a
Push b
Cloud Looks like : b a
Push c
Cloud Looks like : c b a
And now when I retrieve the data, i get it like: a b c
Now what I want is when data is inserted it is put at the 2nd location and not the first location.
Example:
Initial Cloud : "X"
Push a: "X" a
Push b: "X" b a
Push c: "X" c b a
Is there any way I can push data in Parse like this?
I'm doing because when I retrieve data, I wish to execute a method after all the data is retrieved in the background thread. So this way when I reach at X, I can call my method.
Found a solution to the problem....different approach though:
I count the number of objects for that query
Keep a counter increasing every time a record is fetched
when counter reached = total number of objects , then execute method.
NSInteger count= [query countObjects];
for (PFObject *obj in objects) {
[Names addObject:LastName];
if ([Names count] == count) {
[self getResults];
} }
^^^ solution is wrong
This way apparently does block the main thread, so there's a possibility of the app being killed.
Does anyone have any other solutions?
The data stored in the Parse Cloud is in an arbitrary order. Due to the way they currently store the data you may see new data at the end but this behaviour should not be relied upon.
If you want to retrieve data in a specific order then you should add a sorting operation to your PFQuery rather than trying to store the data in a specific order.
I am not sure what you are trying to achieve with your second code block. What does [self getResults] do?
If you want to execute some code after the results have been retrieved, why not just use findObjectsInBackgroundWithBlock?
This allows you to specify code to be executed once the data is received -
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
// The find succeeded.
names = [NSMutableArray arrayWithArray:objects];
// Do something with the found objects
for (PFObject *object in objects) {
NSLog(#"Object Name: %#", object.objectId);
}
} else {
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
If you need to update any UI from the block (or a method called within the block) then you should perform that on the main thread -
dispatch_async(dispatch_get_main_queue(), ^{
[self updateMyUIWithResult:objects];
});
So, putting it together you get -
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateMyUIWithResult:objects];
});
} else {
// Log details of the failure
NSLog(#"Error: %# %#", error, [error userInfo]);
}
}];
I have an block to run to get a query set of data from a Azure Database :
[query readWithCompletion:^(NSArray *items, NSInteger totalCount, NSError *error) {
How can i get the *items and put it in a tableview ? As I cannot see this variable out of the block. I have tried to assign an external __ array in the block , but no use.
Has anyone tried to do this ?
thanks
Jason
i think you need some thing like this
[RSSParser parseRSSFeedForRequest:request success:^(NSArray *feedItems) {
self.linkArray=feedItems;//
dispatch_async(dispatch_get_main_queue(), ^{
//3
[self.tableView reloadData];
});
}
failure:^(NSError *error) { }];
The easiest way to see how this should work is to download the Quickstart application from the Windows Azure Portal after you create a Mobile Service. The quickstart is a Todo application that pulls down todo items you have added and displays them in a ListView. When you call your Mobile Service's read method, you specify a callback as seen here:
[query readWithCompletion:^(NSArray *results, NSInteger totalCount, NSError *error)
{
[self logErrorIfNotNil:error];
items = [results mutableCopy];
// Let the caller know that we finished
completion();
}];
In this method, a QSCompletionBlock called completion is called from the readWithCompletion method when it's received a response from your Mobile Service. In the Quickstart, that completion looks like this:
[self.todoService refreshDataOnSuccess:^
{
if (self.useRefreshControl == YES) {
[self.refreshControl endRefreshing];
}
[self.tableView reloadData];
}];
This then triggers the tableview to reload data. There are other methods that are part of the TableViewController class that are necessary to bind the data to the table view though so I'd highly recommend walking through the Quickstart code.
I am currently developing an app that that will be used by association members that are going to a large annual conference.
The app will pull data from a database that is created by the app and populate it via a web service. The web service is split into 8 pages (this will likely go up). Each page represents a table in the database. The app will have several table views that will be populated by data in one or more of the tables in the database.
What I need is a the best method for going through the list of tables, connecting to their respective web service pages and then populating the respective database tables. This updating needs to take place in the background so the UI doesn't become unresponsive and/or show a downloading/updating/waiting kind of status.
So far I have a static array of the table names and have a loop that goes through the array and appends a URL string with the names, for example:
-(void)startUpdate
{
NSArray* tableNames = #[#"speaker", #"exhibitor", #"workshop", #"workshopspeakers", #"schedule", #"location", #"feedback", #"note", #"usage", #"user"];
NSUInteger loopCount = tableNames.count;
for (int i = 0; i < loopCount; ++i){
NSString *tableName = [tableNames objectAtIndex:i];
[self fetchObjectsWithTableName:[tableName mutableCopy] completion:^(NSArray* objects, NSError*error){
if (error) {
} else {
}
}];
}
}
fetchObjectsWithTableName method then has the connections and retrieves the data:
-(void)fetchData:(NSString *)tableName
withCompletion:(completion_t)completionHandler
{
NSString *currentURL = [NSString stringWithFormat:#"https://testapi.someURL.com/api/congress/%#", tableName];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:currentURL]];
[request addValue:#"application/json" forHTTPHeaderField:(#"Accept")];
[NSURLConnection sendAsynchronousRequest:request
queue:[[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
NSError* err = error;
NSArray* objects; // final result array as a representation of JSON Array
if (response) {
NSHTTPURLResponse *newResp = (NSHTTPURLResponse*)response;
if (newResp.statusCode == 200) {
NSLog(#"FetchData - Status code = %li", (long)newResp.statusCode);
if ([data length] >0 && error == nil)
{
NSError* localError;
objects = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
if (objects) {
if (completionHandler) {
completionHandler(objects, nil);
}
//NSLog(#"Objects in current table - %# = %#", tableName, objects);
[self.tables addObject:objects];
// NSLog(#"Tables now = %#", self.tables);
NSLog(#"FetchData - Objects in current table - %# = %lu", tableName, (unsigned long)objects.count);
return;
} else {
err = localError;
}
} else {
NSLog(#"FetchData - objects is empty");
return;
// err = ...
}
}
NSLog(#"FetchData - Response code not 200#");
}
if (objects == nil) {
NSLog(#"FetchData - Nothing found in table: %#", tableName);
//assert(err);
if (completionHandler) {
completionHandler(nil, err);
}
}
}];
}
This currently goes through the array of table names, makes a connection based on each one and pulls back JSON data and stores it in a temporary array 'objects'. I think what I need now is that in each iteration of this 'objects' array is copied to the relevant table in the database, i.e. 'speaker' table name makes a connection: https://testapi.someURL.com/api/congress/speaker and the JSON is entered into the database under the table 'speaker'. How and where do I do that? Will I need to add a completion handler to startUpdate? If so, how? I don't understand completion handlers despite looking at several examples. Thanks.
No, do it in the NSURLConnection completion block after you have updated your temporary storage.
But, change your approach overall.
If you're only willing to change a bit, start using NSOperationQueue to limit the number of connections that you're trying to make at the same time. Preferably also use Core Data.
If you're willing to make a bigger change, definitely move to Core Data and look at using a framework like RestKit to do all of the download, mapping and storage for you.
(note, in both cases you need to set the max concurrent operation limit to prevent the app from flooding the network with requests - a limit of 5 should be good).