Creating a Custom UI View Class to Use as a Map Annotation

Create custom callout for an annotation

I'm guessing your BuildingViewController is your MapViewController.

You want to declare it as an MKMapViewDelegate in your header. This will give you access to several methods that will allow you to change the view of the annotation that appears and even the pin.

First, import MapKit/MapKit.h and UIKit/UIKit.h if you aren't already.

Second, declare that your BuildingViewController conforms to the MKMapViewDelegate protocol (as stated above).

Third, declare your BuildingViewController as the map view's delegate in ViewDidLoad like:

self.mapView.delegate=self; //assuming that the you have a MKMapView named MapView in your layout

Fourth, implement these protocol methods:

-(MKAnnotationView*)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation;

-(void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view;

First method allows you to set right and left call out accessories as well as modify bunch of other things (this is where you want to do you CGRectMakes).

In second method, you decide what data actually gets displayed in the annotation view.

Search the documentation for MKMapViewDelegate --> here

iOS Dev: Using a custom image for the user annotation in MapBox

override func viewDidLoad() {
super.viewDidLoad()
let mapView = MGLMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.delegate = self

mapView.userTrackingMode = .followWithHeading
mapView.showsUserHeadingIndicator = true
view.addSubview(mapView)
}

func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
// Substitute our custom view for the user location annotation. This custom view is defined below.
if annotation is MGLUserLocation && mapView.userLocation != nil {
return CustomUserLocationAnnotationView()
}
return nil
}

// Create a subclass of MGLUserLocationAnnotationView.
class CustomUserLocationAnnotationView: MGLUserLocationAnnotationView {
...
}

Take a look at this example: https://www.mapbox.com/ios-sdk/maps/examples/user-location-annotation/

There is a method called setupLayers in CustomUserLocationAnnotationView. variable dot is a CALayer, so you can add a UIImage to a CALayer. Change the code in private func setupLayers() like below:

dot = CALayer()
let myImage = UIImage(named: "star")?.cgImage
dot.contents = myImage
layer.addSublayer(dot)

Displaying custom UIView in callout of pin(annotation) in map, using MapKit

- (MKAnnotationView *)mapView:(MKMapView *)mV viewForAnnotation:(id )annotation
{
MKPinAnnotationView *pinView = nil;
static NSString *defaultPinID = @"ReusedPin";
pinView = (MKPinAnnotationView*)[mVdequeueReusableAnnotationViewWithIdentifier:defaultPinID];
if ( pinView == nil )
pinView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:defaultPinID] autorelease];
if (((PinAnnotationView*)annotation).tag == 0 )
{
pinView.pinColor = MKPinAnnotationColorPurple;
}
else {
pinView.pinColor = MKPinAnnotationColorRed;
}
pinView.canShowCallout = YES;
pinView.animatesDrop = YES;
UIImageView *pinImageView = [[UIImageView alloc] initWithFrame:CGRectMake(-5, 0, 34, 34)];
UIImage *pinImage = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"icon" ofType:@"png"]];
pinImageView.image = pinImage;
[pinImage release];
[pinView addSubview:pinImageView];
[pinImageView release];
UIButton *btn = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
btn.tag = ((PinAnnotationView*)annotation).tag;
pinView.rightCalloutAccessoryView = btn;
return pinView;
}

