Basically I need a way to put annotation on all the "Walmarts" that search request picks up. I am not using interface builder, I am just using code for this app.
MKMapView * map = [[MKMapView alloc] initWithFrame:
CGRectMake(0, 0, 320, 480)];
map.delegate = self;
[self.view addSubview:map];
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
MKLocalSearchRequest *request = [[MKLocalSearchRequest alloc] init];
request.naturalLanguageQuery = #"Walmart";
request.region = map.region;
I'd suggest simply creating the mapview in viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
MKMapView * map = [[MKMapView alloc] initWithFrame:self.view.bounds];
map.delegate = self;
[self.view addSubview:map];
}
But when the user moves the map, look for Walmarts and add the annotations:
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
// if on slow network, it's useful to keep track of the previous
// search, and cancel it if it still exists
[self.previousSearch cancel];
// create new search request
MKLocalSearchRequest *request = [[MKLocalSearchRequest alloc] init];
request.naturalLanguageQuery = #"Walmart";
request.region = mapView.region;
// initiate new search
MKLocalSearch *localSearch = [[MKLocalSearch alloc] initWithRequest:request];
[localSearch startWithCompletionHandler:^(MKLocalSearchResponse *response, NSError *error) {
NSMutableArray *annotations = [NSMutableArray array];
[response.mapItems enumerateObjectsUsingBlock:^(MKMapItem *item, NSUInteger idx, BOOL *stop) {
// if we already have an annotation for this MKMapItem,
// just return because you don't have to add it again
for (id<MKAnnotation>annotation in mapView.annotations)
{
if (annotation.coordinate.latitude == item.placemark.coordinate.latitude &&
annotation.coordinate.longitude == item.placemark.coordinate.longitude)
{
return;
}
}
// otherwise, add it to our list of new annotations
// ideally, I'd suggest a custom annotation or MKPinAnnotation, but I want to keep this example simple
[annotations addObject:item.placemark];
}];
[mapView addAnnotations:annotations];
}];
// update our "previous" search, so we can cancel it if necessary
self.previousSearch = localSearch;
}
Clearly, this code sample assumes that you have a weak property for the previous search operation. (This is not, strictly speaking, necessary, but may give you better performance if browsing around on a map when you have a slow Internet connection.) Anyway, that property would be defined as follows:
#property (nonatomic, weak) MKLocalSearch *previousSearch;
There are other possible refinements (e.g. UIActivityIndicatorView or network activity indicator if search is in progress, perhaps remove annotations that are not within the map's current region, etc.), but hopefully you get the idea.
Here is the approach:
MKLocalSearch *localSearch = [[MKLocalSearch alloc] initWithRequest:request];
[self.localSearch startWithCompletionHandler:^(MKLocalSearchResponse *response, NSError *error) {
if(error)
{
NSLog(#"localSearch startWithCompletionHandlerFailed! Error: %#", error);
return;
}
else
{
for(MKMapItem *mapItem in response.mapItems)
{
MKPinAnnotation *annotation = [[MKPinAnnotation alloc] initWithCoordinate: mapItem.placemark.location.coordinate];
[map addAnnotation:annotation];
NSLog(#"Name for result: = %#", mapItem.name);
}
}}];
Related
very very new to iOS programming, so I appreciate any help with this.
All I actually want to do is pass the variable of my current coordinates to a different view when a button is pressed. I can't work out how to do this using my current work - I'm getting in a total muddle. My project is essentially made from lots of chunks of code from various sources. I'll share with you the parts that I think are relevant, and hopefully somebody can at least point me in the right direction!
This is from my MainViewController.m:
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
if (currentPosition == nil) {
MKMapPoint point = MKMapPointForCoordinate(newLocation.coordinate);
double pointsPerMeter = MKMapPointsPerMeterAtLatitude(newLocation.coordinate.latitude);
double visibleDistance = pointsPerMeter * 500.0;
MKMapRect rect = MKMapRectMake(
point.x - visibleDistance, point.y - visibleDistance,
visibleDistance * 2, visibleDistance * 2);
[self.mapView setVisibleMapRect:rect animated:YES];
NSURL *url = [NSURL URLWithString:#"https://url.url/json.json"];
NSData *data = [NSData dataWithContentsOfURL:url];
NSError *error;
NSArray *array = [NSJSONSerialization JSONObjectWithData:data
options:0
error:&error];
if (error != nil)
{
}
CLLocationCoordinate2D location;
NSMutableArray *newAnnotations = [NSMutableArray array];
MKPointAnnotation *newAnnotation;
for (NSDictionary *dictionary in array)
{
location.latitude = [dictionary[#"lat"] doubleValue];
location.longitude = [dictionary[#"lon"] doubleValue];
newAnnotation = [[MKPointAnnotation alloc] init];
newAnnotation.coordinate = location;
HUWMapAnnotation *annotation = [[HUWMapAnnotation alloc] initWithCoordinate:newAnnotation.coordinate];
annotation.messagetitle = dictionary[#"name"];
annotation.email = dictionary[#"message"];
annotation.username = dictionary[#"user"];
[self.mapView addAnnotation:annotation];
[newAnnotations addObject:newAnnotation];
}
}
currentPosition = newLocation;
}
- (void)viewDidLoad { [super viewDidLoad];
// Check if the user has enabled location services.
if ([CLLocationManager locationServicesEnabled]) {
// Create a location manager.
locationManager = [[CLLocationManager alloc] init];
// Set ourselves as it's delegate so that we get notified of position updates.
locationManager.delegate = self;
// Set the desired accuracy.
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
// Start tracking.
[locationManager startUpdatingLocation];
}
}
Like I said, I've trimmed this down substantially to the bits I think are relevant. Let me know if I have missed something essential. I've got my storyboard set up with a button ready to go, and a segue with its identifier.
I assume that I should be using prepareForSegue - but my issue is that I quite simply don't know how to get my coordinates into that situation.
I hope somebody will be able to help me (and I apologise for the large amount of copy-pasted code!)
To pass objects between view controllers using segues, do something like the following:
1) create a segue between the source and destination view controllers in IB, give it an identifier #"MySegue".
2) let's say the destination vc needs a string to run:
// DestinationVC.h
#interface DestinationVC : UIViewController
#property(strong, nonatomic) NSString *string;
#end
3) the source vc initiates a segue:
[self performSegueWithIdentifier:#"MySegue" sender:self];
4) the source vc prepares by initializing the property on the destination vc:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"MySegue"]) {
DestinationVC *vc = [segue destinationViewController];
vc.string = // some object that the source vc has. this could be your CLLocationCoordinate2D
}
}
My app is real-time tracker, where multiple users are logged in and updating their location by sending their co-ordinates to our web service which is then called back let's after every 2 minutes to show all the users on my MapView.
Every time I get the locations of users from web service in connectionDidFinishLoading method, I am parsing, creating polyline through pointsArray and adding them to overlay:
-(void) connectionDidFinishLoading: (NSURLConnection *) connection
{
userLatitudeArray = [[NSMutableArray alloc]init];
userLongitudeArray = [[NSMutableArray alloc]init];
userIdArray = [[NSMutableArray alloc]init];
userNameArray = [[NSMutableArray alloc]init];
userProfilePicArray = [[NSMutableArray alloc]init];
profilePicURLStringArray = [[NSMutableArray alloc]init];
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
NSArray *trackingDict = [NSJSONSerialization JSONObjectWithData:empJsonData options:kNilOptions error:nil];
if ([trackingDict count] >= 2) {
for (trackUsersCount = 0; trackUsersCount< trackingDict.count; trackUsersCount++) {
NSLog(#"trackUsersCount %i", trackUsersCount);
NSMutableArray *latlongArray = [[NSMutableArray alloc]init];
latlongArray = [[trackingDict objectAtIndex:trackUsersCount]objectForKey:#"latlong"];
[userLongitudeArray removeAllObjects];
[userLatitudeArray removeAllObjects];
for (int i = 0; i<latlongArray.count; i++) {
[userLatitudeArray addObject:[[latlongArray objectAtIndex:i]objectForKey:#"lat"]];
[userLongitudeArray addObject:[[latlongArray objectAtIndex:i]objectForKey:#"long"]];
}
NSString *name = [[trackingDict objectAtIndex:trackUsersCount]objectForKey:#"user_firstName"];
// ProfilePIC URL
profilePicURLString = [[trackingDict objectAtIndex:trackUsersCount]objectForKey:#"user_profilePicture"];
[userNameArray addObject:name];
[profilePicURLStringArray addObject:profilePicURLString];
int i;
if (userLatitudeArray.count>1) {
for (i = 0; i<userLatitudeArray.count; i++) {
CLLocationCoordinate2D userLocation;
userLocation.latitude = [[userLatitudeArray objectAtIndex:i]doubleValue];
userLocation.longitude = [[userLongitudeArray objectAtIndex:i] doubleValue];
MKMapPoint * pointsArray = malloc(sizeof(CLLocationCoordinate2D)*userLongitudeArray.count);
pointsArray[i] = MKMapPointForCoordinate(userLocation);
polyline = [MKPolyline polylineWithPoints:pointsArray count:i];
free(pointsArray);
}
polyline.title = name;
[mapView addOverlay:polyline];
}
}
}
}
What I want to do is to have control on each polyline created for each user, so I can change the color of it and hide/show them on click of a button (one to show/hide my track and the other for all other users), this is why I am adding title to it.
I can see now that I am adding polyline to the same overlay, which is wrong I believe. But I don't know how many users will be there in web-service so can add multiple overlays of them.
Initially I thought I am able to remove a particular polyline with title but then I realised it is removing all as polyline.title property gets updated.
Any help would be much appreciated!
You could collect an array of those tracks that relate to other users, and keep a single track for the current user. If you clean the array at the start of the connectionDidFinishLoading function and populate it where you are currently adding the overlays to the map, then you move the addOverlay to a new function that you call at the end.
- (void) resetMap
{
if (showOtherTracks)
{
[mapView addOverlays:otherUserTracks];
} else {
[mapView removeOverlays:otherUserTracks];
}
if (showMyTrack)
{
[mapView addOverlay:myTrack];
} else {
[mapView removeOverlay:myTrack];
}
}
You can also call this when ever the button is pressed and the state changes.
I'm confused on how this works. I'm creating a CLGeocoder to drop a pin based on a string value. I have this:
- (void)placeMarkFromString:(NSString *)address {
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
[geocoder geocodeAddressString:address completionHandler:^(NSArray *placemarks, NSError *error) {
[placemarks enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(#"%#", [obj description]);
}];
// Check for returned placemarks
if (placemarks && [placemarks count] > 0) {
CLPlacemark *topResult = [placemarks objectAtIndex:0];
// Create an MKPlacemark and add it to the mapView
MKPlacemark *place = [[MKPlacemark alloc] initWithPlacemark:topResult];
AddressAnnotation *anAddress = [[AddressAnnotation alloc] init];
anAddress.address = place.subThoroughfare;
anAddress.street = place.thoroughfare;
anAddress.city = place.locality;
anAddress.state = place.administrativeArea;
anAddress.zip = place.postalCode;
anAddress.name = place.name;
//[self.mapView addAnnotation:place];
[self.mapView addAnnotation:anAddress];
self.currentPlacemark = place;
// Center map on that region
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(topResult.location.coordinate, 2000, 2000);
MKCoordinateRegion adjustedRegion = [_mapView regionThatFits:region];
[_mapView setRegion:adjustedRegion animated:YES];
}
else {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"No Results Found" message:#"" delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles: nil];
[alert show];
}
if (error) {
NSLog(#"Error: %#", [error localizedDescription]);
}
}];
}
So originally, I added my MKPlacemark to the map and it shows the red pin. It does not animate however. I basically want the ability to drop any of the 3 MKPinAnnotationView colors, have a callout and the title/subtitle to be the name and address of the place, similar to how google maps does it. But I was not getting any animation.
So I thought that maybe I needed to create my own object that conforms to the MKAnnotation class. So I did that, but when I try to add it to the location, I do not see its annotationView in the viewForAnnotation delegate method. That method is here:
- (MKAnnotationView *)mapView:(MKMapView *)theMapView viewForAnnotation:(id<MKAnnotation>)annotation {
static NSString *placeMarkIdentifier = #"SimplePinIdentifier";
if ([annotation isKindOfClass:[AddressAnnotation class]]) {
MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[theMapView dequeueReusableAnnotationViewWithIdentifier:placeMarkIdentifier];
if (annotationView == nil) {
annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:placeMarkIdentifier];
}
else {
annotationView.annotation = annotation;
}
annotationView.enabled = YES;
annotationView.animatesDrop = YES;
annotationView.draggable = YES;
annotationView.pinColor = MKPinAnnotationColorPurple;
annotationView.canShowCallout = YES;
// Create a button for the annotation
// UIButton *rightArrowButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
// annotationView.rightCalloutAccessoryView = rightArrowButton;
// [self performSelector:#selector(openCallout:) withObject:annotation afterDelay:0.5];
return annotationView;
}
return nil;
}
So I guess my questions are, do I need to create my own object to do this, am I on the right track, what am I doing wrong, and why is it that in one case, I am adding a MKPlacemark object, and then if I do it the way other way, I add a object, but not necessarily a subclass of MKPlacemark. Thanks!
First, the reason you're probably not seeing the annotation view when you use a custom annotation object (ie. AddressAnnotation) is that its coordinate is not being set (and so it's appearing at 0,0).
In the placeMarkFromString method, the code sets a lot of properties of the AddressAnnotation but not the coordinate and so the annotation is not appearing where expected.
If you set the coordinate, it will appear where expected and with the animation.
Regarding the other question as to why you see no animation when using an MKPlacemark:
By default, the map view will create an MKPinAnnotationView with a red pin but with animatesDrop set to NO.
So in viewForAnnotation, you have to explicitly do it for MKPlacemark just like you are for AddressAnnotation.
If all your annotations will be using MKPinAnnotationView, instead of checking for each different class of annotation you will be creating, you can flip the condition around by returning nil at the top of the method when the annotation class is MKUserLocation and then run the rest of the code without any class-checking (ie. returning MKPinAnnotationView with animatesDrop set to YES in all other cases).
I want to add a number of annotations(arround 500) on a mapview but the maximum it seems to display is 100. If I go beyond 100, viewForAnnotation delegate method is not called. However it works perfectly for annotations below 100.
here is the code: (works only when count of annotationArray array is less than 101)
_allPoints = [[NSMutableArray alloc] init];
NSString* responseFile = [[NSBundle mainBundle] pathForResource:#"Datafile" ofType:#"txt"];
NSData *sampleData = [NSData dataWithContentsOfFile:responseFile];
if (sampleData) {
NSError* error;
NSDictionary* json = [NSJSONSerialization
JSONObjectWithData:sampleData
options:kNilOptions
error:&error];
NSArray* response = [json objectForKey:#"response"];
for (NSDictionary *lineDict in response) {
if ([[lineDict objectForKey:#"data"] isKindOfClass:[NSArray class]]) {
SinglePoint *singlePoint = [[SinglePoint alloc] initWithDictionary:lineDict];
[_allPoints addObject:singlePoint];
}
else {
NSLog(#"Error");
}
}
}
_mapView = [[MKMapView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
[_mapView setDelegate:self];
[self.view addSubview:_mapView];
NSMutableArray *annotationArray = [[NSMutableArray alloc] init];
for (int i=0; i<[_allPoints count]; i++) {
SinglePoint *singlePoint = [_allPoints objectAtIndex:i];
NVPolylineAnnotation *annotation = [[NVPolylineAnnotation alloc] initWithPoint:singlePoint mapView:_mapView];
[annotationArray addObject:annotation];
}
[_mapView addAnnotations:(NSArray *)annotationArray];
CLLocationCoordinate2D centerCoord = { 28.632747, 77.219982 };
[_mapView setCenterCoordinate:centerCoord zoomLevel:12 animated:NO];
The delegate method is:
EDIT: As per comments, started reusing the view but with no luck :(
if ([annotation isKindOfClass:[NVPolylineAnnotation class]]) {
static NSString *viewIdentifier = #"annotationView";
NVPolylineAnnotationView *annotationView = (NVPolylineAnnotationView *) [mapView dequeueReusableAnnotationViewWithIdentifier:viewIdentifier];
if (annotationView == nil) {
annotationView = [[NVPolylineAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:viewIdentifier mapView:_mapView];
}
return annotationView;
}
return nil;
I could not find any restriction in the documentation or anywhere else. Could it be memory issue?
The MKMapView has not Limit for annotationViews. But it gets quite laggy and unusable above a certain number of views (+1000).
I believe, that the reason for this is that you handle the annotationView management totally wrong. You shouldn't create a unique view for every single annotation, even if you are using ARC. Your rather should reuse every unused View like the cell of a UITableView.
There are a couple of good tutorials for this like Introduction to MapKit on iOS - Ray Wenderlich.
If this won't resolve your problem, you should try to debug your annotation classes. (NVPolylineAnnotation and NVPolylineAnnotationView). Maybe there's something wrong.
Did you also checked your points array for equal coordinates?
Finally was able to track down the problem. Solved it by setting the center of mapView first and then adding annotations later. I still dont know why 100 annotations were shown earlier (and why the no 100 only). Thank you all for your suggestions and time on this.
This is the code I changed:-
_mapView = [[MKMapView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
[_mapView setDelegate:self];
[self.view addSubview:_mapView];
CLLocationCoordinate2D centerCoord = { 28.632747, 77.219982 };
[_mapView setCenterCoordinate:centerCoord zoomLevel:12 animated:NO];
NSMutableArray *annotationArray = [[NSMutableArray alloc] init];
for (int i=0; i<[_allPoints count]; i++) {
SinglePoint *singlePoint = [_allPoints objectAtIndex:i];
NVPolylineAnnotation *annotation = [[NVPolylineAnnotation alloc] initWithPoint:singlePoint mapView:_mapView];
[annotationArray addObject:annotation];
}
[_mapView addAnnotations:(NSArray *)annotationArray];
You can add as many annotations as you like. However, the views for each annotation will not be created until that annotation's coordinate property intersects with the visible portion of your map view. MapKit will not create a view just because you added an annotation to the map.
I'm having really weird situation. I create a singletone object of a class named "Profile like this:
static Profile *currentProfile;
+ (Profile *)currentProfile
{
#synchronized(self)
{
if (currentProfile == nil)
currentProfile = [[Profile alloc] init];
}
return currentProfile;
}
- (id)init
{
self = [super init];
if (self)
{
// Initialization code here.
isChecked = [[[NSUserDefaults standardUserDefaults] objectForKey:#"isChecked"] boolValue];
if (isChecked)
{
NSLog(#"isChecked block is called");
NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:#"myEncodedObjectKey"];
self = (Profile *) [NSKeyedUnarchiver unarchiveObjectWithData:data];
[self retain];
for (int i = 0; i < self.avatar.count; i++)
[self.avatar replaceObjectAtIndex:i withObject:[UIImage imageWithData:[self.avatar objectAtIndex:i]]];
}
else
{
password = #"";
pfefferID = #"";
email = #"";
avatar = [[NSMutableArray alloc] initWithObjects:
[UIImage imageNamed:#"empty_image.png"],
[UIImage imageNamed:#"empty_image.png"],
[UIImage imageNamed:#"empty_image.png"],
[UIImage imageNamed:#"empty_image.png"],
[UIImage imageNamed:#"empty_image.png"],nil];
isBoy = YES;
friends = [[NSMutableDictionary alloc] init];
rating = 0;
}
}
return self;
}
In init method i check a certain condition stored in NSUserDefaults by using BOOL variable named "isChecked". isChecked is equal to YES and everything works fine. But... i create this Profile object in AppDelegate file like this
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
users = [[NSMutableDictionary alloc] init];
usersForLeaderboardFromServer = [[NSMutableDictionary alloc] init];
listOfFriendsFromServer = [[NSMutableDictionary alloc] init];
currentProfile = [Profile currentProfile];
sessionID = 0;
if (!currentProfile.isChecked)//why????
NSLog(#"not checked");
if (currentProfile.isChecked)
{
[self getUsersForLeaderboardFromServer];
MainMenuView *mainMenu = [[[MainMenuView alloc] init] autorelease];
[self.navigationController pushViewController:mainMenu animated:YES];
}
}
So the same isChecked variable which a moment (far less than a moment actually) ago was equal to YES gets equal to NO when being used in application didFinishLaunchingWithOptions method by accessing it via dot. What's going on? I'm able to handle it but i'm just curious about this situation. Do you know what's wrong with it?
You're reassigning self in init, so you're returning the new object rather than the one you set isChecked on. See this code:
self = (Profile *) [NSKeyedUnarchiver unarchiveObjectWithData:data];
[self retain];
It's slightly awkward to do things like you've got - I would certainly not recommend replacing it in the way you have. For a start, the value you set to the static currentProfile is not being updated when you reassign self so that's still the old one. And also you're not releasing the old self when you reassign.
To fix it you could do something like this:
id newSelf = (Profile *) [NSKeyedUnarchiver unarchiveObjectWithData:data];
newSelf.isChecked = isChecked;
[self release];
self = [newSelf retain];
But I don't really advocate that personally. I suggest you load in the object from your archive and then proceed to update yourself with it rather than reassigning self.