Subclasses of MKAnnotationView - objective-c

I was wondering if anyone knows of any subclasses for the MKAnnotationView class. On apples documentation they say one example is the MKPinAnnotationView so I was wondering if there were other pre-created subclasses like the one used to track the devices current location. If anyone has tips for creating my own subclass of the MKAnnotationView class that be great as well.
Thanks,
bubster

In case anyone is still interested in this:
You can get all the subclasses of a class using the Objective-C runtime functions, as described here: http://cocoawithlove.com/2010/01/getting-subclasses-of-objective-c-class.html
Other classes that inherit from MKAnnotationView are:
MKTransitCalloutView, MKAdAnnotationView, MKUserLocationView, MKUserLocationBreadCrumbView, and MKPinAnnotationView
where MKPinAnnotationView is the only one that is documented. All others are private classes that Apple uses internally.

I don't know of any other templates, but that does not mean that they don't exist. :)
Anyway, here's how to create custom ones:
Create a new class conforming to the MKAnnotation protocol. You will need to have two instance variables of type NSString* named title and subtitle and one of type CLLocationCoordinate2D named coordinate and an appropriate setter method (e.g. property). Those strings are going to be displayed in the callout. In the delegate of your mapView, implement the method -mapView:viewForAnnotation: in a similar way as you would implement the datasource for a UITableView. That is, dequeueing an annotationView by an identifier, setting the new properties (e.g. a button of type UIButtonTypeDetailDisclosure for the right accessory view). You will want to add an image to display underneath the offset. Just use the image property of your MKAnnotationView. The center of your custom image will be placed at the coordinate specified, so you may want to give an offset: aView.centerOffset = CGPointMake(0, -20)
Here is some sample code:
- (MKAnnotationView *) mapView: (MKMapView *) mapView viewForAnnotation: (id<MKAnnotation>) annotation {
// reuse a view, if one exists
MKAnnotationView *aView = [mapView dequeueReusableAnnotationViewWithIdentifier:#"pinView"];
// create a new view else
if (!aView) {
aView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"pinView"];
}
// now configure the view
aView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
[(UIButton*)aView.rightCalloutAccessoryView addTarget:self action:#selector(showDetails:) forControlEvents:UIControlEventTouchUpInside];
aView.canShowCallout = YES;
aView.enabled = YES;
aView.image = [UIImage imageNamed:#"green_pin.png"];
aView.centerOffset = CGPointMake(0, -20);
return aView;
}

Related

How can I get the frame of a UIBarButtonItem?

I'm trying to get the frame of a UIBarButtonItem, which just inherits from UIBarItem/NSObject.
In iOS 5.0, 5.1, and 6.0, use the following method. This method can be easily modified for use with a UIToolBar as well.
- (UIControl *) findBarButtonItem:(UIBarButtonItem *)barButtonItem
{
UINavigationBar *toolbar = self.navigationController.navigationBar;
UIControl *button = nil;
for (UIView *subview in toolbar.subviews) {
if ([subview isKindOfClass:[UIControl class]]) {
for (id target in [(UIControl *)subview allTargets]) {
if (target == barButtonItem) {
button = (UIControl *)subview;
break;
}
}
if (button != nil) break;
}
}
return button;
}
Usage:
UIControl *barButton = [self findBarButtonItem:myBarButtonItem];
CGRect barButtonFrame = barButton.frame;
This way works best for me:
UIView *targetView = (UIView *)[yourBarButton performSelector:#selector(view)];
CGRect rect = targetView.frame;
I know you posted this question for the purpose of posting the answer you found. I thought I would add an alternate solution that doesn't have the risk of breaking in a future iOS update.
If you create your UIBarButtonItem using a custom view then you can access the customView property of the UIBarButtonItem. The frame of the customView will reflect its position in the toolbar or navbar.
Obviously this solution prevents you from using the standard system defined buttons. But you can easily replicate them with your own image.
Generally the custom view you would use would be a UIButton with an appropriate icon image. One trick is to ensure you enable the button's showsTouchWhenHighlighted property so you get the usual highlight effect.
Setup the UIButton with the same target/action you would use on the UIBarButtonItem.
Here's a way to do this without having to resort to undocumented behaviour or assumptions.
Instead of using a UIBarButtonSystemItem, use a UIBarButtonItem with a custom view. You can get the image from the UIBarButtonSystemItem using a technique like this one:
Use UIBarButtonItem icon in UIButton

Change MKAnnotationView programmatically

Is there a way to change MKAnnotationView style (like from red label with number to green colored label with number).
I want to change this style according to distance from target. My annotation is moving, with user.
I dont want to use remove / add annotation, because it causes "blinking".
Can it be done someway?
UPDATE:
I am adding code, how I am doing it right now
MKAnnotationView *av = [mapView viewForAnnotation:an];
if ([data->type isMemberOfClass:[UserAnnotationImage class]])
{
UIImage *img = [UIImage imageNamed: ((UserAnnotationImage *)data->type)->url];
[av setImage:img];
}
else if ([data->type isMemberOfClass:[UserAnnotationLabel class]])
{
UIView * v = [av viewWithTag:0];
v = ((UserAnnotationLabel *)data->type)->lbl;
av.frame = ((UserAnnotationLabel *)data->type)->lbl.frame;
}
else if ([data->type isMemberOfClass:[UserAnnotationView class]])
{
UIView * v = [av viewWithTag:0];
v = ((UserAnnotationView *)data->type)->view;
av.frame = ((UserAnnotationView *)data->type)->view.frame;
}
Sadly, its not working :(
Yes, basically you get a reference to the annotation view and update its contents directly.
Another way, if you have a custom annotation view class, is to have the annotation view monitor the changes it is interested in (or have something outside tell it) and update itself.
The first approach is simpler if you are using a plain MKAnnotationView or MKPinAnnotationView.
Wherever you detect that a change to the view is needed, get a reference to the view by calling the map view's viewForAnnotation instance method. This is not the same as calling the viewForAnnotation delegate method.
Once you have a reference to the view, you can modify as needed and the changes should appear immediately.
An important point is that the logic you use to update the view outside the delegate method and the logic you have in the viewForAnnotation delegate method must match. This is because the delegate method may get called later (after you've updated the view manually) by the map view and when it does, the code there should take the updated data into account.
The best way to do that is to have the annotation view construction code in a common method called both by the delegate method and where you update the view manually.
See change UIImage from MKAnnotation in the MKMapView for an example that updates just the annotation view's image.
For an example (mostly an idea for an approach) of updating the view using a custom annotation view class, see iPad Mapkit - Change title of "Current Location" which updates the view's pin color periodically (green, purple, red, green, purple, red, etc).
There are too many unknowns in your code to explain why it doesn't work. For example:
What is data? Is it annotation-specific (is it related to an)? What is type? Does it change after the annotation has been added to the map?
Why is data storing entire view objects like a UILabel or UIView instead of just the underlying data that you want to show in those views?
imageNamed requires the image to be a resource in the project (not any arbitrary url)
Don't use a tag of 0 (that's the default for all views). Start numbering from 1.
You get a view using viewWithTag but then replace it immediately with another view.
I'll instead give a more detailed but simple(r) example...
Assume you have an annotation class (the one that implements MKAnnotation) with these properties (in addition to coordinate, title, and subtitle):
#property (nonatomic, assign) BOOL haveImage;
#property (nonatomic, copy) NSString *labelText;
#property (nonatomic, copy) NSString *imageName;
#property (nonatomic, assign) CLLocationDistance distanceFromTarget;
To address the "important point" mentioned above (that the viewForAnnotation delegate method and the view-update-code should use the same logic), we'll create a method that is passed an annotation view and configures it as needed based on the annotation's properties. This method will then be called both by the viewForAnnotation delegate method and the code that manually updates the view when the annotation's properties change.
In this example, I made it so that the annotation view shows the image if haveImage is YES otherwise it shows the label. Additionally, the label's background color is based on distanceFromTarget:
-(void)configureAnnotationView:(MKAnnotationView *)av
{
MyAnnotationClass *myAnn = (MyAnnotationClass *)av.annotation;
UILabel *labelView = (UILabel *)[av viewWithTag:1];
if (myAnn.haveImage)
{
//show image and remove label...
av.image = [UIImage imageNamed:myAnn.imageName];
[labelView removeFromSuperview];
}
else
{
//remove image and show label...
av.image = nil;
if (labelView == nil)
{
//create and add label...
labelView = [[[UILabel alloc]
initWithFrame:CGRectMake(0, 0, 50, 30)] autorelease];
labelView.tag = 1;
labelView.textColor = [UIColor whiteColor];
[av addSubview:labelView];
}
if (myAnn.distanceFromTarget > 100)
labelView.backgroundColor = [UIColor redColor];
else
labelView.backgroundColor = [UIColor greenColor];
labelView.text = myAnn.labelText;
}
}
The viewForAnnotation delegate method would look like this:
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MyAnnotationClass class]])
{
static NSString *myAnnId = #"myann";
MKAnnotationView *av = [mapView dequeueReusableAnnotationViewWithIdentifier:myAnnId];
if (av == nil)
{
av = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:myAnnId] autorelease];
}
else
{
av.annotation = annotation;
}
[self configureAnnotationView:av];
return av;
}
return nil;
}
Finally, the place where the annotation's properties change and where you want to update the annotation view, the code would look something like this:
ann.coordinate = someNewCoordinate;
ann.distanceFromTarget = theDistanceFromTarget;
ann.labelText = someNewText;
ann.haveImage = YES or NO;
ann.imageName = someImageName;
MKAnnotationView *av = [mapView viewForAnnotation:ann];
[self configureAnnotationView:av];

