I created the following method to add pins to my map view. I have tried calling it to get it to add the single pin but it does nothing when I call it from a different view controller?
-(void)addPins:(NSDecimalNumber*) woX ycoord:(NSDecimalNumber*) woY {
CLLocationCoordinate2D coordinate;
coordinate.latitude = 35;
coordinate.longitude = -80;
MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
[annotation setCoordinate:coordinate];
[annotation setTitle:#"Title"];
[self.mapView addAnnotation:annotation];
}
I can call it from viewDidLoad of the DetailViewController and it will display the single pin:
[self addPins:woX ycoord:woY];
But when I call it from the MasterViewController nothing displays, but will log the comments so I know that it was run?
DetailViewController *firstController = [[DetailViewController alloc] init];
[firstController addPins:woX ycoord:woY];
I think this happens because the map object doesn't loaded yet in memory when you calling [firstController addPins:woX ycoord:woY]; can you try to call it after delay like this way [firstController performSelectorWithObjects:#[woX, woY] afterDelay:1.0f]
Good luck
Anna was on the right path. I was creating a new instance of the DetailViewController.
I was able to solve it by adding
#property (strong, nonatomic) DetailViewController *detailViewController;
to the MasterViewController.h. And then just calling
[self.detailViewController addPins:woX ycoord:woY desc:cellView];
in MasterViewController.m
Related
In my app I'm downloading the location data and showing them on the map. There are some scenarios which I cannot understand why they are happening. Sometimes, my mapView(Google map) class dealloc is getting called and I'm unable to see the markers on my map(I can still see my current location).
This is how my mapView is setUp in the storyBoard:
[ContainerVc] -embedSegue->[MapView]
Container Vc is the root Vc.
and in my rootVc class:
#property (weak, nonatomic) MapVc *mapVc;
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if ([[segue identifier] isEqualToString: #"embeddedMapVc"]) {
self.mapVc = [segue destinationViewController];
self.mapVc.delegate = self;
}
}
and in my mapVC class(all the properties are strong,nonatomic):
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self setupMapView];
}
- (void)setupMapView{
MyLog(#"Map view just refreshed/setup");
//set map view
self.infoWindow = [[MyMapInfoWindow alloc] initWithFrame:CGRectMake(0, 0, 210, 47)];
self.infoWindow.delegate = self;
CLLocation *center = [[MyLocationMonitor sharedInstance] getLocation];
GMSCameraPosition *camera = [GMSCameraPosition cameraWithLatitude:center.coordinate.latitude longitude:center.coordinate.longitude zoom:18];
self.mapView = [GMSMapView mapWithFrame:CGRectZero camera:camera];
self.mapView.camera=camera;
[self.mapView setMapType:kGMSTypeNormal];
[self.mapView setMinZoom:14 maxZoom:18];
[self.mapView setDelegate:self];
self.mapView.myLocationEnabled=YES;
self.mapView.settings.rotateGestures = NO;
self.mapView.settings.myLocationButton = YES;
[self.mapView setPadding:UIEdgeInsetsMake(0, 0, 62, 0)];
self.view = self.mapView;
[self setupMarkers];
}
- (void)setupMarkers{
MyLog(#"setting up markers");
self.annotations = nil;
[self.mapView clear];
[[MyLocationMonitor sharedInstance] getBuildingsWithCompletion:^(BOOL success, NSArray *buildings) {
self.buildings = buildings;
MyLog(#"_buildings_: %#", self.buildings);
if (success) {
GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] init];
self.annotations = [[NSMutableArray alloc] init];
for (NSDictionary *location in buildings) {
CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake([[location objectForKey:#"Latitude"] doubleValue], [[location objectForKey:#"Longitude"] doubleValue]);
bounds = [bounds includingCoordinate:coordinate];
GMSMarker *marker = [GMSMarker markerWithPosition:coordinate];
marker.title = [location objectForKey:#"name"];
marker.map = self.mapView;
marker.icon = [UIImage imageNamed:#"Boost-BuildingMarkerIcon"];
marker.userData = #{#"building":location};
marker.infoWindowAnchor = CGPointMake(1.0f, -0.1f);
marker.opacity = 1.0;
[self.annotations addObject:marker];
}
[self.mapView animateWithCameraUpdate:[GMSCameraUpdate fitBounds:bounds withPadding:50.0]];
}
}];
}
-(void)dealloc{
MyLog(#"MApVC dealloc called");
}
So, from the rootVc I move to another controller and after I finish a network call I comeback to rootVc, where the prepareForSegue gets triggered and the call goes to mapVc class, and it setups the mapView(In this process mapVc dealloc gets called sometimes in that case, I'm unable to see the markers on the map).
I couldn't get much info from stacktrace. I would like to know why my mapVc is getting deallocated sometimes ? How to make sure my mapVc is not deallocated at any time ?
Edit:
I logged the mapVc self in the dealloc case:
<MapVc: 0x7ff83f2883c0> On didAppear
<MapVc: 0x7ff83f2883c0> on dealloc (why is this happening ?)
<MapVc: 0x7ff84475b6f0> new instance again on viewDidAppear
How to make sure my mapVc is not deallocated at any time ?
If you require a VC to exist at a time when it is not onscreen (or at least in a navigation stack), then you have broken MVC. I don't think that's quite what's happening here, but it's important to keep in mind. Network operations should happen in your Model layer, and your view layer should observe the model. View controllers are responsible for managing views that are currently visible, not network operations. You likely have organized your view controllers incorrectly.
That said, that probably isn't the actual problem in this case. The problem is that nothing is retaining mapVc. It's weak in the root view controller so it's probably being released as soon as the segue is complete (the segue retains it while it is running). If mapVc is embedded in the root view controller, it should almost certainly be strong.
I am trying to transfer a users location from a screen where I allow the user to take a picture to another view where I have a map. The purpose of this is to tag the image location on the map and have that be related with the image. The current code I have for launching the ability to take an image is as follows:
- (IBAction)takePhoto:(UIButton *)sender {
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.delegate = self;
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
[self presentViewController:picker animated:YES completion: NULL];
}
-(void) imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
// Pin location as annotation on map
NSString *dateString = [NSDateFormatter localizedStringFromDate:[NSDate date]
dateStyle:NSDateFormatterShortStyle
timeStyle:NSDateFormatterFullStyle];
MKPointAnnotation *point = [[MKPointAnnotation alloc] init];
MKUserLocation *userLocation = [[MKUserLocation alloc] init];
MKMapView *mapView = [[MKMapView alloc] init];
point.coordinate = userLocation.coordinate;
point.title = #"Picture Entry";
point.subtitle = dateString;
[mapView addAnnotation:point];
// Close camera and go back to home screen
[self dismissModalViewControllerAnimated:YES];
}
I'm new to iOS programming and I'm not sure how to pass the location back to my mapView, and I believe in this code I'm just initializing a new map to save the annotation to, and thats not what I want obviously.
MapViewController.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#interface MapViewController : UIViewController <MKMapViewDelegate>
#property (strong, nonatomic) IBOutlet MKMapView *mapView;
#end
MapViewController.m
#import "MapViewController.h"
#interface MapViewController ()
#end
#implementation MapViewController
#synthesize mapView;
- (void)viewDidLoad
{
// Get user location
mapView.showsUserLocation = YES;
self.mapView.delegate = self;
}
// Update map based on user location and zoom to area
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:
(MKUserLocation *)userLocation
{
mapView.centerCoordinate = userLocation.location.coordinate;
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(userLocation.coordinate, 800, 800);
[self.mapView setRegion:[self.mapView regionThatFits:region] animated:YES];
}
#end
This is what I have for now, and I'm at a loss for how to handle this.
I believe in this code I'm just initializing a new map to save the annotation to, and thats not what I want obviously.
You're right -- you're creating a new map and adding the annotation to that object. (Also, you're not creating the map view correctly -- you should use -initWithFrame: because that's a designated initializer for views.)
You should rethink your approach. What you seem to be trying to do is to modify the map view of one view controller (your MapViewController) from a different view controller (assuming that the image picker delegate is some view controller other than MapViewController). A view controller should be in charge of its own views but shouldn't mess with the views of another view controller. Also, adding the annotation that way means that you're effectively storing the location in the map view, and you should avoid using views to store data.
Instead, think about where you should really be saving your data. The M in MVC is for model, i.e. that part of your app that manages the data. As you can see in the acronym, it should be separate from your view(s) and from your controller(s). The model is something that your various view controllers can all use to get the information they need, and having one makes your app simpler because you don't have to worry as much about sending data back and forth between view controllers. The model doesn't have to be anything terribly complicated -- for very simple apps that mainly manage a list of items, even an plain old NSArray or NSSet could serve as a data model. Give each view controller that needs it a reference to the model, or to part of the model.
With that in mind, the right way to fix your app is to have some sort of data model. The image picker delegate should update the map by adding information to the model. The map view controller should be in charge of adding annotations to the map view, and it can do that based on the information in the model.
I've a viewController (with a mapView) which is included in a tabBarController (my viewController called MapViewController is a section of TabBarController).
My TabBarController is included in a navigationController.
I created the annotationView that are added (via the delegate method MKMapViewDelegate) to the map. I allow the callout to show title and subtitle. Title and subtitle are taken from the queries made to my database. From these queries I get title, details, ID (in string version) and images.
I have no problems in setting up title and subtitle in the callout of each annotation.
But when I set the images for each callout as LeftCalloutAccessoryView, the system puts me all the same image.
And also when I go to click on the RightCalloutAccessoryView (that is a button which push to another viewController) which should open (so push) as a new window navigationController, give me back the wrong ID (ID of another annotation) and therefore are the details wrong in the new window.
I know that maybe explaining it so it is a bit difficult to understand, but instead here's the code:
+ (CGFloat)annotationPadding;
{
return 10.0f;
}
+ (CGFloat)calloutHeight;
{
return 40.0f;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[_mapView setDelegate:self];
_arrPosters = [[NSMutableArray alloc] init];
_script = [[DBGMScr alloc] init]; //Is a class that provides script database connection
//With fieldsRequest I get the array with the field results sought
_arrID = [[NSMutableArray alloc] initWithArray:[_script fieldsRequest:#"SELECT ID FROM Event ORDER BY DateTime DESC"]];
_arrEventNames = [[NSMutableArray alloc] initWithArray:[_script fieldsRequest:#"SELECT Name FROM Event ORDER BY DateTime DESC"]];
_arrEventLatitude = [[NSMutableArray alloc] initWithArray:[_script fieldsRequest:#"SELECT Latitude FROM Event ORDER BY DateTime DESC"]];
_arrEventLongitude = [[NSMutableArray alloc] initWithArray:[_script fieldsRequest:#"SELECT Longitude FROM Event ORDER BY DateTime DESC"]];
_arrEventDescription = [[NSMutableArray alloc] initWithArray:[_script fieldsRequest:#"SELECT Description FROM Event ORDER BY DateTime DESC"]];
for (int i = 0; i < [_arrID count]; i++) {
//With getPoster I get the poster for the event (located on the server) by using a script (it works perfectly), and add it to the array of posters
UIImage *poster = [_script getPoster:_arrID[i]];
[_arrPosters insertObject:poster atIndex:i];
}
for (int i = 0; i < [_arrID count]; i++) {
MKPointAnnotation *aAnnotationPoint = [[MKPointAnnotation alloc] init];
CLLocationCoordinate2D theCoordinate;
theCoordinate.latitude = [_arrEventLatitude[i] doubleValue];
theCoordinate.longitude = [_arrEventLongitude[i] doubleValue];
aAnnotationPoint.coordinate = theCoordinate;
aAnnotationPoint.title = _arrEventNames[i];
aAnnotationPoint.subtitle = _arrEventDescription[i];
// Add the annotationPoint to the map
[_mapView addAnnotation:aAnnotationPoint];
}
}
#pragma mark - MKMapViewDelegate
-(MKAnnotationView*)mapView:(MKMapView*)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
CGRect myRect = CGRectMake(-20, -20, 40, 40);
EventAnnotationView *viewAnno = [[EventAnnotationView alloc] initWithFrame:myRect];
viewAnno.canShowCallout = YES;
UIButton* rightButton = [UIButton buttonWithType:
UIButtonTypeDetailDisclosure];
viewAnno.rightCalloutAccessoryView = rightButton;
UIImageView *iconView = [[UIImageView alloc] initWithFrame:myRect];
iconView.image = defaultImage; //defaultImage for now, but I want to show a different image for each annotation
viewAnno.leftCalloutAccessoryView = iconView;
NSString *viewTitle = [viewAnno.annotation title];
return viewAnno;
}
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
EventViewerViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"EventViewer"];
[self.navigationController pushViewController:controller animated:YES];
NSDictionary *selection = #{#"ID" : _arrID[_eventIndex] //but it get an incorrect ID,
#"eventName" : _arrEventNames[_eventIndex]};
[controller setValue:selection forKey:#"selection"];
}
-(void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
//_eventIndex is an NSInteger declared in file .h
_eventIndex = [_mapView.annotations indexOfObject:view.annotation];
}
EventAnnotationView is subclass of MKAnnotationView:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// make sure the x and y of the CGRect are half it's
// width and height, so the callout shows when user clicks
// in the middle of the image
CGRect viewRect = CGRectMake(-20, -20, 40, 40);
UIImageView* imageView = [[UIImageView alloc] initWithFrame:viewRect];
// keeps the image dimensions correct
// so if you have a rectangle image, it will show up as a rectangle,
// instead of being resized into a square
imageView.contentMode = UIViewContentModeScaleAspectFit;
_imageView = imageView;
[self addSubview:imageView];
}
return self;
}
- (void)setImage:(UIImage *)image
{
// when an image is set for the annotation view,
// it actually adds the image to the image view
_imageView.image = image;
}
There are several issues here but to answer your two main questions first:
To set leftCalloutAccessoryView to a different image for each annotation, you need to look at some value in the annotation parameter that is passed to the viewForAnnotation delegate method. At the crudest level, you could set the image based on the value of annotation.title (eg. if title is "A" then set image to "1" else if title is "B" then set image to "2" etc).
Tapping on the rightCalloutAccessoryView gives you the wrong Event Id because of this code in didSelectAnnotationView:
_eventIndex = [_mapView.annotations indexOfObject:view.annotation];
The above code assumes that the annotations array that the map view returns will be in the same exact order that you added the annotations in (and that it will only contain annotations that you added which isn't the case if showsUserLocation is YES). This assumption is false. Do not assume the annotations array is in any particular order -- do not rely on its order.
A better solution is to use the annotation object itself that you can access directly from the calloutAccessoryControlTapped delegate method using view.annotation.
Since you need the Event's Id and the default MKPointAnnotation class obviously has no property to store that anywhere, you should create your own class (eg. EventAnnotation) that implements MKAnnotation and add all the event properties you need to know for each annotation. Then, you'll be able to access event-specific values directly using the annotation object in the map view's delegate methods without trying to reference back to your original arrays.
The EventAnnotation class might be declared like this:
#interface EventAnnotation : NSObject<MKAnnotation>
#property (assign) CLLocationCoordinate2D coordinate;
#property (copy) NSString *title;
#property (copy) NSString *subtitle;
#property (copy) NSString *eventId;
//Not sure if your event id is a string.
//Change to "#property (assign) int eventId;" if it's an integer.
#end
Then to create the annotation, create an EventAnnotation instead of MKPointAnnotation:
EventAnnotation *aAnnotationPoint = [[EventAnnotation alloc] init];
CLLocationCoordinate2D theCoordinate;
theCoordinate.latitude = [_arrEventLatitude[i] doubleValue];
theCoordinate.longitude = [_arrEventLongitude[i] doubleValue];
aAnnotationPoint.coordinate = theCoordinate;
aAnnotationPoint.title = _arrEventNames[i];
aAnnotationPoint.subtitle = _arrEventDescription[i];
//store event id in the annotation itself...
aAnnotationPoint.eventId = _arrID[i];
[_mapView addAnnotation:aAnnotationPoint];
Finally, in calloutAccessoryControlTapped (you won't need to implement didSelectAnnotationView), you could do this:
EventViewerViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"EventViewer"];
[self.navigationController pushViewController:controller animated:YES];
//cast the view's annotation object to EventAnnotation...
EventAnnotation *eventAnn = (EventAnnotation *)view.annotation;
NSDictionary *selection = #{#"ID" : eventAnn.eventId,
#"eventName" : eventAnn.title };
[controller setValue:selection forKey:#"selection"];
Another thing you should do in viewForAnnotation is explicitly set the view's annotation property immediately after creating it. Although it may be getting assigned automatically somehow, I'd rather do it explicitly to be safe:
EventAnnotationView *viewAnno = [[EventAnnotationView alloc] initWithFrame:myRect];
viewAnno.annotation = annotation; // <-- add this
Two unrelated points:
Instead of creating a separate array for each event property (Ids, Names, Descriptions, Latitudes, etc), I would highly recommend creating an "Event" class with all those properties and then you could create just one array of Event objects. In fact, you could make this Event class implement MKAnnotation itself (instead of creating two classes Event and EventAnnotation).
The code is executing a separate SQL query for each column. All the queries are identical except for the column retrieved. This could be improved greatly by getting all the columns in a single SQL query (eg. SELECT ID, Name, Latitude, Longitude, Description FROM Event ORDER BY DateTime DESC). I am not familiar with this DBGMScr class but you should really look into it. If the table contains 500 annotations and 5 columns, you are currently retrieving a total of 2500 rows when it could be just 500 in one shot.
Having scanned all the similar problems and tried all possible solutions, I still can't find the solution to my case. I tried to put multiple pins on my MapView but it seems that my pin can't be added normally. I started a NSTimer in my viewDidLoad method so that every 5 seconds, some updated pins will be put on Map. I added debugging information and found the problem was the that the method viewForAnnotation is not getting called.
(I've already set the delegate by calling [_map setDelegate:self])
My code is as following:
- (void)viewDidLoad
{
[super viewDidLoad];
_map = [[MKMapView alloc] initWithFrame:[[self view] frame]];
[_map setDelegate:self];
CLLocationCoordinate2D startLocation;
startLocation.latitude = [startLat floatValue];
startLocation.longitude = [startLong floatValue];
MKCoordinateSpan span = MKCoordinateSpanMake(0.002, 0.002);
MKCoordinateRegion region = MKCoordinateRegionMake(startLocation, span);
[_map setRegion:region];
[[self view] addSubview:_map];
[self getPlacesForLocation:[_map centerCoordinate]];
NSTimer *currentTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:#selector(theActionMethod:) userInfo:nil repeats:YES];
[currentTimer fire];
}
The getPlacesForLocationMethod:
-(void)getPlacesForLocation:(CLLocationCoordinate2D)location
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^
{
/* get data from the website
** get the geo information and put them in the MapPin struct
*/
[self putPinsOnMap];
}
}
putPinsOnMap:
-(void)putPinsOnMap
{
for(Pinfo *iter in [_PinfoArray copy])
{
MapPin *pin = [[MapPin alloc] init];
[pin setTitle:[iter from_user_name]];
[pin setSubtitle:[iter text]];
[pin setCoordinate:[iter location]];
//NSLog(#"The coordinate is %f %f", [pin coordinate].latitude, [pin coordinate].longitude);
[_map addAnnotation:pin];
/****************************************/
NSLog(#"Try to put the pin on Map");
/****************************************/
}
}
And here is the content of my MapPin.h:
#interface MapPin : NSObject<MKAnnotation>
#property (nonatomic, copy) NSString *title, *subtitle;
#property (nonatomic) CLLocationCoordinate2D coordinate;
#end
If I stay in place, then every time the (void)putPinsOnMap gets called, it prints "Try to put pin on Map" but the viewForAnnotation method is not getting called (I also added debugging info there but none of them are printed). Only a few times, the method viewForAnnotation sometimes will be called if I zoom out to a great extent.
You say you added debugging info to viewForAnnotation, but you haven't subclassed MKMapView to override the viewForAnnotation method (or at least, you are allocating an MKMapView, not any subclass). I think you are looking for the delegate method:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation;
and if you implement that method in your view controller (which you are making the delegate of the MKMapView) you might get useful information.
If mapView:viewForAnnotation: is still not called except when you zoom way out then you have probably placed your annotations incorrectly and they're just way out on some other part of the globe than the one you have initially on screen.
I want to show the pin annotation on the new view controller when the "map" button is clicked on the upper level view controller.
So I used the "IBAction" method on the method file of the upper level controller as below.
Then the latitude and longitude values appeared normally (in the NSLog) from the property list.
But I can't see the pin annotation on the new view controller.
However, if I put the code marked "code for viewDidLoad" on the new view controller (named "location") then I can see the pin annotation.
But the latitude and longitude value is only 0.00000.
I think the variable isn't shared between two view controller.
Please help me to solve this problem.
- (IBAction) goAddView:(id)sender {
// the code for viewDidLoad
double myLat = [[drink objectForKey:lati_KEY] doubleValue];
double myLong = [[drink objectForKey:long_KEY] doubleValue]; CLLocationCoordinate2D theCoordinate;
theCoordinate.latitude = myLat;
theCoordinate.longitude = myLong;
NSLog(#"the latitude = %f",theCoordinate.latitude);
NSLog(#"the longitude = %f",theCoordinate.longitude);
myAnnotation *myAnnotation1=[[myAnnotation alloc] init];
myAnnotation1.coordinate=theCoordinate;
myAnnotation1.title=#"Destination";
myAnnotation1.subtitle=#"in the city";
[self.mapView addAnnotation:myAnnotation1];
// the code end
location *lm= [[location alloc] initWithNibName:#"location" bundle:nil];
[self.navigationController pushViewController:lm animated:YES];
I assume the variable you want to share is drink. If you've just declared drink as an ivar in both view controllers, that won't "share" it automatically. After you alloc+init location in goAddView, drink will be nil in lm. The viewDidLoad method in location will get called when you push/present it.
One way to pass the value to location is using properties. First, declare drink as a property in the location view controller:
//in location.h:
#property (retain) NSDictionary *drink;
//in location.m:
#synthesize drink;
//and release in dealloc if not using ARC
Next, set the property after you alloc+init it in goAddView and before you call pushViewController:
- (IBAction) goAddView:(id)sender
{
location *lm = [[location alloc] initWithNibName:#"location" bundle:nil];
lm.drink = drink; //point drink in lm to the one in current vc
[self.navigationController pushViewController:lm animated:YES];
//and do [lm release]; if not using ARC
}