Trying to mutate array within completion block (CLGeocoder reverse geocoding) [duplicate] - ios

This question already has answers here:
Cannot AddObject to NSMutableArray from Block
(2 answers)
Closed 8 years ago.
I have city names and coordinates stored on my core data database and trying to get the state.
In order to do this I am reverse geocoding like this
code moved below
The problem is the locationStr object is not getting stored in the tempCities array. I have tested inside the block and I know the locationStr is getting created and exists.
I've been trying to figure this out for hours. Can someone clear this up for me?
Tell me if you need any other info.
EDIT:
The array is being used to fill a table view. The code is in a helper method which returns an array (the tempCities array). I'm checking the array against nil and 0 right after the for loop.
Heres what the UISearchControllerDelegate method looks like in the View controller
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
NSString *searchText = searchController.searchBar.text;
if ([searchText length] >= 3)
{
self.resultsArray = [[[CityHelper sharedInstance]testWithSearch:searchText]mutableCopy];
[self.resultsTableView reloadData];
}
}
And in the CityHelper class
- (NSArray *) testWithSearch: (NSString *)search
{
NSArray *cities = [self getCitiesStartingWith:search];//stores NSManagedObject subclass instances with cityName, lat, and long.
NSArray *coords = [self coordinatesForCities:cities];
NSMutableArray *tempCities = [NSMutableArray new];
for (MMCoordinate *coordinate in coords) {
CLGeocoder *geocoder = [CLGeocoder new];
CLLocation *location = [[CLLocation alloc]initWithLatitude:[coordinate.latitude floatValue]
longitude:[coordinate.longitude floatValue]];
if (![geocoder isGeocoding]) {
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
CLPlacemark *placemark = placemarks[0];
NSString *locationStr = [NSString stringWithFormat:#"%#, %#", placemark.locality, placemark.administrativeArea];
[tempCities addObject:locationStr];
}];
}
}
if (!tempCities) {
NSLog(#"its nil");
}
if ([tempCities count] == 0) {
NSLog(#"its 0");
}
return tempCities;
}
This always returns an empty (0 count) array

Essentially, the situation you have can be made clear with a simple code snippet:
- (void)someMethod
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// this will be executed in two seconds
NSLog(#"I'm the last!");
});
NSLog(#"I'm first!");
}
the block passed to dispatch_after is invoked after the method invocation ended, just like the block you passed to reverseGeocodeLocation:completionHandler:. You'll see on the console I'm first first, and then I'm the last!.
What should you do?
You need to solve that problem by introducing a callback to your method, because your method does things in the background and needs to call back when it's done. When you want to know how to declare a block for example in a method, this website explains how to use block syntax: http://fuckingblocksyntax.com/
In your case you need also to determine in the reverseGeocodeLocation:completionHandler block when to invoke the callback you should add to testWithSearch as a parameter. You could for example increase a counter every time you call reverseGeocodeLocation:completionHandler and decrease it every time when the completionHandler got invoked. When the counter reaches 0 you invoke the testWithSearch callback with the array result.
Example:
- (void)doSomething:(dispatch_block_t)callback
{
// we need __block here because we need to
// modify that variable inside a block
// try to remove __block and you'll see a compiler error
__block int counter = 0;
for (int i = 0; i < 15; i ++) {
counter += 1;
dispatch_async(dispatch_get_main_queue(), ^{
counter -= 1;
if (counter <= 0 && callback) {
callback();
}
});
}
}

Root cause is declaring the tempCities as a __block variable. Because of __block, iOS doesn't retain the tempCities on entering to the completion block. Since the completion block is executed later and the tempCities is a local variable, it actually gets deallocated at time when the completion block starts execution.
Please declare the tempCities as follows:
NSMutableArray *tempCities = [NSMutableArray new];

Related

iOS fetching data from server inside for loop

