iOS Swift - Draw route using multiple coordinates in MKMapView [duplicate] - ios

I'm using this code to connect pins(points on map) with polyline:
CLLocationCoordinate2D coordinates[allLocations.count];
int i = 0;
for (locHolder *location in allLocations) {
coordinates[i] = CLLocationCoordinate2DMake([location.lat floatValue], [location floatValue]);
i++;
}
MKPolyline *polyline = [MKPolyline polylineWithCoordinates:coordinates count:[allLocations count]];
self->polyline = polyline;
[self.mapView addOverlay:self->polyline level:MKOverlayLevelAboveRoads];
But this code connects them over-the-air(ignoring roads), is it possible to connect multiple CLLocationCoordinate2D locations with polyline that follows roads?
**
And a shor sub question,
Is there any difference in performance between [allLocations count] and allLocations.count.
Thanks.

In these days I've came across with the same problem, and looking for answers here and there, I found this answer from Rob very useful for this case.
First, supose you have in your MapViewController, an array of objects containing an origin an a destination, each one being of type CLLocationCoordinate2D
In your MapViewController.h
#property (nonatomic, strong) NSArray *locations;
Then, in the implementation, populate that array with some data that goes like this:
_locations = [NSArray arrayWithObjects:
[RouteObject routeWithOrigin:CLLocationCoordinate2DMake(-23.595571, -46.684408) destination:CLLocationCoordinate2DMake(-23.597886, -46.673950)],
[RouteObject routeWithOrigin:CLLocationCoordinate2DMake(-23.597886, -46.673950) destination:CLLocationCoordinate2DMake(-23.597591, -46.666805)],
[RouteObject routeWithOrigin:CLLocationCoordinate2DMake(-23.597591, -46.666805) destination:CLLocationCoordinate2DMake(-23.604061, -46.662728)], nil];
Now that you have your array of origin and destinations, you can start drawing a route between them (from A to B, from B to C and from C to D).
Add a button and then connect an IBAction so in that method you are going to do de magic.
- (IBAction)btnDirectionsPressed:(id)sender {
[self enableUI:NO]; // This method enables or disables all the UI elements that interact with the user
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); // Create the semaphore
__block NSMutableArray *routes = [[NSMutableArray alloc] init]; // Arrays to store MKRoute objects and MKAnnotationPoint objects, so then we can draw them on the map
__block NSMutableArray *annotations = [[NSMutableArray alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
for (RouteObject *routeObject in _locations) {
MKDirectionsRequest *directionsRequest = [MKDirectionsRequest new];
[directionsRequest setTransportType:MKDirectionsTransportTypeAutomobile];
[directionsRequest setSource:[routeObject originMapItem]];
[directionsRequest setDestination:[routeObject destinationMapItem]];
[directionsRequest setRequestsAlternateRoutes:NO];
MKDirections *direction = [[MKDirections alloc] initWithRequest:directionsRequest];
// For each object in the locations array, we request that route from its origin and its destination
[direction calculateDirectionsWithCompletionHandler: ^(MKDirectionsResponse *response, NSError *error) {
if (error) {
NSLog(#"There was an error getting your directions");
return;
}
MKRoute *route = [response.routes firstObject];
[routes addObject:route];
[annotations addObject:[routeObject destinationAnnotation]];
dispatch_semaphore_signal(semaphore); // Send the signal that one semaphore is ready to consume
}];
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
for (int i = 0; i < _locations.count; i++) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // Wait until one semaphore is ready to consume
dispatch_async(dispatch_get_main_queue(), ^{ // For each element, dispatch to the main queue to draw route and annotation corresponding to that location
MKRoute *route = routes[i];
[self.mapView addOverlay:route.polyline];
[self.mapView addAnnotation:annotations[i]];
});
}
dispatch_async(dispatch_get_main_queue(), ^{ // Finally, dispatch to the main queue enabling elements and resizing map to show all the annotations
[self enableUI:YES];
[self fitRegionToRoutes];
});
});
}
Hope this help!
Joel.

Related

Get directions from one location to another based on vehicle course

I am developing an iOS app where I have to get directions from one location to another. I am getting the directions using the MKDirectionsRequest API. This api gives direction between two locations. But the problem is that the route returned by this api do not consider the direction (course) in which the vehicle is moving. Is there any way to get route between user current location to the destination in iOS which also takes the course of the moving vehicle into consideration. Currently I am using the following function:
-(void)displayDirections{
CLLocation *currentLocation = [[CLLocation alloc] initWithLatitude:currentLocationLatitude longitude:currentLocationLongitude];
MKPlacemark *placemarker0 = [[MKPlacemark alloc]initWithCoordinate:currentLocation.coordinate];
MKMapItem *myMapItem0 = [[MKMapItem alloc] initWithPlacemark:placemarker0];
CLLocation *location = [[CLLocation alloc] initWithLatitude:destinationLocationLatitude longitude:destinationLocationLongitude]
MKPlacemark *placemarker1 = [[MKPlacemark alloc]initWithCoordinate:location.coordinate];
MKMapItem *myMapItem1 = [[MKMapItem alloc] initWithPlacemark:placemarker1];
MKDirectionsRequest *request = [[MKDirectionsRequest alloc] init];
[request setSource:myMapItem0];
[request setDestination:myMapItem1];
[request setTransportType:MKDirectionsTransportTypeAny];
[request setRequestsAlternateRoutes:YES]; // Gives you several route options.
MKDirections *directions = [[MKDirections alloc] initWithRequest:request];
__weak COMSiteDetailViewController *weakSelf = self;
[directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse *response, NSError *error) {
if (!error) {
[weakSelf.mkMapView removeOverlays:weakSelf.mkMapView.overlays];
NSArray *routes = [response routes];
NSArray *sortedArray;
sortedArray = [routes sortedArrayUsingComparator:^NSComparisonResult(MKRoute *a, MKRoute *b) {
CLLocationDistance first = a.distance;
CLLocationDistance second = b.distance;
return first < second;
}];
int count = 1;
for (MKRoute *route in sortedArray) {
if(count < sortedArray.count){
route.polyline.title = #"longer";
}else{
route.polyline.title = #"shortest";
}
count++;
[self.mkMapView addOverlay:[route polyline] level:MKOverlayLevelAboveRoads];
}
}
}];
}
Thanks in advance!
After searching the web I have found that neither Google API or Apple MapKit Directions API consider the heading or movement of the vehicle while giving the route information. It just consider the coordinates of the start/end locations and give the shortest route(s).
So there is no way, at the moment, to display route considering vehicle heading using Apple/Google Apis.
Although, there are other paid Apis such as https://developer.here.com which I used in my app. This api is very reliable and the pricing too is very reasonable.
Thanks!

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) {
}];
}