-(void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control {
if ( control.tag !=0) {
ShowProviderDetailVC *viewControlle = [[ShowProviderDetailVC alloc]initWithNibName:@"ShowProviderDetailVC" bundle:nil];
viewControlle.lastViewName = @"SearchView";
for (NSMutableDictionary* dict in globalLocArray) {
if ( control.tag ==[[dict valueForKey:@"ID"] intValue] )
{
viewControlle.providerInfoDict = dict;
}
}
[self.navigationController pushViewController:viewControlle animated:YES];
[viewControlle release];
}

Adding a callout/speech bubble to the User annotation of a MapView

When I implemented a custom callout for my Mapbox annotations I used a xib file to design the actual callout. I find that it gives me a lot more instant feedback than than trying to conjure the UI from code (but obviously do whatever your preference is).

Sample Image

Which gives me something like the following.

Sample Image

Using a UIImage for the background allows me to achieve any shape I choose. Here I use transparency around the white to give me the circular elements and the bottom triangle you mention in your question.

The Swift file for this UIView (your SpeechBubble) needs to conform to the MGLCalloutView protocol not MGLMapViewDelegate as you have it currently. Your ViewController is the MGLMapViewDelegate, not your custom callout. Pair the xib file and the Swift file in the usual way in Identity Inspector in IB. So would be something like this:

    import UIKit
import Mapbox

class SpeechBubble: UIView, MGLCalloutView {

// Your IBOutlets //
@IBOutlet var contentView: UIView! // The custom callout's view.

var representedObject: MGLAnnotation
var annotationPoint: CGPoint
// Required views but unused for this implementation.
lazy var leftAccessoryView = UIView()
lazy var rightAccessoryView = UIView()

weak var delegate: MGLCalloutViewDelegate?

// MARK: - init methods

required init(annotation: YourAnnotation, frame: CGRect, annotationPoint: CGPoint) {
self.representedObject = annotation
self.annotationPoint = annotationPoint

super.init(frame: frame)

commonInit()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)

commonInit()
}

private func commonInit() {
Bundle.main.loadNibNamed("SpeechBubble", owner: self, options: nil)
addSubview(contentView)
contentView.frame = self.bounds

// Do your initialisation //
}

// MARK: - MGLCalloutView methods

func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
// Present the custom callout slightly above the annotation's view. Initially invisble.
self.center = annotationPoint.applying(CGAffineTransform(translationX: 0, y: -self.frame.height - 20.0))

// I have logic here for setting the correct image and button states //
}

func dismissCallout(animated: Bool) {
removeFromSuperview()
}

Then you just seem to be missing the MGLMapViewDelegate method to actually return your SpeechBubble view when requested. It should be in your ViewController file.

    func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
// Do your annotation-specific preparation here //

// I get the correct size from my xib file.
let viewFrame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 261.0, height: 168.0))
// Get the annotation's location in the view's coordinate system.
let annotationPoint = mapView.convert(annotation.coordinate, toPointTo: nil)
let customCalloutView = SpeechBubble(annotation: YourAnnotation, frame: viewFrame, annotationPoint: annotationPoint)

return customCalloutView
}

Hopefully this will get you closer to achieving what you're trying to do. BTW this version of your question is miles ahead of the first one.

EDIT +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

It's going to be almost impossible to work this through without sight of your project so I have put together a bare bones implementation. It is based on the Mapbox example here: Mapbox Custom Callout which for some reason doesn't show how to actually supply the callout view. I've also extended it to allow for a custom annotation image. If you can get this working you should be able to move the relevant parts into your own project.

I strongly recommend that if you try to implement the stuff below that you do it in a fresh project.

The view controller.

    import Mapbox

class ViewController: UIViewController, MGLMapViewDelegate {

override func viewDidLoad() {
super.viewDidLoad()

let mapView = MGLMapView(frame: view.bounds, styleURL: MGLStyle.lightStyleURL)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.tintColor = .darkGray
view.addSubview(mapView)

// Set the map view‘s delegate property.
mapView.delegate = self

// Initialize and add the marker annotation.
let coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0)
let marker = MyAnnotation(coordinate: coordinate, title: "Bingo", subtitle: "Bongo")

// Add marker to the map.
mapView.addAnnotation(marker)
}

func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
return true
}

func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
// Instantiate and return our custom callout view.
let annotationPoint = mapView.convert(annotation.coordinate, toPointTo: nil)
let viewFrame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 250.0, height: 178.0))
return CustomCalloutView(representedObject: annotation, frame: viewFrame, annotationPoint: annotationPoint)
}

func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "myAnnotationView") {
return annotationView
} else {
let annotationView = MyAnnotationView(reuseIdentifier: "myAnnotationView", size: CGSize(width: 45, height: 45), annotation: annotation)
return annotationView
}
}

func mapView(_ mapView: MGLMapView, tapOnCalloutFor annotation: MGLAnnotation) {
// Optionally handle taps on the callout.
print("Tapped the callout for: \(annotation)")

// Hide the callout.
mapView.deselectAnnotation(annotation, animated: true)
}
}

CustomCalloutView.swift

import UIKit
import Mapbox