I want to fetch data from server with multiple calls inside for loop. I'm passing different parameter each time. I know it is possible to fetch data like, I'm fetching in code below :
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
dispatch_async(dispatch_get_main_queue(), ^{
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
// reloading table view.
[self.tableView reloadData];
});
}
} failure:^(NSError *error) {
}];
}
The problem is, Whenever I add data to dataSourceArry, It is not adding sequentially. It is adding according to response of API calls. Please let me know, If it is not clear.
In your case, I would allocate a mutable array first and set [NSNull null] at each position:
NSInteger count = [[feed objectForKey:#"content"] count];
NSMutableArray *dataSourceArray = [NSMutableArray arrayWithCapacity:count];
for (NSInteger i = 0; i < count; ++i) {
[dataSourceArray addObject:[NSNull null]];
}
Then, I would use something called dispatch groups (see more here http://commandshift.co.uk/blog/2014/03/19/using-dispatch-groups-to-wait-for-multiple-web-services/):
__block NSError *apiCallError = nil; // Just to keep track if there was at least one API call error
NSInteger index = 0;
// Create the dispatch group
dispatch_group_t serviceGroup = dispatch_group_create();
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// Start a new service call
dispatch_group_enter(serviceGroup);
// url with feedItem data.
NSURL *url = ...
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
dispatch_async(dispatch_get_main_queue(), ^{
// Add data to Data Source
// index should be the correct one, as the completion block will contain a snapshot of the corresponding value of index
dataSourceArray[index] = [placeData objectForKey:#"data"];
}
dispatch_group_leave(serviceGroup);
} failure:^(NSError *error) {
apiCallError = error;
dispatch_group_leave(serviceGroup);
}];
index++;
}
dispatch_group_notify(serviceGroup, dispatch_get_main_queue(),^{
if (apiCallError) {
// Make sure the Data Source contains no [NSNull null] anymore
[dataSourceArray removeObjectIdenticalTo:[NSNull null]];
}
// Reload Table View
[self.tableView reloadData];
});
Hope it works for you.
This might be of help for you,
//keep dictionary property which will store responses
NSMutableDictionary *storeResponses = [[NSMutableDictionary alloc]init];
//Somewhere outside function keep count or for loop
NSInteger count = 0;
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
//Find out index of feddItem
NSInteger indexOfFeedItem = [[feed objectForKey:#"content"] indexOfObject:feedItem];
NSString *keyToStoreResponse = [NSString stringWithFormat:#"%d",indexOfFeedItem];
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
//instead of storing directly to array like below
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
//follow this
//increase count
count++;
[storeResponses setObject:[placeData objectForKey:#"data"] forKey:keyToStoreResponse];
// reloading table view.
if(count == [feed objectForKey:#"content"].count)
{
NSMutableArray *keys = [[storeResponses allKeys] mutableCopy]; //or AllKeys
//sort this array using sort descriptor
//after sorting "keys"
for (NSString *key in keys)
{
//add them serially
[dataSourceArray addObject:[storeResponses objectForKey:key]];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
}
} failure:^(NSError *error) {
}];
}
Edit : The answer I have given is directly written here,you might face compilation errors while actually running this code
Don't reload your table each time in the loop. After the loop finishes fetching data , do a sorting on your datasourcearray to get the desired result and then reload table.
This is because you're calling web-services asynchronously so it's not give guarantee that it's give response in sequence as you have made request!
Now solutions for that :
You should write your api like it's give all data at a time. So,
You not need to make many network call and it will improve
performance also!
Second thing you can make recursive kind of function, I mean make another request from completion handler of previous one. In this case once you got response then only another request will be initialize but in this case you will have to compromise with performance!! So first solution is better according to me!
Another thing you can sort your array after you get all the responses and then you can reload your tableView
Try the following :-
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
// reloading table view.
dispatch_sync(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
});
} failure:^(NSError *error) {
}];
}

Returning an NSMutableArray in objective C

I have been trying to work out how to return an MSMutableArray in objective c, I have this code here.
- (NSMutableArray*)generateRandomNumber{
NSMutableArray *unqArray=[[NSMutableArray alloc]init];
int randNum;
int counter = 0;
while (counter< 6) {
randNum = arc4random_uniform(40.0);
if (![unqArray containsObject:[NSNumber numberWithInt:randNum]]) {
[unqArray addObject:[NSNumber numberWithInt:randNum]];
counter++;
}
}
return unqArray;
}
- (IBAction)button:(id)sender {
NSMutableArray *results = [[NSMutableArray alloc]init];
*results = generateRandomNumber();
}
This is my code at the moment, where it says,
NSMutableArray *results = [[NSMutableArray alloc]init];
I get the following errors...
Implicit declaration of function 'generateRandomNumber' is invalid in C99
Assigning to 'NSMutableArray' from incompatible type 'int'
If anybody is willing to show me my mistake and help me out as well as many others I will appreciate it as much as possible.
Thanks to all who help me out!!!
generateRandomNumber is a method, not a C function, so use [self generateRandomNumber] to call it, and you are assigning results incorrectly, so:
results = [self generateRandomNumber];
Alternatively if you want to define it as a C function, use:
NSMutableArray *generateRandomNumber() {
...
}
Also, as pointed-out by #Larme, there is no need to allocate results before assigning it from generateRandomNumber.
- (IBAction)button:(id)sender {
NSMutableArray *results = [[NSMutableArray alloc]init];
results = [self generateRandomNumber];
}
Or more simply:
- (IBAction)button:(id)sender {
NSMutableArray *results = [self generateRandomNumber];
// Do something with this Array
}
Though you should consider a different name for that method as currently it sounds like it is returning one number, not an array of random numbers.
When calling/using a function in Objective-C
it's [self generateRandomNumber]
When you declare a variables it's NSMutableArray *results = [[NSMutableArray alloc]init]; and use if like results = mutableArry, * before it is not needed..
about your problem, you better do it like this:
- (IBAction)button:(id)sender
{
NSMutableArray *results = [self generateRandomNumber];
}
you dont need to allocate anymore, because you are passing an mutable array that is already allocated..
Your method generateRandomNumbers should work as expect the problem is here:
- (IBAction)button:(id)sender {
NSMutableArray *results = [[NSMutableArray alloc]init];
*results = generateRandomNumber();
}
The first thing is that you are creating an unneeded mutable array, second your are trying to substitute the value of the pointer that points to result array, third generateRandonNumber is a method not a function you should call it like [self generateRandomNumber].
Also I would implement a optimization since I'm pretty sure that you are not going to modify the random number array, the returned instance should be an immutable copy.
Here the final code:
- (NSArray*)generateRandomNumber{
NSMutableArray *unqArray=[[NSMutableArray alloc]init];
int randNum;
int counter = 0;
while (counter< 6) {
randNum = arc4random_uniform(40.0);
if (![unqArray containsObject:[NSNumber numberWithInt:randNum]]) {
[unqArray addObject:[NSNumber numberWithInt:randNum]];
counter++;
}
}
return unqArray.copy;
}
- (IBAction)button:(id)sender {
NSArray *results = nil;
results = [self generateRandomNumber];
}
Your generateRandomNumbers method should work with no problem, but in the IBAction there you try to put an INT directly into the array, you need to wrap it in an NSNumber like you have in the other method there
Your method is correct.
What you are doing wrong is here: *results = generateRandomNumber();
There is no need of an * here,because in this way you are trying to assign the pointer address of your array to results object.
Secondly you are trying to call an Objective-C method in C syntax.
So the correct syntax would be: results = [self generateRandomNumber];

Check if NSMutableArray contains an int

I am making an app which asks the user a series of questions. The question asked depends on the random int produced. When an int is used, I want to add it to an NSMutableArray, and then check if the array contains a number the next time a random number is chosen. I am currently using the following code to do this:
- (void) selectQuestionNumber {
textNum = lowerBounds + arc4random() % (upperBounds - lowerBounds);
if ([previousQuestions containsObject:[NSNumber numberWithInt:textNum]]) {
[self selectQuestionNumber];
NSLog(#"The same question number appeared!");
} else {
questionLabel.text = [self nextQuestion];
[self questionTitleChange];
NSLog(#"New question made");
}
[previousQuestions addObject:[NSNumber numberWithInt:textNum]];
}
However, the code NSLog(#"The same question number appeared!"); is never shown in the console, even when the same question will appear twice.
My code is obviously non-functional, so what code can I use to check if an NSMutable array contains an int?
Original solution (works with Array and Set):
-(void)selectQuestionNumber
{
textNum = lowerBounds + arc4random() % (upperBounds - lowerBounds);
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"intValue=%i",textNum];
NSArray *filteredArray = [previousQuestions filteredArrayUsingPredicate:predicate];
if ([filteredArray count]) {
[self selectQuestionNumber];
NSLog(#"The same question number appeared!");
} else {
questionLabel.text = [self nextQuestion];
[self questionTitleChange];
NSLog(#"New question made");
}
[previousQuestions addObject:[NSNumber numberWithInt:textNum]];
}
Best solution, and better performance, especialy with mutableSet ( According to Duncan C).
-(void)selectQuestionNumber
{
textNum = lowerBounds + arc4random() % (upperBounds - lowerBounds);
if ([previousQuestions containsObject:[NSNumber numberWithInteger:textNum]]) {
[self selectQuestionNumber];
NSLog(#"The same question number appeared!");
} else {
questionLabel.text = [self nextQuestion];
[self questionTitleChange];
NSLog(#"New question made");
// And add the new number to mutableSet of mutableArray.
[previousQuestions addObject:[NSNumber numberWithInteger:textNum]];
}
}
Your problem is likely something other than detecting membership of NSNumbers in an NSArray. It can take a large number of tries for a set of random numbers to repeat. It is theoretically possible for it to not repeat until every possible value has been generated once. For a large range of legal values it can take quite a while.
I suggest logging the values that you add to the array on each pass, and the new value.
Your code above always adds the new value to the array even it if matched, so your array is going to grow with duplicates. You would be better off only adding the new number to the array if it did not match. you would probably also be better off using an NSMutableSet instead of an array. NSSets contain at most one instance of an object, and their containsObject method is faster than that of NSArray.
Instead of using NSArray, you can use NSMutableIndexSet. This is the same as NSSet, with just NSUIntegers instead of objects. Very useful.
//during init
NSMutableIndexSet *tSet = [[NSMutableIndexSet alloc] init];
//...
//later in the code, in whatever loop you have on new values
NSUInteger newInt = lowerBounds + arc4random() % (upperBounds - lowerBounds);
if ([tSet containsIndex:newInt]){
//value already exists in the set
}
else {
//value does not exist, add it
[tSet addIndex:newInt];
}
NSMutableSet *myNumbers = [NSMutableSet Set]; // or NSMutableArray..
NSNumber *aNumber = [NSNumber numberWithInt:getRandomInt() ]; //let us say it returns 1.
[myNumbers addObject:aNumber];
-(BOOL)succesfullyAddNewUniqueRandomMember{
NSInteger randInt = getRandomInt(); //let us say it returns 1 again..
NSNumber *aSubsequentNumber = [NSNumber numberWithInt:randInt;
for (NSNumber *previousEntry in myNumbers){
if ([previousEntry isEqual:aSubsequentNumber]) return NO;
}
[myNumbers addObject:aSubsequentNumber];
return YES;
}
^ are these objects equal (aNumber, aSubsequentNumber) ? YES
^ are they the same object ? NO, two different NSNumbers made with equal integer..
NSSet will also happily add both, because they are not the same object.
therefore you need to loop through and compare directly to each previous member, the (already contains object) filter of NSSet will not do the trick.
by wrapping this in a -(BOOL) type method we can repeat it with a
while(![self succesfullyAddNewUniqueRandomMember])
In other words, in your code
if ([previousQuestions containsObject:[NSNumber numberWithInt:textNum]])
always returns NO because it is comparing NSNumber objects, not their integerValue.

NSMutableArray adds object locally, but seems to clear when accessed in the next function

So basically, I declare an NSMutableArray, alloc and init it in the viewdidload, call a function that adds an object to the array (which I verify with an NSLog right after its added), but then when the next method is called and tries to access the NSMutableArray— it goes blank.
What is going on here?
#interface ViewController ()
{
NSMutableArray * array;
}
#end
#implementation ViewController
#synthesize array;
- (void)viewDidLoad
{
[super viewDidLoad];
array = [[NSMutableArray alloc] init];
[self firstfunction];
if (j.intValue == 1)
{
[self secondfunction];
}
}
- (void)firstfunction
{
//a bunch of stuff happens with a server
[array addObject:object];
NSLog(#"There are %d in array",array.count);
//** NSLOG RETURNS "THERE ARE 1 IN ARRAY" **//
[tableView reloadData];
//clean up code
j = [[NSNumber alloc] initWithInt:1];
}
- (void)secondfunction
{
NSLog(#"There are these many in the array now %d",array.count);
//**NSLOG RETURNS:"There are these many in the array now 0"**//
}
Thanks in advance.
Please write [self secondfunction]; at the end of first function.
Reason is, before the array is filled your second function is called.
because I think you are having the server link code in some asynchronous block.SO after you got the results you are adding it to the array,But secondFunction is executed before you get the results and add it to array.
-(void)firstfunction{
//a bunch of stuff happens with a server
[array addObject:object];
NSLog(#"There are %d in array",array.count);
[tableView reloadData];
[self secondfunction];
}
-(void)secondfunction{
NSLog(#"There are these many in the array now %d",array.count)
//**NSLOG RETURNS:"There are these many in the array now 0"**//
}

EXC_BAD_ACCESS thrown when adding objects to NSMutableArray

I am getting an EXC_BAD_ACCESS error when attempting to add objects to an array. I understand that this could mean I am pointing to something that doesn't exist in memory, or that the objects contain a nil value.
Code:
- (void)fadeInPlayer:(AVAudioPlayer *)player withMaxVolume:(float)maxVolume {
NSLog(#"player: %#", player);
NSLog(#"maxVolume: %f", maxVolume);
NSMutableArray *playerAndVolume = [NSMutableArray arrayWithObjects: player, maxVolume, nil];
if (player.volume <= maxVolume) {
player.volume = player.volume + 0.1;
NSLog(#"%# Fading In", player);
NSLog(#"Volume %f", player.volume);
[self performSelector:#selector(fadeInPlayer:withMaxVolume:) withObject:playerAndVolume afterDelay:0.5];
//playerAndVolume array used here because performSelector can only accept one argument with a delay and I am using two...
}
}
The strange thing is that when I print the objects I am trying to add to the console (shown as NSLogs above), they return data:
player: <AVAudioPlayer: 0x913f030>
maxVolume: 0.900000
The app crashes immediately after the NSLogs. The rest of the code works fine without the array, but I need to use it to call performselector:withObject:AfterDelay on the method.
So there must be a problem with how I am initialising the array, or maybe the object types, but I can't figure it out.
Any help appreciated.
You can't add a float to an NSArray. You'll have to wrap it in an NSNumber.
But
The real problem is that the first argument passed in is the NSArray you created, the second parameter passed into your function is the NSTimer that backs the performSelector:afterDelay:... methods. It doesn't spread out the objects in your array, it just passes the array as the first argument. If you are insistent upon remaining with this API design then you need to test the class of the first argument to see if it is an NSArray or an AVAudioPlayer. You could implement this function like this:
-(void)fadeInPlayer:(AVAudioPlayer *)player withMaxVolume:(NSNumber *)maxVolume {
if ([player isKindOfClass:[NSArray class]]){
// This is a redundant self call, and the player and max volume are in the array.
// So let's unpack them.
NSArray *context = (NSArray *)player;
player = [context objectAtIndex:0];
maxVolume = [context objectAtIndex:1];
}
NSLog(#"fading in player:%# at volume:%f to volume:%f",player,player.volume,maxVolume.floatValue);
if (maxVolume.floatValue == player.volume || maxVolume.floatValue > 1.0) return;
float newVolume = player.volume + 0.1;
if (newVolume > 1.0) newVolume = 1.0;
player.volume = newVolume;
if (newVolume < maxVolume.floatValue){
NSArray *playerAndVolume = [NSArray arrayWithObjects: player, maxVolume, nil];
[self performSelector:#selector(fadeInPlayer:withMaxVolume:) withObject:playerAndVolume afterDelay:0.5];
}
}
You would use this, wrapping the float in an NSNumber, like so:
[self fadeInPlayer:player withMaxVolume:[NSNumber numberWithFloat:1.0]];
Please note, this would be considered a very odd function, but this code does run.

Resources