Unable to update NSManagedObject using Magical record

I am trying to update NSManagedObjects using magical record by fetching all objects and then using for loop changing every object and after saving context no change is there
NSArray * locationArray = [[NSArray alloc]initWithArray:[Locations findAllInContext:[NSManagedObjectContext contextForCurrentThread]]];
for(Locations * currentLocation in locationArray)
{
currentLocation.name = #"Hello world";
}
[[NSManagedObjectContext contextForCurrentThread]saveToPersistentStoreAndWait];
You can simply place you code in to the MagicalRecord's saving block
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
// This block runs in background thread
NSArray * locationArray = [[NSArray alloc]initWithArray:[Locations findAllInContext:[NSManagedObjectContext contextForCurrentThread]]];
for(Locations * currentLocation in locationArray){
currentLocation.name = #"Hello world";
}
} completion:^(BOOL success, NSError *error) {
// This block runs in main thread
}];

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

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];

NSMutable Array Property becomes null after function is exited

I am trying take the values of an array of CLLocation's made from a JSON request and assign them to a property that is a NSMutable array.
Here the relevant controller code:
- (void)viewDidLoad
{
[super viewDidLoad];
//using a background thread to receive the json call so that the UI doesn't stall
dispatch_async(directionQueue, ^{
NSData* data = [NSData dataWithContentsOfURL:openMapURL];
[self performSelectorOnMainThread:#selector(fetchedData:)
withObject:data waitUntilDone:YES];
});
NSLog(#"%#", self.coordinates);
}
- (void)fetchedData:(NSData *)responseData
{
//parse out the json data
NSError* error;
NSDictionary* json = [NSJSONSerialization
JSONObjectWithData:responseData //turn the data from the json request into a gigantic dictionary
options:0
error:&error];
NSDictionary* route = [json objectForKey:#"route"]; //created a dictionary out of the contents of route
NSDictionary* shape = [route objectForKey:#"shape"]; //create a sub-dictionary with the contents of shape
NSArray* shapePoints = [shape objectForKey:#"shapePoints"]; //turn shapePoints object into an NSArray
//Loop to turn the array of coordinate strings into CLLocation array
NSMutableArray* locations = [[NSMutableArray alloc] init];
NSUInteger i;
for (i = 0; i < ([shapePoints count])/2 ; i = i+2)
{
[locations addObject: [[CLLocation alloc]
initWithLatitude:[[shapePoints objectAtIndex:i] floatValue]
longitude:[[shapePoints objectAtIndex:i+1] floatValue]
]];
}
//When I NSLog within the function, the array has the correct data. But in viewDidLoad
//the array is null
[self.coordinates initWithArray:locations copyItems:YES];
}
How come this array become's null in viewDidLoad?
You are using GCD to make asynchronous request to fetch the data from viewDidLoad method. As it is asynchronous, it will not block the viewDidLoad method. The array gets populated once the async request downloads and gets parsed. That is the reason your array is nil in viewDidLoad.
If your UI looks blank until the data downloads and populates the array, you can choose to show an activity indicator. This will give the app user an idea that some activity is going on.
Hope that helps!
Probably what you need to do is wait for the process to get completed and the block literally doesn't know what your doing inside it or i would say the inside function is not been captured by the block.so use the array(call the function which is using your array) only after block gets completed.
- (void)fetchedData:(NSData *)responseData
{
//parse out the json data
NSError* error;
NSDictionary* json = [NSJSONSerialization
JSONObjectWithData:responseData //turn the data from the json request into a gigantic dictionary
options:0
error:&error];
NSDictionary* route = [json objectForKey:#"route"]; //created a dictionary out of the contents of route
NSDictionary* shape = [route objectForKey:#"shape"]; //create a sub-dictionary with the contents of shape
NSArray* shapePoints = [shape objectForKey:#"shapePoints"]; //turn shapePoints object into an NSArray
//Loop to turn the array of coordinate strings into CLLocation array
NSMutableArray* locations = [[NSMutableArray alloc] init];
NSUInteger i;
for (i = 0; i < ([shapePoints count])/2 ; i = i+2)
{
[locations addObject: [[CLLocation alloc]
initWithLatitude:[[shapePoints objectAtIndex:i] floatValue]
longitude:[[shapePoints objectAtIndex:i+1] floatValue]
]];
}
//When I NSLog within the function, the array has the correct data. But in viewDidLoad
//the array is null
[self.coordinates initWithArray:locations copyItems:YES];
//Here you call the methods which is accessing your self.coordinates array.
NSLog(#"%#", self.coordinates);
}

Resources