I have a class with this code which gets called a few times per minute on average, and only runs on the main thread:
PFObject* eventObj = [PFObject objectWithClassName:#"AdminConsoleEvent"];
eventObj[kACParseEventName] = event;
eventObj[kACParseEventUrgency] = urgency;
if( param1 )
eventObj[kACParseEventParam1] = param1;
eventObj[kACParseEventPointerToAdminConsole] = self.adminConsole;
=== [eventObj saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
+++ if( !succeeded ) {
//here
}
}];
If I put a breakpoint where === is, I see that every time eventObj is how I expect... a non-nil object with valid information on it.
If I put a breakpoint where +++ is, then I see that it gets hit exactly only once -- the first time this code is called. If I look on the Parse data browser (online), sure enough, only the first object gets saved (immediately)! The rest never show up.
Why the heck isn't the block (+++) ever running for subsequent calls? Why aren't the other objects being saved?
OK this fixed it...
[PFObject saveAllInBackground:#[eventObj, self.adminConsole] block:^(BOOL succeeded, NSError *error) {
I assume that this is because there was a circular reference: self.adminConsole had a reference being added to it for eventObj, and eventObj had a reference being added to it for self.adminConsole. For whatever reason, that breaks Parse for me if I use saveInBackground directly on the objects.
Related
I am now confused by pointer to pointer even though I've read Why does NSError need double indirection? (pointer to a pointer) and NSError * vs NSError ** and much more.
I've done some thinking and still got some questions.
Here I wrote this:
NSError *error = [NSError errorWithDomain:#"before" code:0 userInfo:nil];
NSLog(#"outside error address: %p", &error];
[self doSomethingWithObj:nil error:&error];
In order to test the above NSError method, I wrote this:
- (id)doSomethingWithObj:(NSObject *)obj error:(NSError *__autoreleasing *)error
{
NSLog(#"inside error address: %p", error);
id object = obj;
if (object != nil)
{
return object;
}
else
{
NSError *tmp = [NSError errorWithDomain:#"after" code:0 userInfo:nil];
*error = tmp;
return nil;
}
}
But I found that the two logging addresses are different. Why is that?
2016-08-19 19:00:16.582 Test[4548:339654] outside error address: 0x7fff5b3e6a58
2016-08-19 19:00:16.583 Test[4548:339654] inside error address: 0x7fff5b3e6a50
Shouldn't they be the same since that was just a simple value copy? If they should be different, how can pointer to pointer end up pointing to the same NSError instance?
The variable in the caller has type NSError*. The address has type NSError* *. The function expect NSError* __autoreleasing *. Therefore the compiler creates a hidden variable of type NSError* __autoreleasing, copies the NSError* into the hidden variable before the call, and copies it back after the call to get the semantics of __autoreleasing right.
So, after initialisation on the first line, error is a pointer to an NSError object.
In the first log, you are logging the address at which that pointer is held. That's the effect of the & address-of operator. Anyway, that is the address gets passed into the doSomething method.
You're passing in: pointer -> pointer -> nserror-object.
But notice the double indirection in the signature of doSomething. The autoreleasing annotation makes it hard to spot, but its NSError **.
So the compiler takes your parameter and 'unwraps' it twice.
It starts as pointer -> pointer -> nserror-object. Then after the first indirection it becomes pointer -> nserror-object. Then after the second indirection it becomes nserror-object.
Ergo, you are logging two different things. The first is the address of the pointer to the nserror-object. The second is the address of the nserror-object itself.
EDIT:
#MANIAK_dobrii points out that the object pointed to by error is itself different in the before and after case.
That's true. If an error occurs in doSomething then it creates an entirely new NSError instance in the else clause. It then loads that back into the error pointer. That's why you would see two different addresses, afterwards the error pointer is pointing to another object entirely.
I'm hoping there's an experienced CloudKit guru out there, but based off my google search queries, I'm not sure if you exist. I think this may be a bug with Apple, but I can't be sure :\
I can save a subscription to my CKDatabase fine, no problems at all.
[publicDatabase saveSubscription:subscription completionHandler:^(CKSubscription *subscription, NSError *error) {
if (error)
{
//No big deal, don't do anything.
}
else
{
[[NSUserDefaults standardUserDefaults] setObject:[subscription subscriptionID] forKey:#"SUBSCRIPTION"];
}
}];
Whenever I change a field in my record, I get a push notification, and everything is happy.
My problem is removing this subscription.
I have tried calling -deleteSubscriptionWithID:completionHandler:
As you can see in the above code snippet, I save off the subscription ID (Have also confirmed it to be the correct subscription ID by calling -fetchAllSubscriptionsWithCompletionHandler:
I passed the subscriptionID in that message, like so:
[publicDatabase deleteSubscriptionWithID:[[NSUserDefaults standardUserDefaults] objectForKey:#"SUBSCRIPTION"] completionHandler:^(NSString * _Nullable subscriptionID, NSError * _Nullable error) {
if( error ) {
NSLog(#"ERROR: %#", [error description] );
}
else
{
NSLog(#"SUCCESS: %#", subscriptionID);
}
}];
But it doesn't delete my subscription:
And no matter what I pass as the subscriptionID, there is no error and I see "SUCCESS" upon "deleting".
...so yeah. Clearly that isn't going to work.
If I manually delete the subscription through the Cloudkit Dashboard, my -fetch call properly notices that and returns an empty array:
So at this point I'm certain that I'm either deleting a subscription incorrectly in code, or it's broken and (not likely) nobody has asked on SO or any other forum that I can find?
I have also tried using a CKModifySubscriptionsOperation
CKModifySubscriptionsOperation *deleteSub = [[CKModifySubscriptionsOperation alloc] initWithSubscriptionsToSave:nil subscriptionIDsToDelete:#[[[NSUserDefaults standardUserDefaults] objectForKey:#"SUBSCRIPTION"]]];
[publicDatabase addOperation:deleteSub];
No results :(
I delete subscriptions using the database.deleteSubscriptionWithID function.
If you want to make sure that the ID is correct you could also first fetch all of them using database.fetchAllSubscriptionsWithCompletionHandler({subscriptions, error in
Then in the completion handler check if it's a valid subscription using: if let subscription: CKSubscription = subscriptionObject
And then delete one or more using: database.deleteSubscriptionWithID(subscription.subscriptionID, completionHandler: {subscriptionId, error in
Here you can see code how I delete all subscriptions:
https://github.com/evermeer/EVCloudKitDao/blob/1bfa936cb46c5a2ca75f080d90a3c02e925b7e56/AppMessage/AppMessage/CloudKit/EVCloudKitDao.swift#L897-897
I don't understand how Objective-C loop system works. I have function (hope names are right, rather check in code) which executes query from Health Kit. I got my mind blown when I realised that function pass return value before query finishes.
__block bool allBeckuped = true;
HKSampleQuery *mySampleQuery = [[HKSampleQuery alloc] initWithSampleType:mySampleType
predicate:myPredicate
limit:HKObjectQueryNoLimit
sortDescriptors:#[mySortDescriptor]
resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
if(!error && results)
{
for(HKQuantitySample *samples in results)///main hk loop
{
allBeckuped = false;
NSLog(#"1");
}
}
}];//end of query
[healthStore executeQuery:mySampleQuery];
NSLog(#"2");
return allBeckuped;
I'm trying to check if there are any new data, but I don't know where to put condition for that, because nslog2 is called before nslog 1.
Any words I should Google up?
Any words I should google up?
You can start with: asynchronous design, blocks, GCD/Grand Central Dispatch should help as well - you're not using it but asynchronous designs often do.
Look at the initWithSampleType: method you are calling, it is an example of a method following the asynchronous model. Rather than return a result immediately, which is the synchronous model you are probably used to, its last argument, resultsHandler:, is a block which the method calls at some future time passing the result of its operation to it.
This is the pattern you will need to learn and follow.
Your method which contains the call to initWithSampleType: cannot return a result (e.g. your allBeckuped) synchronously. So it needs to take a "results handler" block argument, and the block you pass to initWithSampleType: should call the block passed to your method - and so the asynchronous flow of control is weaved.
HTH
I have this piece of code and tries to set the variable NotAllSaved to true but after call to the saveObject returns, the variable is false again.
NotAllSaved=false
[healthStore saveObject:theSample withCompletion:^(BOOL success, NSError *error){
if (!success){
if (error.code==HKErrorAuthorizationDenied) {
NotAllSaved=true;
} else {
...
}
}
}];
if (NotAllSaved) {
// Does never come here
}
How can I set the variable so I can handle the error outside the call to SaveObject? If I try to popup an alert there, the app takes a lot of time before showing the popup.
-
Added:
Thank you Lytic and Saheb Roy, your answers (and some additional Googling) solved my problem, so the solution that works for me is this:
__block bool NotAllSaved=false;
dispatch_group_t theWaitGroup = dispatch_group_create();
dispatch_group_enter(theWaitGroup);
[HDBI.healthStore saveObject:theSample withCompletion:^(BOOL success, NSError *error){
if (!success){
if (error.code==HKErrorAuthorizationDenied){
NotAllSaved=true;
} else {
.. .
}
}
dispatch_group_leave(theWaitGroup);
}];
dispatch_group_wait(theWaitGroup, DISPATCH_TIME_FOREVER);
if (NotAllSaved) {
This is a behavior of using blocks asynchronously.
The code contained in withCompletion:{} is actually not executed until the operation queue runs the block. Which typically will happen AFTER the check if(NotAllSaved)
The error must be handled within the completion block (or could call an error handler)
For Example:
NotAllSaved=false
[healthStore saveObject:theSample withCompletion:^(BOOL success, NSError *error)
{
if (!success)
{
if (error.code==HKErrorAuthorizationDenied)
{
NotAllSaved=true;
}
else
{
...
}
}
if (NotAllSaved)
{
// Will execute now
}
}];
This is because this isnt a method, its a block starting with the caret symbol ^, when you want to access any local variable inside a block, the block makes a copy of the variable or rather think of it like this, inside the block the variable is read only, to make the variable read write use this
__block BOOL NotAllSaved = false;
Now setting this by using this __block (2 underscores) you call the variable by its reference
Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
This question appears to be off-topic because it lacks sufficient information to diagnose the problem. Describe your problem in more detail or include a minimal example in the question itself.
Closed 8 years ago.
Improve this question
In my iOS app that uses Parse, some users will need to save thousand of objects in one action. I've tried iterating through the array of data and creating/saving the objects one by one, but this results in the objects saving to my data browser pretty slowly. Each object only needs to contain a few strings, so I don't understand why its taking so long to save these objects.
Is there a faster way to save thousands of objects to Parse?
Edited:
I've always used [PFObject saveAllInBackground:array block:^(BOOL succeeded, NSError *error) {}]; but....another method I have just attempted semi-successfully, was to upload a Json string as a PFFile(no 128k limit), and then use cloud code to parse it and create the necessary PFObjects. I was able to get this to work with small quantities, but unfortunately the cloud code timed out when using a large quantity. I instead opted to utilize a background job to perform the parsing. This takes a considerable amount of time before the data is completely available, but can handle the large amount of data. The upload time itself was much quicker. When using 1000 objects with 3 strings each upload was roughly .8 seconds, vs 23 seconds doing a save all in background, 5000 objects 3 strings each was only 2.5 seconds upload time. In addition to the quicker time you also get progress updates. Depending on the use-case, utilizing this alternative may work best if immediate and quick upload is important, vs making the data immediately available.
IOS Code:
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i<5; i++) {
//subclass of PFObject
Employee *employee = [Employee object];
employee.firstName = #"FName";
employee.lastName = #"LName";
employee.employeeID = #"fid54";
[array addObject:[employee dictionaryWithValuesForKeys:employee.allKeys]];
}
//Seperate class only to store the PFFiles
PFObject *testObject = [PFObject objectWithClassName:#"fileTestSave"];
testObject[#"testFile"] = [PFFile fileWithData:[NSJSONSerialization dataWithJSONObject:array options:0 error:nil]];
NSLog(#"started");
//**notice I am only saving the test object with the NSData from the JSONString**
[testObject saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (!error && succeeded) NSLog(#"succeeded");
else NSLog(#"error");
}];
Edited: Instead of saving in the beforeSave or afterSave Cloud Code which can cause timeout issues, the background job below can be run anytime. It grab all rows in the "fileTestSave" table, parses the JSON strings in those rows, and adds them to the "Person" table. Once completed it will rows from the table. All asynchronously!
var _ = require('underscore.js');
Parse.Cloud.job("userMigration", function(request, status)
{
// Set up to modify user data
Parse.Cloud.useMasterKey();
//Table called fileTestSave stores a PFFile called "testFile" which we will use an HTTPRequest to get the data. Is there a better way to get the data?
//This PFFile stores a json string which contains relavent data to add to the "Person" table
var testFileSave = Parse.Object.extend("fileTestSave");
var query = new Parse.Query(testFileSave);
query.find().then(function(results)
{
//Generate an array of promises
var promises = [];
_.each(results, function(testFileSaveInstance){
//add promise to array
promises.push(saveJsonPerson(testFileSaveInstance));
});
//only continue when all promises are complete
return Parse.Promise.when(promises);
}).then(function()
{
// Set the job's success status
console.log("Migration Completed NOW");
status.success("Migration completed");
}, function(error) {
// Set the job's error status
status.error("Uh oh, something went wrong.");
});
});
function saveJsonPerson(fileTestSave)
{
//Get the pffile testfile
var testFile = fileTestSave.get("testFile");
//get the fileURL from the PFFile to generate the http request
var fileURL = testFile["url"]();
//return the promise from the httpRequest
return Parse.Cloud.httpRequest({
method:"GET",
url: fileURL
}).then(function(httpResponse){
//return the promise from the parsing
return parsehttpResponse(httpResponse,fileTestSave);
},
function(error){
console.log("http response error");
}
);
}
function parsehttpResponse(httpResponse,fileTestSave)
{
var jsonArray = eval( '(' + httpResponse.text + ')' );
var saveArray =[];
//parse each person in the json string, and add them to the saveArray for bulk saving later.
for (i in jsonArray)
{
var personExtend = Parse.Object.extend("Person");
var person = new personExtend();
person.set("classDiscriminator",jsonArray[i]["classDiscriminator"]);
person.set("lastName",jsonArray[i]["lastName"]);
person.set("firstName",jsonArray[i]["firstName"]);
person.set("employeeID",jsonArray[i]["employeeID"]);
saveArray.push(person);
};
//return the promise from the saveAll(bulk save)
return Parse.Object.saveAll(
saveArray
).then(function(){
//return the promise from the destory
return fileTestSave.destroy(
).then(function(){
},function(error){
console.log("error destroying");
}
);
},function(error){
console.log("Error Saving");
}
);
}
Old Cloud Code that timed out as reference:
Parse.Cloud.afterSave("fileTestSave", function(request) {
//When accessing PFFiles you don't get the actual data, there may be an easier way, but I just utitlized an HTTPRequest to get the data, and then continued parsing.
var file = request.object.get("testFile");
var fileURL = file["url"]();
console.log("URL:"+fileURL);
Parse.Cloud.httpRequest({
method:"GET",
url: fileURL,
success: function(httpResponse)
{
var jsonArray = eval( '(' + httpResponse.text + ')' );
var saveArray =[];
for (i in jsonArray)
{
var personExtend = Parse.Object.extend("Person");
var person = new personExtend();
//May be a better way to parse JSON by using each key automatically, but I'm still new to JS, and Parse so I set each individually.
person.set("classDiscriminator",array[i]["classDiscriminator"]);
person.set("lastName",array[i]["lastName"]);
person.set("firstName",array[i]["firstName"]);
person.set("employeeID",array[i]["employeeID"]);
saveArray.push(person);
};
Parse.Object.saveAll(saveArray,
{
success: function(list) {
// All the objects were saved.
},
error: function(error) {
// An error occurred while saving one of the objects.
},
});
},
error: function(httpResponse) {
console.log("http response error");
}
});
});
Another method for uploading thousands of objects in the background, again this takes some time but can be sized to avoid timing out, as arrays are saved in chunks recursively. I've had no problem saving 10k+ items. Implemented as a category, just enter how many objects at a time you want save at a time, it will save them in the background serially, and recursively until all objects are saved, it also features progress updating via a separate block.
// PFObject+addOns.h
#import <Parse/Parse.h>
#interface PFObject (addOns)
+(void)saveAllInBackground:(NSArray *)array chunkSize:(int)chunkSize block:(PFBooleanResultBlock)block progressBlock:(PFProgressBlock)progressBlock;
#end
#import "PFObject+addOns.h"
#interface PFObject (addOns_internal)
+(void)saveAllInBackground:(NSArray *)array chunkSize:(int)chunkSize block:(PFBooleanResultBlock)block trigger:(void(^)())trigger;
#end
#implementation PFObject (addOns)
+(void)saveAllInBackground:(NSArray *)array chunkSize:(int)chunkSize block:(PFBooleanResultBlock)block progressBlock:(PFProgressBlock)progressBlock
{
unsigned long numberOfCyclesRequired = array.count/chunkSize;
__block unsigned long count = 0;
[PFObject saveAllInBackground:array chunkSize:chunkSize block:block trigger:^() {
count++;
progressBlock((int)(100.0*count/numberOfCyclesRequired));
}];
}
+(void)saveAllInBackground:(NSArray *)array chunkSize:(int)chunkSize block:(PFBooleanResultBlock)block trigger:(void(^)())trigger
{
NSRange range = NSMakeRange(0, array.count <= chunkSize ? array.count:chunkSize);
NSArray *saveArray = [array subarrayWithRange:range];
NSArray *nextArray = nil;
if (range.length<array.count) nextArray = [array subarrayWithRange:NSMakeRange(range.length, array.count-range.length)];
[PFObject saveAllInBackground:saveArray block:^(BOOL succeeded, NSError *error) {
if(!error && succeeded && nextArray){
trigger(true);
[PFObject saveAllInBackground:nextArray chunkSize:chunkSize block:block trigger:trigger];
}
else
{
trigger(true);
block(succeeded,error);
}
}];
}
#end
I think you should be able to do this with sending the save process in quantities of five into the background, so to speak fork it, "thread" it, as apple would refer to it.
here is the link to apples ios threading guides.
I have not used it yet, but I will soon need it as well, as I'm working on a massive database app.
here's the link
https://developer.apple.com/library/mac/Documentation/Cocoa/Conceptual/Multithreading/AboutThreads/AboutThreads.html
If you have an array of objects, you can use saveAllInBackgroundWithBlock. This method takes an array of PFObjects as its argument:
https://parse.com/docs/ios/api/Classes/PFObject.html#//api/name/saveAllInBackground:block:
For the faster processing, you can use the parse cloud code, whihc is simply a javascript. You can create a function which takes the array of the data as argument and then in function, you can save the objects.
Parse cloud code has better processing speed than the native one.
For its use, you can refer :
https://parse.com/docs/cloud_code_guide
https://parse.com/docs/js_guide