Access int variable from other class

I have two windows, my main window "window" and "help window" all inside my App Delegate. In my main window its view is subclassed and I want to draw a rect inside it. My help window has a rect also but it has an NSTracker on it. What I want to do is draw my rect in my window subclass with the x and y coordinates equal to my NSTracker position. The problem I am having is it crashes when I try to bring in the coordinates, any ideas of what I could be doing wrong? thanks
//My subclass of window is called CutoutView. This is all in draw rect
AppDelegate *controller = [[[NSApp delegate] window] contentView];
xValue = controller.mouseLoc.x;
yValue = controller.mouseLoc.y;
NSRectFillUsingOperation(NSMakeRect(xValue,yValue, 600, 400), NSCompositeClear);
[self update];
- (void)update
{
NSLog(#"test");
[self setNeedsDisplay:YES];
}
//My AppDelegate with the tracker helpView is a reference to the view of my second window "Help Window"
-(void)updateTrackingAreas
{
if(trackingArea != nil) {
[self.helpView removeTrackingArea:trackingArea];
[trackingArea release];
}
opts = (NSTrackingActiveAlways | NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved);
trackingArea = [ [NSTrackingArea alloc] initWithRect:[self.helpView bounds]
options:opts
owner:self
userInfo:nil];
[self.helpView addTrackingArea:trackingArea];
}
-(void)mouseMoved:(NSEvent *)theEvent
{
mouseLoc = [NSEvent mouseLocation];
NSLog(#"mouseMoved: %f %f", mouseLoc.x, mouseLoc.y);
}
in my CutoutView am i getting the AppController wrong because it is in a different window "helpWindow"? or does it have to do with my int values?
There are many things wrong with your code and it's obvious that you are fundamentally misunderstanding some basic concepts.
Firstly, you state that this code is in your drawRect: method;
AppDelegate *controller = [[[NSApp delegate] window] contentView];
xValue = controller.mouseLoc.x;
yValue = controller.mouseLoc.y;
NSRectFillUsingOperation(NSMakeRect(xValue,yValue, 600, 400), NSCompositeClear);
[self update];
There are several immediate flaws apparent. Firstly, why are you declaring controller to be of type AppController* when the method you are calling (-contentView) returns an NSView?
Your AppController is not a view (at least it should not be!), so you should be declaring the object as such:
NSView* mainView = [[[NSApp delegate] window] contentView];
If you are indeed using a view as a controller then this is completely wrong. See below for my note about MVC.
You don't specify where the mouseLoc property is coming from. We need to see where this is declared, because that will affect whether or not there are problems with it.
Your drawing code calls [self update], which simply tells the view to redraw itself. This will result in an infinite loop because every time the view draws it is forced to redraw. You should never call setNeedsDisplay: from inside drawRect:.
Even after making these changes, this code is very badly structured and the design is broken.
As it stands, your code violates the Model-View-Controller pattern. A views should not have knowledge of other views. You need to restructure things so that your views display properties of your controller without needing knowledge of other views. That means that you must store the mouse location in your controller (or a model object) and use some method for the view to access that information, preferably a datasource protocol or similar. In my answer to this other question I give an example of how to do that.
You need to read the Cocoa Drawing Guide. You also need to learn more core Cocoa concepts as it is clear you are misunderstanding how Cocoa is supposed to work.

Can't select an annotation?

I have a single annotation on a map view. I can select it programmaticly, but the I tap it nothing happens. Could you help me? Did anyone encounter similar problem? Here is mehod for setting up anotations:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
MKAnnotationView *aView = [mapView dequeueReusableAnnotationViewWithIdentifier:#"MapVC"];
if (!aView) {
aView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"MapVC"];
aView.canShowCallout = YES;
aView.draggable=YES;
aView.leftCalloutAccessoryView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
// could put a rightCalloutAccessoryView here
}
aView.annotation = annotation;
[(UIImageView *)aView.leftCalloutAccessoryView setImage:nil];
return aView;
}
And adding them to map view:
- (void)updateMapView
{
if (self.mapView.annotations) [self.mapView removeAnnotations:self.mapView.annotations];
if (self.annotation) [self.mapView addAnnotation:self.annotation];
}
And mehod reacting to pressing of annotations:
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)aView
{
NSLog(#"did select annotation");
}
By the way, method [self.mapView selectAnnotation:annotation] works, but doesn't put up a callout(i checked it with breakpoint). While just taping annotation doesn't(again cheked through breakpoints).
If an annotation's title is nil or blank, the callout will not show (even if everything else is set properly including canShowCallout).
When you tap on an annotation, the didSelectAnnotationView delegate method will get called and if the annotation has a non-blank title, the callout will be displayed.
Regarding your question in the comments:
...is it right I have a seperate class to wrap all my data in to, my
annotation class contains an instance of that data class?
There's nothing wrong with this.
If you want to keep map-related logic separate from the base class, that's fine and probably a good idea for a complex app where the base data class may be used for more than just annotations.
If your app is very simple and the data is only used for annotations, you could keep things very simple and combine the two but it's not a requirement.
As long as you stick to using direct references instead of trying to, for example, use array indexes or view/button tags to link back to some data object from the annotation, the "right" class implementation depends on what works for your app.
Try setting canShowCallout property of the MKAnnotationView to YES in case you didn't.

Changing an MKAnnotationView custom image?

I know you can create a custom annotation view using something like:
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
MKPinAnnotationView *annotationView = [[[CustomAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"CustomAnnotation"] autorelease];
annotationView.image = [UIImage imageNamed:#"customPin.png"];
return annotationView;
}
.. but how do I change the image in other parts of my code.. (after it has been created with the above)?
You probably don't need the answer anymore, but still, the question is unanswered. What I usually do is add a property to the annotation, telling which image should be used. It can be a BOOL, a UIImage, or pretty much whatever you like.
In viewForAnnotation, I check for that value and set the appropriate image.
Whenever I want to update the image, I change the property's value, and I remove and add the annotation :
[theMapView removeAnnotation: myAnnotation];
[theMapView addAnnotation: myAnnotation];
That way, the annotation is re-drawn.