I have a view with a little mapView inside. I have a custom annotationView to mark the user position. Everything works ok as usual but I've realize that the annotation is not clipped to the map view so, when I drag the map, it goes out of bounds. Check the pictures:
The mapView has the Clip Subviews enabled. I think this is the first time I found this behavior. Maybe I've never placed a map inside a bigger view with space around. I'm targeting iOS7 by the way.
EDIT: Here's the code. Nothing out of ordinary, I think.
Here, I add the custom annotation:
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
static NSString *const kAnnotationIdentifier = #"DYNAnnotationView";
DYNAnnotationView *annotationView = (DYNAnnotationView *)
[mapView dequeueReusableAnnotationViewWithIdentifier:kAnnotationIdentifier];
if (! annotationView)
{
annotationView = [[DYNAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:kAnnotationIdentifier];
}
[annotationView setAnnotation:annotation];
return annotationView;
}
And here is the custom annotationView:
DYNAnnotation.h
#import <MapKit/MapKit.h>
#interface DYNAnnotationView : MKAnnotationView
- (id)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier;
#end
DYNAnnotation.m
#import "DYNAnnotationView.h"
#implementation DYNAnnotationView
-(id)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
if (self)
{
UIImage *image = [UIImage imageNamed:#"locationMark"];
CGRect frame = [self frame];
frame.size = [image size];
[self setFrame:frame];
[self setCenterOffset:CGPointMake(0.5f, 1.0f)];
[self setImage:image];
}
return self;
}
OK. The solution was easy after all. All I needed to do was embed the mapView inside another view and set its clip subview property to YES. Still don't understand why the map view doesn't take care of this.
Hope this help somebody.
Related
I'm adding labels just underneath my map pins to show some detail of the pin. I'm doing this by adding a UILabel as a subview of the annotationView and setting it's number of lines to 0 in order to display multiple lines on the same label. I'm having an issue where the map is initially loading the correct annotations and I can see the correct data in the labels for the map pin, but when I navigate to a different screen and then go back to the map, the map pins are all in the right places but the labels beneath them no longer show the correct data in the label, they all get mixed up. The map pins start showing data belonging to other map pins. Also if I move the map around a bit and then go back to the same pin, same thing happens.
I'm adding data to the label in the viewForAnnotation method:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation{
// Return the view marker different for editing items ie draggable undragrable.
if ([annotation isKindOfClass:[PSMapAnnotation class]] == YES) {
PSMapAnnotation *senderAnnotation = (PSMapAnnotation *)annotation;
PSMapAnnotationView *annotationView = (PSMapAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:[senderAnnotation getPinKey]];
if (annotationView == nil) {
annotationView = [[PSMapAnnotationView alloc]
initWithAnnotation:senderAnnotation
reuseIdentifier:[senderAnnotation getPinKey]];
// THIS LINE RETURNS A STRING ARRAY CONTAINING LABEL DATA
NSMutableArray *taggedAnswers = [self _getTaggedAnswersForMarkerLabel:annotationView];
annotationView.enabled = YES;
annotationView.canShowCallout = NO;
UILabel *annotationLabel = [[UILabel alloc] init];
annotationLabel.text = #"";
annotationLabel.numberOfLines = 0;
annotationLabel.font = [UIFont fontWithName:#"HelveticaNeue" size:14.0];
annotationLabel.textColor = [UIColor blackColor];
annotationLabel.textAlignment = NSTextAlignmentCenter;
// LOOP THROUGH STRING ARRAY AND ADD ALL STRINGS TO ANNOTATION LABEL
for (NSString* taggedAnswer in taggedAnswers)
{
NSString *textToShow = [NSString stringWithFormat: #"%#\n", taggedAnswer];
annotationLabel.text = [annotationLabel.text stringByAppendingString: textToShow];
}
[annotationLabel sizeToFit];
annotationLabel.center = CGPointMake(annotationView.center.x, annotationView.center.y);
annotationView.myLabel = annotationLabel;
[annotationView addSubview:annotationLabel];
[annotationView setAnnotation:annotation];
}
else {
[annotationView setAnnotation:annotation];
}
return annotationView;
All of your code for configuring the annotation view is inside the if (annotationView == nil) { ... } block. So, if it successfully reused an annotation view (your else block), you’re not setting the label.
You should move the annotationLabel.text = ... code outside of this if statement. E.g.
if (annotationView == nil) {
// instantiate annotation view and add subview, but don’t set the `text` of the label yet
} else {
annotationView.annotation = annotation;
}
annotationView.annotationLabel.text = ...
For example
// CustomAnnotationView.h
#import UIKit;
#import MapKit;
#interface CustomAnnotationView : MKPinAnnotationView
#end
And
// CustomAnnotationView.m
#import "CustomAnnotationView.h"
#interface CustomAnnotationView ()
#property (nonatomic) UILabel *label;
#end
#implementation CustomAnnotationView
- (instancetype)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
if (self) {
self.enabled = YES;
self.canShowCallout = NO;
self.label = [[UILabel alloc] init];
self.label.translatesAutoresizingMaskIntoConstraints = false;
self.label.text = #"";
self.label.numberOfLines = 0;
self.label.font = [UIFont fontWithName:#"HelveticaNeue" size:14.0];
self.label.textColor = [UIColor labelColor];
self.label.textAlignment = NSTextAlignmentCenter;
[self addSubview:self.label];
[NSLayoutConstraint activateConstraints:#[
[self.label.topAnchor constraintEqualToAnchor:self.bottomAnchor constant:8],
[self.label.centerXAnchor constraintEqualToAnchor:self.centerXAnchor constant:-self.centerOffset.x]
]];
[self updateForAnnotation:annotation];
}
return self;
}
- (void)setAnnotation:(id<MKAnnotation>)annotation {
[super setAnnotation:annotation];
[self updateForAnnotation:annotation];
}
- (void)updateForAnnotation:(id<MKAnnotation>)annotation {
self.label.text = annotation.title;
}
#end
That yields:
Clearly customize this as you see fit, but hopefully it illustrates the idea.
Note the use of constraints to make sure that the label is always centered.
Probably needless to say, here I am using the title property of the annotation, but you could check for your annotation subclass and grab whatever property you want, e.g. your textToShow. But given that there is already a property for associating a text string with an annotation, either title (and optionally subtitle, if you need it), I would use that.
You might consider moving the adding of the label into the PSMapAnnotationView init method, so the annotation view takes care of configuring its subviews. And you can override the setter of the annotation property for PSMapAnnotationView to set the label text for you.
This has the benefit of (a) it simplifies your MKMapViewDelegate code; (b) keeps subview configuration where it belongs; and (c) opens up the door for completely eliminating viewForAnnotation if only supporting iOS 11 and later. E.g. in viewDidLoad add a line that says:
[self.mapView registerClass:[CustomAnnotationView class] forAnnotationViewWithReuseIdentifier:MKMapViewDefaultAnnotationViewReuseIdentifier];
And then you can remove viewForAnnotation entirely (unless you need support for multiple annotation types).
Setting up a map view with annotation of nearby cafes.
But when I try to custom the annotations using MKMarkerAnnotationView, nothing shows on the map view.
and when I log the marker, the frame is (0 0; 0 0);
I tried pin too, still didn't work.
I set up delegate on storyboard already.
I also debugged the view controller there are marker views but it is not displaying them because the width and height are zero ?
- (void)viewDidLoad {
[super viewDidLoad];
[self.cafeMap setShowsUserLocation:YES];
[self.locationManager requestWhenInUseAuthorization];
self.cafes = [NSMutableArray array];
[self fetchData];
[self.cafeMap registerClass:[MKAnnotationView class] forAnnotationViewWithReuseIdentifier:#"marker"];
}
-(void)fetchData{
[self.networkManager fetchCafeData:^(NSArray * _Nonnull businesses) {
for (NSDictionary* cafeInfo in businesses) {
Cafe *cafe = [[Cafe alloc]initWithCafeInfo:cafeInfo];
[self.cafes addObject:cafe];
}
for (Cafe *cafe in self.cafes) {
[self.cafeMap addAnnotation:cafe];
}
[self.cafeMap showAnnotations:self.cafes animated:YES];
}];
}
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
}
MKMarkerAnnotationView *marker = [[MKMarkerAnnotationView alloc]initWithAnnotation:annotation reuseIdentifier:#"marker"];
marker = (MKMarkerAnnotationView*) [mapView dequeueReusableAnnotationViewWithIdentifier:#"marker" forAnnotation:annotation];
UIButton *button = [UIButton buttonWithType:UIButtonTypeInfoLight];
marker.rightCalloutAccessoryView = button;
[marker setEnabled:YES];
[marker setCanShowCallout:YES];
NSLog(#"MARKER:%#",marker);
return marker;
}
This is the output:
MARKER:<MKAnnotationView: 0x7f9fa3f333b0; frame = (0 0; 0 0); layer = <CALayer: 0x60000253d220>>
Note that your message says that it is a MKAnnotationView. That is because you have registered MKAnnotationView for your identifier rather than MKMarkerAnnotationView. You want:
[self.cafeMap registerClass:[MKMarkerAnnotationView class] forAnnotationViewWithReuseIdentifier:#"marker"];
As an aside, you should be able to simplify viewForAnnotation to:
MKAnnotationView *marker = [mapView dequeueReusableAnnotationViewWithIdentifier:#"marker" forAnnotation:annotation];
Personally, I’d move the configuration of the annotation view into its own subclass:
static NSString * const cafeClusteringIdentifier = #"cafe";
#interface CafeAnnotationView: MKMarkerAnnotationView
#end
#implementation CafeAnnotationView
- (instancetype)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier]) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeInfoLight];
self.rightCalloutAccessoryView = button;
self.canShowCallout = true;
self.clusteringIdentifier = cafeClusteringIdentifier;
}
return self;
}
- (void)setAnnotation:(id<MKAnnotation>)annotation {
[super setAnnotation:annotation];
self.clusteringIdentifier = cafeClusteringIdentifier;
}
#end
By doing this, I avoid bloating my view controller with code for configuring annotation views.
Note, I’m setting the clusteringIdentifier so that you enjoy that behavior of the MKMarkerAnnotationView.
And then I’d register that class for MKMapViewDefaultAnnotationViewReuseIdentifier:
[self.cafeMap registerClass:[CafeAnnotationView class] forAnnotationViewWithReuseIdentifier:MKMapViewDefaultAnnotationViewReuseIdentifier];
The benefit of using MKMapViewDefaultAnnotationViewReuseIdentifier is that you don’t have to implement viewForAnnotation at all. Delete your implementation of that method entirely. In iOS 11 and later, you only need to implement viewForAnnotation if you need to do something special like having multiple custom reuse identifiers for multiple types of annotations.
Anyway, that yields:
i have a map view in my view controller, when i make a pin on the desired place i give my own image despite of the default image , but when i run and check the map it always shows default pin in my map instead of my image that i have passed. My code is this,
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation{
MKAnnotationView *view=[self.mapView dequeueReusableAnnotationViewWithIdentifier:#"annoView"];
if (!view) {
view=[[MKAnnotationView alloc]initWithAnnotation:annotation reuseIdentifier:#"annoView"];
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
}else{
view.image=[UIImage imageNamed:#"car_marker.png"];
view.canShowCallout=YES;
}
}
return view;
}
MapPin *pin=[[MapPin alloc]init];
pin.title=loc[#"name"];
pin.subtitle=nil;
pin.coordinate=annoCordinate;
[self.mapView addAnnotation:pin];
[self.mapView setCenterCoordinate:pin.coordinate animated:YES];
This is very easy to accomplish, you just need to subclass MKAnnotationView and add a custom image view to it. A good example can be found here.
In case the link dies, I have also included the code snippet below, please do show the OP some love though!
#interface TStickerAnnotationView : MKAnnotationView
#property(nonatomic) float stickerColor;
#end
#interface TStickerAnnotationView () {
UIImageView *_imageView;
TCircleView *_circleView;
}
#end
#implementation TStickerAnnotationView
- (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(-30, -30, 60, 60);
TCircleView* circleView = [[TCircleView alloc] initWithFrame:viewRect];
_circleView = circleView;
[self addSubview:circleView];
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;
}
- (void)stickerColor:(float)color {
_circleView.green = color;
[_circleView setNeedsDisplay];
}
Currently, I am having an issue with my project in implementing a custom MKAnnotationView that has multiple custom UIImageViews. So these custom UIImageViews have a clear button on top of them to not have to add gesture recognizers.
As you can see, it would be beneficial to actually tap the MKAnnotationView subviews and have some action happen.
I implemented a protocol for the MKAnnotationView where each image subview within the MKAnnotationView makes a callback to the controller that is the owner of the MKMapView... Heres the code...
PHProfileImageView *image = [[PHProfileImageView alloc] initWithFrame:CGRectMake(newX - radius / 5.0f, newY - radius / 5.0f, width, height)];
[image setFile:[object objectForKey:kPHEventPictureKey]];
[image.layer setCornerRadius:image.frame.size.height/2];
[image.layer setBorderColor:[[UIColor whiteColor] CGColor]];
[image.layer setBorderWidth:2.0f];
[image.layer setMasksToBounds:YES];
[image.profileButton setTag:i];
[image.profileButton addTarget:self action:#selector(didTapEvent:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:image];
- (void)didTapEvent:(UIButton *)button
{
NSLog(#"%#", [self.pins objectAtIndex:button.tag]);
if (self.delegate && [self.delegate respondsToSelector:#selector(didTapEvent:)]) {
[self.delegate JSClusterAnnotationView:self didTapEvent:[self.pins objectAtIndex:button.tag]];
}
}
So as you can see, I already attempt to log the result of the tapped image but nothing :(. Is the way I'm implementing this not the way to go? Am I supposed to have CAShapeLayers or something? Not really sure at this point. Anyone got any ideas?
Edit
Im thinking that I might have to implement a custom callout view. Since a callout view actually adds buttons to its view and can respond to touch events... Not totally sure though because callouts are only shown once the annotation view is tapped. And in this case, the ACTUAL annotation view is the middle label
So I resized the mkannotationview's frame to a much larger frame and apparently all the subviews are actually not within the MKAnnotationView's bounds, so the subviews aren't actually being tapped. Now that Im thinking about this solution, it probably wasn't the best solution.
If anyone has any suggestions rather than adding subviews to a MKAnnotationView to create the view I currently have, that would be great!
For the Custom AnnotationView with Clickable Buttons, you have to create custom AnnotationView SubClass in the Project. For that create a new file.
And add these two methods to the implementation file.
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
UIView* hitView = [super hitTest:point withEvent:event];
if (hitView != nil)
{
[self.superview bringSubviewToFront:self];
}
return hitView;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
CGRect rect = self.bounds;
BOOL isInside = CGRectContainsPoint(rect, point);
if(!isInside)
{
for (UIView *view in self.subviews)
{
isInside = CGRectContainsPoint(view.frame, point);
if(isInside)
break;
}
}
return isInside;
}
Then go to the ViewController.m file again and modify the viewDidLoad method as this.
- (void)viewDidLoad {
[super viewDidLoad];
self.mapKit.delegate = self;
//Set Default location to zoom
CLLocationCoordinate2D noLocation = CLLocationCoordinate2DMake(51.900708, -2.083160); //Create the CLLocation from user cordinates
MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(noLocation, 50000, 50000); //Set zooming level
MKCoordinateRegion adjustedRegion = [self.mapKit regionThatFits:viewRegion]; //add location to map
[self.mapKit setRegion:adjustedRegion animated:YES]; // create animation zooming
// Place Annotation Point
MKPointAnnotation *annotation1 = [[MKPointAnnotation alloc] init]; //Setting Sample location Annotation
[annotation1 setCoordinate:CLLocationCoordinate2DMake(51.900708, -2.083160)]; //Add cordinates
[self.mapKit addAnnotation:annotation1];
}
Now add that custom View to the ViewController.xib.
Now create this delegate method as below.
#pragma mark : MKMapKit Delegate
-(MKAnnotationView *)mapView:(MKMapView *)mV viewForAnnotation:(id <MKAnnotation>)annotation
{
AnnotationView *pinView = nil; //create MKAnnotationView Property
static NSString *defaultPinID = #"com.invasivecode.pin"; //Get the ID to change the pin
pinView = (AnnotationView *)[self.mapKit dequeueReusableAnnotationViewWithIdentifier:defaultPinID]; //Setting custom MKAnnotationView to the ID
if ( pinView == nil )
pinView = [[AnnotationView alloc]
initWithAnnotation:annotation reuseIdentifier:defaultPinID]; // init pinView with ID
[pinView addSubview:self.customView];
addSubview:self.customView.center = CGPointMake(self.customView.bounds.size.width*0.1f, -self.customView.bounds.size.height*0.5f);
pinView.image = [UIImage imageNamed:#"Pin"]; //Set the image to pinView
return pinView;
}
I also got this answer few months ago from someone posted on Stackoverflow. I modified it to my project as I want. Hope this will do your work.
I have a map view controller (UIViewController, MKMapView), with its delegate (HCIResultMapViewController).
I wish to have following functionality in this part.
1). I wish to use my custom made NSObject , so that I can associate others details along with the basic entities like title, subtitle etc.
Hence according to my needs I coded as following
In HCIResultMapViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_houseList = [[_resultList objectForKey:#"result"] objectForKey:#"listings"];
// NSLog([_houseList description]);
int i = 0;
for (NSDictionary *house in _houseList) {
HCIAnnotationViewController *annotation = [[HCIAnnotationViewController alloc]
initwithHouse:house];
[_mapView addAnnotation:annotation];
// NSLog(#"asdjhasdjsajdhaksdjghasdasdjahsdahskvdka");
self.currIdentifier = i;
i++;
}
[_mapView setShowsUserLocation:NO];
}
The other delegate functions
-(void) mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control {
for (NSObject<MKAnnotation> *annotation in _mapView.selectedAnnotations) {
NSLog(#"hellomaster");
NSLog(annotation.title);
if ([annotation isKindOfClass:[HCIAnnotationViewController class]]) {
NSLog(#"hellomaster");
}
}
The last one
-(MKAnnotationView*) mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
NSString *identifier = #"currIdentifier";
MKPinAnnotationView *annotationView =
(MKPinAnnotationView *)[_mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (annotationView == nil) {
annotationView = [[MKPinAnnotationView alloc]
initWithAnnotation:annotation
reuseIdentifier:identifier];
} else {
annotationView.annotation = annotation;
}
annotationView.enabled = YES;
annotationView.canShowCallout = YES;
annotationView.tag = self.currIdentifier;
// Create a UIButton object to add on the
UIButton *rightButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
[rightButton setTitle:annotation.title forState:UIControlStateNormal];
[annotationView setRightCalloutAccessoryView:rightButton];
/*
UIButton *leftButton = [UIButton buttonWithType:UIButtonTypeInfoLight];
[leftButton setTitle:annotation.title forState:UIControlStateNormal];
[annotationView setLeftCalloutAccessoryView:leftButton];
*/
return annotationView;
}
But I see that the class equivalence fails. Can you tell me what I'm doing wrong?
I think what I want, in simple words, is, how can I send some data (NSDictionary*) along with a annotation such that I can retrieve it whenever I want?
Please dont tag this as repeated question or so. I have tried many questions, googling etc. but couldn't find a suitable solution for this.
Here You can also set NSMutableDictionary instand of NSString.
Create custom AnnotationView:
#import <MapKit/MapKit.h>
#interface AnnotationView : MKPlacemark
#property (nonatomic, readwrite, assign) CLLocationCoordinate2D coordinate;
#property (nonatomic, strong) NSString *title; //Here You cam set `NSMutableDictionary` instand of `NSString`
#property (nonatomic, strong) NSString *subtitle; //Here You cam set `NSMutableDictionary` instand of `NSString`
#end
And in .m file
#import "AnnotationView.h"
#implementation AnnotationView
- (id)initWithCoordinate:(CLLocationCoordinate2D)coordinate addressDictionary:(NSDictionary *)addressDictionary
{
if ((self = [super initWithCoordinate:coordinate addressDictionary:addressDictionary]))
{
self.coordinate = coordinate;
}
return self;
}
#end
// Use Annotation Add #import "AnnotationView.h" in your relevant .m file:
CLLocationCoordinate2D pCoordinate ;
pCoordinate.latitude = LatValue;
pCoordinate.longitude = LanValue;
// Create Obj Of AnnotationView class
AnnotationView *annotation = [[AnnotationView alloc] initWithCoordinate:pCoordinate addressDictionary:nil] ;
annotation.title = #"I m Here";
annotation.subtitle = #"This is Sub Tiitle";
[self.mapView addAnnotation:annotation];
I couldn't find exact way to implement custom data variables or the reason why my class equivalence fails. But I figured out a way to over come this kind of situation. This might be redundant in method, but still works like a charm.
So the idea is to use the tagging system in control button. I observed that every time I send a message to my map view to add a annotation it immediately calls my viewforannotation method. Since I'm iterating through an array, I maintained a index global pointer, with which I set tag of the control button. Later when the button is clicked, I retrieve the tag and use it to get the object I want.
Anyone else have any alternative or direct solution, please do post.