In my iOS app, I am using the forecast.io API to get a weather forecast for 3 specific days. Once I get the array from all 3, I want to create an NSMutableArray and add all of those objects to it. The problem I am getting is that it is trying to create the NSMutableArray before the forecast data is retrieved. Here is what I have so far:
typedef void(^myCompletion)(BOOL);
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:YES];
[self myMethod:^(BOOL finished) {
if(finished){
NSMutableArray *allOfIt = [[NSMutableArray alloc] initWithObjects:self.weatherSaturday, self.weatherSunday, self.weatherMonday, nil];
NSLog(#"%#", allOfIt);
}
}];
}
-(void) myMethod:(myCompletion) compblock{
//do stuff
ForecastKit *forecast = [[ForecastKit alloc] initWithAPIKey:#"MY-API-KEY"];
// Request the forecast for a location at a specified time
[forecast getDailyForcastForLatitude:37.438905 longitude:-106.886051 time:1467475200 success:^(NSArray *saturday) {
// NSLog(#"%#", saturday);
self.weatherSaturday = saturday;
} failure:^(NSError *error){
NSLog(#"Daily w/ time %#", error.description);
}];
[forecast getDailyForcastForLatitude:37.438905 longitude:-106.886051 time:1467561600 success:^(NSArray *sunday) {
// NSLog(#"%#", sunday);
self.weatherSunday = sunday;
} failure:^(NSError *error){
NSLog(#"Daily w/ time %#", error.description);
}];
[forecast getDailyForcastForLatitude:37.438905 longitude:-106.886051 time:1467648000 success:^(NSArray *monday) {
// NSLog(#"%#", monday);
self.weatherMonday = monday;
} failure:^(NSError *error){
NSLog(#"Daily w/ time %#", error.description);
}];
compblock(YES);
}
When the code is ran, it fires the NSLog for allOfIt, which shows as null, before it gets any of the forecast data. What am I missing?
The problem I am getting is that it is trying to create the NSMutableArray before the forecast data is retrieved
Yup, exactly. The problem is simply that you don't understand what "asynchronous" means. Networking takes time, and it all happens in the background. Meanwhile, your main code does not pause; it is all executed instantly.
Things, therefore, do not happen in the order in which your code is written. All three getDailyForcastForLatitude calls fire off immediately and the whole method ends. Then, slowly, one by one, in no particular order, the server calls back and the three completion handlers (the stuff in curly braces) are called.
If you want the completion handlers to be called in order, you need each getDailyForcastForLatitude call to be made in the completion handler of the getDailyForcastForLatitude call that precedes it. Or, write your code in such a way that it doesn't matter when and in what order the completion handlers come back to you.
Related
I've finished implementing a swipe feature similar to tinder but running into problems when saving objects.
I have 2 columns in the currentUser's row in my DB. One column hold an array of acceptedUsers (users have been liked) and the other is a rejectedUsers column that holds an array of rejected users (users that have been left swiped).
This is how my DB is updated upon swipe:
-(void)cardSwipedLeft:(UIView *)card;
{
NSString *swipedUserId = [[[userBeingSwipedArray objectAtIndex:0] valueForKey:#"user"] valueForKey:#"objectId"];
[currentUserImagesRow addUniqueObject:swipedUserId forKey:#"rejectedUsers"];
[currentUserImagesRow saveInBackground];
This works fine when I left about 2+ seconds between swipes. However fast swiping causes some saves to fail.
Is there a better way to do this without spoiling the users experience of the app?
I've saved multiple rows to my database before using for loops and this has always worked for me. I thought parse.com would be able to handle the speed of the saving.
I'm using both swift and objective-c for this project.
Thanks for your time
Its a fun problem. I think the way to go is to decouple the swiping and the saving a little bit more. Start with a collection of what needs saving...
#property(nonatomic, strong) NSMutableArray *toSave;
#property(nonatomic, assign) BOOL busySaving;
// on swipe
[self.toSave addObject: currentUserImagesRow];
[self doSaves];
- (void)doSaves {
// we get called because of user interaction, and we call ourselves
// recursively when finished. keep state so these requests don't pile up
if (self.busySaving) return;
if (self.toSave.count) {
self.busySaving = YES;
[PFObject saveAllInBackground:self.toSave block:^(BOOL succeeded, NSError *error) {
self.busySaving = NO;
// remove just the elements that were saved, remaining aware that
// new ones might have arrived while the last save was happening
NSMutableArray *removes = [#[] mutableCopy];
for (PFObject *object in self.toSave) {
if (!object.isDirty) [removes addObject:object];
}
[self.toSave removeObjectsInArray:removes];
[self doSaves];
}];
}
}
Now, instead of processing single saves, we can handle small batches. A user swipe causes a single save, and we block additional requests until the current one is complete. During the current request, we let more saves queue up as the user continues to interact. We call ourselves recursively after a save in case one or more records were queued. If none were, the recursive call ends immediately.
EDIT - Saving just one object is easier, just do the same blocking trick and recursive call at the end, but no need to track or save batches...
#property(nonatomic, assign) BOOL busySaving;
// on swipe
[self doSaves];
- (void)doSaves {
if (self.busySaving) return;
if (currentUserImagesRow.isDirty) {
self.busySaving = YES;
[currentUserImagesRow saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
self.busySaving = NO;
[self doSaves];
}];
}
}
Danh led me to my answer. There was an issue on my side which was causing the problem mentioned in the comments above. The same ID was being used after each swipe because I didn't remove it from the array that held my user objects. Below is how I solved the issue and used the Danh's answer to find my solution.
-(void)cardSwipedRight:(UIView *)card
{
NSString *swipedUserId = [[[userBeingSwipedArray objectAtIndex:0] valueForKey:#"user"] valueForKey:#"objectId"];
// I save the swiped users id and the key for the column it will
// be saved in e.g. if liked then "acceptedUsers" if notLiked then
// "rejectedUsers" so that the doSave method saves to correct column
NSArray *array = #[swipedUserId, #"acceptedUsers"];
[self.toSave addObject: array];
//remove user from array since he/she is now saved
[userBeingSwipedArray removeObjectIdenticalTo:[userBeingSwipedArray objectAtIndex:0]];
[self doSaves];
and then:
- (void)doSaves {
if (self.busySaving) return;
if (self.toSave.count) {
self.busySaving = YES;
NSArray *arrayWithSwipedUsersIdAndKeyForColumn = [self.toSave objectAtIndex:0];
[currentUserImagesRow addUniqueObject:[arrayWithSwipedUsersIdAndKeyForColumn objectAtIndex:0] forKey:[arrayWithSwipedUsersIdAndKeyForColumn objectAtIndex:1]];
[currentUserImagesRow saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
self.busySaving = NO;
//remove object that was just saved seeing as it is no longer needed
[self.toSave removeObjectIdenticalTo:arrayWithSwipedUsersIdAndKeyForColumn];
[self doSaves];
}];
}
}
Saving now works 100% of the time providing there is an internet connection. I can swipe as fast as I wish and objects always get saved.
I have a UITableView and a Refresh button in order to get new data from the server (if any).
[self startUpdates]; // animations
[[User myUser] getDataFromServer]; //async
[[User myUser] refreshElements:[[[UpdateContext alloc] initWithContext:data_ with:self with:#selector(endUpdates)] autorelease]];
[self.tableView reloadData];
The code above does not work well, because the getDataFromServer is an asynchronous method that is completed when the server returns the new data (the response). I want to be 100% sure that the refreshElements is being executed only when getDataFromServer gets the response back.
The question is: what is the correct way to do this. I want line 3 to gets executed if and only if line 2 gets the response from the server. Any ideas?
The easiest way would be to change the getDataFromServer method to accept a block that will contain the code that needs to be executed after the data comes from the server. You should ensure that the block will be executed in the Main thread.
Here is an example:
Changed method:
- (void)getDataFromServer:(void (^)(NSError * connectionError, NSDictionary *returnData))completionHandler{
//perform server request
//...
//
NSDictionary * data; //the data from the server
NSError * connectionError; //possible error
completionHandler(connectionError, data);
}
And how to call the new method with the block:
[self getDataFromServer:^(NSError *connectionError, NSDictionary *returnData) {
if (connectionError) {
//there was an Error
}else{
//execute on main thread
dispatch_async(dispatch_get_main_queue(), ^{
[[User myUser] refreshElements:[[[UpdateContext alloc] initWithContext:data_ with:self with:#selector(endUpdates)] autorelease]];
[self.tableView reloadData];
});
}
}];
I need to retrieve a couple of NSDicionaries that are compared against an id.
First, I'm calling a NSArray with these id's in them. I'm looping over them to see get the details of that id, and with that i'm calling another pfcloud function. Up until this point, all goes well. However, when I'm logging the payment details of the payment id's, the order sequence is is in a different order than the array I putted it in.
for(__block NSString *paymentId in success){
[self getPaymentDetails:paymentId];
}
So for instance: success = #[#"1",#"2",#"3"]
the method getPaymentDetails will log me#[#"details about 1", #"details about 3", #"details about 2"]
However, I need them to be in the exact same order.
This is my getPaymentDetails code:
-(void)getPaymentDetails:(NSString *)paymentId{
PFUser *currentUser = [PFUser currentUser];
[PFCloud callFunctionInBackground:#"getpaymentdetails"
withParameters:#{#"objectid": paymentId, #"userid": currentUser.objectId}
block:^(NSDictionary *success, NSError *error) {
if(success){
NSDictionary *payment = success;
NSString *amount = [payment objectForKey:#"amount"];
if (![amount isKindOfClass:[NSNull class]]) {
[self.amountArray addObject:amount];
}
else {
[self.amountArray addObject:#""];
}
NSString *from = [payment objectForKey:#"from"];
if (![from isKindOfClass:[NSNull class]]) {
[self.fromArray addObject:from];
}
else {
[self.fromArray addObject:#""];
}
} else{
NSLog(#"Error logged getpaymentdetails: %#", error);
}
}];
}
The values stored in the amountArray for instance, do not match the index of the paymentId
How come and how do I solve this?
It may be simpler to just move the whole for loop into the background and then call the Parse function synchronously
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group,queue, ^{
for(__block NSString *paymentId in success){
[self getPaymentDetails:paymentId];
}
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// Release the group when it is no longer needed.
dispatch_release(group);
Then in your getPaymentDetails you would call callFunction:withParameters:error: instead of callFunctionInBackground:withParameters:
This isn't an ideal solution however, as you are eliminating concurrency and so it will take longer to execute.
A better solution is to deal with the fact that the array is unordered at the conclusion of the loop and sort it once all of the data has been retrieved
The request callFunctionInBackground will do is executed asynchronously and there is no guarantee that the first call you make in your loop will finish first. This is not really related to Parse itself, that is just the nature of how this is done. You may end up with the same order by coincidence or a completely random one each time you execute this code.
If you want the order to stay the same, either pass in all IDs to your Cloud Function and update your Cloud Function to handle it or always wait for one call to finish, add the result to your array and then get the details with the next ID (basically a queue).
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'm using JSONModel to retrieve data from a simple webservice. I need to get the values of key #"message" into a mutable array.
- (void)viewDidLoad
{
[super viewDidLoad];
self.delegate = self;
self.dataSource = self;
NSString *conversationid = #"xx";
NSString *callURL = [NSString stringWithFormat:#"http://mydomain.com/inbox_messages.php?conversation=%#", conversationid];
_feed = [[MessageFeed alloc] initFromURLWithString:callURL
completion:^(JSONModel *model, JSONModelError *err)
{
self.messages = [[NSMutableArray alloc]initWithObjects:[_feed.messagesinconversation valueForKey:#"message"], nil];
NSLog(#"messages %#", self.messages);
}];
NSLog(#"test %#", self.messages);
}
The problem I'm experiencing is that while: NSLog(#"messages %#", self.messages); returns all the right data, NSLog(#"test %#", self.messages); returns (null).
The variable is declared in .h of my class as: #property (strong, nonatomic) NSMutableArray *messages;
This is probably a noob question but I'm a beginner and if somebody could give me a pointer in the right direction, I would be very happy.
Your NSLog for self.messages is outside of the completion block. The completion block is called after the data is loaded. The log is called immediately after creating the MessageFeed request. So, of course, the object self.messages is null because the request has not completed.
The solution to this would be to either handle all of your parsing within the completion block, or call another class method to parse the received data.
Your completion handler is being called after your NSLog("test %#", self.messages); is.
Blocks usually happen concurrently and when a certain event has occurred like the completion handler here or sometimes an error handler. By looking at your code you're probably getting something like:
test nil
messages
So your MessageFeed object is being run but it continues through your code and runs the NSLog outside of the completion handler scope first. When your JSON object has downloaded, which happens after, and parses it then runs the completion handler.
- (void)viewDidLoad
{
[super viewDidLoad];
self.delegate = self;
self.dataSource = self;
NSString *conversationid = #"xx";
NSString *callURL = [NSString stringWithFormat:#"http://mydomain.com/inbox_messages.php?conversation=%#", conversationid];
_feed = [[MessageFeed alloc] initFromURLWithString:callURL //this method takes some time to complete and is handled on a different thread.
completion:^(JSONModel *model, JSONModelError *err)
{
self.messages = [[NSMutableArray alloc]initWithObjects:[_feed.messagesinconversation valueForKey:#"message"], nil];
NSLog(#"messages %#", self.messages); // this is called last in your code and your messages has been has been set as an iVar.
}];
NSLog(#"test %#", self.messages); // this logging is called immediately after initFromURLWithString is passed thus it will return nothing
}