class CustomCalloutView: UIView, MGLCalloutView {

@IBOutlet var contentView: UIView!

weak var delegate: MGLCalloutViewDelegate?

var representedObject: MGLAnnotation
var annotationPoint: CGPoint
// Required views but unused for this implementation.
lazy var leftAccessoryView = UIView()
lazy var rightAccessoryView = UIView()

required init(representedObject: MGLAnnotation, frame: CGRect, annotationPoint: CGPoint) {
self.representedObject = representedObject
self.annotationPoint = annotationPoint

super.init(frame: frame)

commonInit()
}

required init?(coder aDecoder: NSCoder) {
let coordinate = CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)
self.representedObject = MyAnnotation(coordinate: coordinate, title: "", subtitle: "")
self.annotationPoint = CGPoint(x: 50.0, y: 50.0)

super.init(coder: aDecoder)

commonInit()
}

func commonInit() {
Bundle.main.loadNibNamed("CustomCalloutView", owner: self, options: nil)
addSubview(contentView)
}

func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
// Present the custom callout slightly above the annotation's view. Initially invisble.
self.center = annotationPoint.applying(CGAffineTransform(translationX: 0.0, y: -120.0))

view.addSubview(self)
}

func dismissCallout(animated: Bool) {
removeFromSuperview()
}

}

This is associated/identified with a xib file. It just contains a simple image shape for now. I had to (re)introduce the contentView IBOutlet as I was having trouble loading things from the Bundle and adding it to self in commonInit() made everything happy.

Sample Image

The custom annotation class.

    import UIKit
import Mapbox

// MGLAnnotation protocol reimplementation
class MyAnnotation: NSObject, MGLAnnotation {

// As a reimplementation of the MGLAnnotation protocol, we have to add mutable coordinate and (sub)title properties ourselves.
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?

// Custom properties that we will use to customize the annotation.
var image: UIImage?
var reuseIdentifier: String?

init(coordinate: CLLocationCoordinate2D, title: String?, subtitle: String?) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle

self.reuseIdentifier = "myAnnotation"
}
}

The MGLAnnotationView subclass.

    import UIKit
import Mapbox

class MyAnnotationView: MGLAnnotationView {

init(reuseIdentifier: String, size: CGSize, annotation: MGLAnnotation) {
super.init(reuseIdentifier: reuseIdentifier)

// This property prevents the annotation from changing size when the map is tilted.
scalesWithViewingDistance = false

// Begin setting up the view.
frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)

let imageView = UIImageView(frame: frame)
var image = UIImage()
if annotation is MyAnnotation {
image = UIImage(named: "frog")!
}

imageView.image = image
addSubview(imageView)
}

override init(frame: CGRect) {
super.init(frame: frame)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Sample Image

Naturally there is a lot of hard coded numbers and the requirement for an image called frog but you can change all of that and improve it as you wish. The CustomCalloutView.swift and CustomCalloutView.xib need to be linked in the usual way in the identity inspector, etc.

How to make a custom annotation using Map Kit in iOS?

There is a tutorial on this, available at:

http://www.codigator.com/tutorials/advanced-mapkit-tutorial-for-ios-custom-callout/

I'll describe an overview below.

If the viewcontroller containing your mapView conforms to the MKMapViewDelegate protocol, you can respond to the didSelectAnnotationView method. This allows us to create a custom view with whatever controls we need. Not shown in this example is the positioning of the custom view:

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
// note that we don't do this if the selected annotation is for the user location
if(![view.annotation isKindOfClass:[MKUserLocation class]]) {

UIView * myCustomView = [self createMyCustomView];
[view addSubview:myCustomView];

// you can also load a view from a xib file.
// If there are going to be a lot of annotations, I'd probably load a xib
// to a local view property during the parent view controller's viewDidLoad,
// and simply refresh the contents of that view whenever this is called.
}
}

// Build a view hierarchy programmatically.
- (UIView *) createMyCustomView {

UIView * myView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
UIImageView * myImage = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
[myImage setImage:[UIImage imageNamed:@"some_image"]];
[myView addSubview:myImage];

UIButton * thisButton = [[UIButton alloc] initWithFrame:CGRectMake(50,50, 50, 10)];
[thisButton.titleLabel setText:@"My Button"];
[thisButton addTarget:self action:@selector(handleMyButton:) forControlEvents:UIControlEventTouchUpInside];
[myView addSubview:thisButton];

// etc.

return myView;

}

You should also therefore respond to the didDeselectAnnotationView protocol:

- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view
{
for (UIView *subview in view.subviews ){
[subview removeFromSuperview];
}
}


Related Topics



Leave a reply



Submit