Change a custom MKAnnotationView when it is selected - ios

I have a MKMapView with several different types of MKAnnotationViews on it. Chiefly, I have a PricedAnnotationView, which is a fairly complicated MKAnnotationView with a UILabel and some other views. The label can have different text in it, based off of the original MKAnnotation that it is associated with.
class PricedAnnotationView: MKAnnotationView {
static let ReuseID = "pricedAnnotation"
let labelContainingView: UIView = {
let containingView = UIView()
containingView.backgroundColor = .blue
containingView.layer.cornerRadius = 3
containingView.translatesAutoresizingMaskIntoConstraints = false
return containingView
}()
let label: UILabel = {
let label = UILabel(frame: CGRect.zero)
label.textColor = .white
label.font = ViewConstants.Text.BodyFontBold
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let containingView: UIView = {
let v = UIView(frame:CGRect.zero)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
let triangle = UIView(frame: CGRect(x: 0, y: 0, width: 9, height: 9))
triangle.translatesAutoresizingMaskIntoConstraints = false
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 5.5, y: 11))
path.addLine(to: CGPoint(x: 11, y: 0))
path.close()
let triangleLayer = CAShapeLayer()
triangleLayer.path = path.cgPath
triangleLayer.fillColor = UIColor.blue.cgColor
triangle.layer.addSublayer(triangleLayer)
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
self.canShowCallout = false
self.frame = labelContainingView.frame
labelContainingView.addSubview(label)
containingView.addSubview(labelContainingView)
containingView.addSubview(triangle)
self.addSubview(containingView)
// Get the label correctly inset in the labelContainingView
label.topAnchor.constraint(equalTo: labelContainingView.topAnchor, constant: 3).isActive = true
label.leadingAnchor.constraint(equalTo: labelContainingView.leadingAnchor, constant: 6).isActive = true
label.trailingAnchor.constraint(equalTo: labelContainingView.trailingAnchor, constant: -6).isActive = true
label.bottomAnchor.constraint(equalTo: labelContainingView.bottomAnchor, constant: -3).isActive = true
// The triangle.topAnchor purposefully puts the triangle a bit under the label. In testing, while moving around
// the map, a little gap would appear between the label and the triangle. This change fixes that. The triangle
// was made two pixels bigger so that it would appear to be the same size.
triangle.topAnchor.constraint(equalTo: labelContainingView.bottomAnchor, constant: -2).isActive = true
triangle.centerXAnchor.constraint(equalTo: labelContainingView.centerXAnchor).isActive = true
triangle.widthAnchor.constraint(equalToConstant: 11).isActive = true
triangle.heightAnchor.constraint(equalToConstant: 11).isActive = true
containingView.topAnchor.constraint(equalTo: labelContainingView.topAnchor).isActive = true
containingView.leadingAnchor.constraint(equalTo: labelContainingView.leadingAnchor).isActive = true
containingView.trailingAnchor.constraint(equalTo: labelContainingView.trailingAnchor).isActive = true
containingView.bottomAnchor.constraint(equalTo: triangle.bottomAnchor).isActive = true
}
/// - Tag: DisplayConfiguration
override func prepareForDisplay() {
super.prepareForDisplay()
displayPriority = .required
guard let annotation = annotation as? MyAnnotation else { return }
if case let .priced(price, currencyCode) = annotation.status {
label.text = StringFormatter.formatCurrency(amount: price, currencyCode: currencyCode)
}
self.layoutIfNeeded() // Calculate the size from the constraints so we can know the frame.
self.frame = containingView.frame
self.centerOffset = CGPoint(x: 0, y: -(containingView.frame.height / 2)) // Center point should be where the triangle points
}
}
Every other annotation is much simpler and just uses one of two UIImage to support itself. I use two different reuse Ids with those simpler MKAnnotationView so I don't have to keep setting the image; I don't know if that is a best practice or not. Here is a sample of how I do it:
private let unpricedAnnotationClusterId = "unpriced"
private let unpricedAnnotationReuseID = "unpricedAnnotation"
func createUnpricedAnnotation(mapView: MKMapView, annotation: MKAnnotation) -> MKAnnotationView {
let annotationView: MKAnnotationView
if let dequeuedAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: unpricedAnnotationReuseID) {
annotationView = dequeuedAnnotationView
annotationView.annotation = annotation
} else {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: unpricedAnnotationReuseID)
annotationView.image = UIImage(bundleAsset: "map_pin_blue")
annotationView.centerOffset = CGPoint(x: 0, y: -(annotationView.frame.height / 2)) // Center point should be where the pin points
annotationView.clusteringIdentifier = unpricedAnnotationClusterId
}
return annotationView
}
When the user taps on an annotation in the map, I have an information window below the map populate with information about the selected item. That works fine. I would like it if the annotation view changed though so it was clear as to where the item is that is being displayed. I would be okay with just setting it to an image. However, I can't find any example of how to do that; most examples just change the image. When I try that with PricedAnnotationView the previous label does not go away, although the two simpler annotations work fine. I'm also unsure how changing the image would affect the reuse Ids that I am using. I don't see a way to manually change the reuse identifier.
My MKMapViewDelegate has these functions:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if allowSelection {
if let item : MyAnnotation = view.annotation as? MyAnnotation {
...
// display selected item in information view
...
}
view.image = UIImage(bundleAsset: "map_pin_red")
}
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? MyAnnotation else {
return nil
}
switch annotation.status {
case .notPriced:
return createUnpricedAnnotation(mapView: mapView, annotation: annotation)
case .priced(_, _):
return PricedAnnotationView(annotation: annotation, reuseIdentifier: PricedAnnotationView.ReuseID)
default:
return createErrorAnnotation(mapView: mapView, annotation: annotation)
}
}
If I could go the route of just setting the image, how do I change it back after the user has selected something else. Would I need to recreate PricedAnnotationView from scratch if it was previously one of those annotation? What would that do to the reuse Ids and the reuse queue?
When I have just changed the image, the view for PricedAnnotationView does not actually go away, and is just moved to the side as the image is shown beneath.
Ideally, I could do something to trigger a call to mapView(_:viewFor:) and have some intelligence in there to remember if the item is selected or now, but I have not found anything in my research to show how to do that. (It is not completely trivial because the isSelected property does not seem to be set on the MKAnnotationView that I would have just dequeued. No surprise there.)
Any suggestions as to how to handle this would be greatly appreciated.

I've played around with this, and here is what I've done to get around this issue:
I stopped using different reuse ids for the two types of image annotations. I created this function to manage that:
func createImageAnnotation(mapView: MKMapView, annotation: MKAnnotation, imageString: String) -> MKAnnotationView {
let annotationView: MKAnnotationView
if let dequeuedAnnotationView = mapView.dequeueReusableAnnotationView(withIdentifier: imageAnnotationReuseID) {
annotationView = dequeuedAnnotationView
annotationView.annotation = annotation
} else {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: imageAnnotationReuseID)
}
annotationView.image = UIImage(bundleAsset: imageString)
annotationView.centerOffset = CGPoint(x: 0, y: -(annotationView.frame.height / 2)) // Center point should be where the pin points
annotationView.clusteringIdentifier = imageAnnotationClusterId
return annotationView
}
I then added to the select and deselect calls so that the PricedAnnotationView is hidden and the image set when appropriate. Like this:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if allowSelection {
if let item : MyAnnotation = view.annotation as? MyAnnotation {
...
// display selected item in information view
...
view.image = UIImage(bundleAsset: selectedAnnotationImage)
if case .priced(_,_) = item.rateStatus {
(view as! PricedAnnotationView).containingView.isHidden = true
}
view.centerOffset = CGPoint(x: 0, y: -(view.frame.height / 2)) // Center point should be where the pin points
}
}
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
guard let annotation = view.annotation as? MyAnnotation else { return }
switch annotation.rateStatus {
case .notPriced:
view.image = UIImage(bundleAsset: unpricedAnnotationImage)
case .priced(_, _):
view.image = nil
if let myView = (view as? PricedAnnotationView) {
myView.containingView.isHidden = false
view.frame = myView.containingView.frame
view.centerOffset = CGPoint(x: 0, y: -(myView.containingView.frame.height / 2)) // Center point should be where the triangle points
}
default:
view.image = UIImage(bundleAsset: errorAnnotationImage)
}
}
This seems to mostly work, with the only quirk being that when the PricedAnnotationView is changed to the image, the image briefly and visibly shrinks from the old frame size to the correct frame size. However, if I do not set the frame, then the PricedAnnotationView is oddly offset.

Related

Name under custom annotation views

I have a custom annotation view, when I click on any annotation point, I can see the custom view with all information. but what I need is to see name of each industrial parks under each annotation points. now I can see only point but without names
I need to see name under points.
//MARK: MKMapViewDelegate
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation
{
return nil
}
var annotationView = self.mapView.dequeueReusableAnnotationView(withIdentifier: "Pin")
if annotationView == nil{
annotationView = AnnotationView(annotation: annotation, reuseIdentifier: "Pin")
annotationView?.canShowCallout = false
}else{
annotationView?.annotation = annotation
}
annotationView?.image = UIImage(named: "test3a")
return annotationView
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)
{
// 1
if view.annotation is MKUserLocation
{
// Don't proceed with custom callout
return
}
// 2
let starbucksAnnotation = view.annotation as! StarbucksAnnotation
let views = Bundle.main.loadNibNamed("CustomCalloutView", owner: nil, options: nil)
let calloutView = views?[0] as! CustomCalloutView
calloutView.starbucksName.text = starbucksAnnotation.name
calloutView.starbucksAddress.text = starbucksAnnotation.address
calloutView.starbucksPhone.text = starbucksAnnotation.phone
//
let button = UIButton(frame: calloutView.starbucksPhone.frame)
button.addTarget(self, action: #selector(CellViewController.callPhoneNumber(sender:)), for: .touchUpInside)
calloutView.addSubview(button)
calloutView.starbucksImage.image = starbucksAnnotation.image
// 3
calloutView.center = CGPoint(x: view.bounds.size.width / 2, y: -calloutView.bounds.size.height*0.52)
view.addSubview(calloutView)
mapView.setCenter((view.annotation?.coordinate)!, animated: true)
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
if view.isKind(of: AnnotationView.self)
{
for subview in view.subviews
{
subview.removeFromSuperview()
}
}
}
If your goal is to have a label under the annotation, just have your custom annotation add this subview (and have it observe changes to the title so that it can update the label).
For example:
class AnnotationView: MKAnnotationView {
static var image: UIImage = ...
private var titleObserver: NSObjectProtocol!
private let titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = UIColor.black.withAlphaComponent(0.25)
label.font = UIFont.preferredFont(forTextStyle: .caption1)
return label
}()
override var annotation: MKAnnotation? {
didSet { updateForNewAnnotation() }
}
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
image = Self.image
centerOffset = CGPoint(x: 0, y: -Self.image.size.height / 2)
configureTitleView()
updateForNewAnnotation()
}
func configureTitleView() {
addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
titleLabel.topAnchor.constraint(equalTo: bottomAnchor)
])
clipsToBounds = false
}
func updateForNewAnnotation() {
guard let annotation = annotation as? MKPointAnnotation else { // replace `MKPointAnnotation` with whatever class your annotations are
titleObserver = nil
titleLabel.text = nil
return
}
titleLabel.text = annotation.title
titleObserver = annotation.observe(\.title) { [weak self] annotation, _ in
self?.titleLabel.text = annotation.title
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
That yields:
Obviously, feel free to configure your label however you want, but this illustrates the basic idea of adding subview and observing changes on the annotation’s title.
As an aside, notice that I set the image inside the AnnotationView class. If you keep all configuration inside the AnnotationView class, not only is it a better separation of responsibilities, but you can then retire mapView(_:viewFor:) entirely, and replace it with a single line inside your viewDidLoad that registers the annotation view class with register(_:forAnnotationViewWithReuseIdentifier:):
mapView.register(AnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)

Glitchy map AnnotationView Behavior

I've created a custom MKAnnotationView similar to this project created by Apple. In it, I place an imageView and a label. However, I am noticing very glitchy behavior as I scroll and zoom around on the map. Even right after launching this behavior is displayed. The views can be seen briefly in the upper left-hand corner before they jump around.
Below is a link to a gif of the problem
The Problem
Below is the code for the Custom AnnotationView
Update
I switched to using a modified version of the AnnotationView apple uses with the suggestions below but I'm still experiencing the same jumpy behavior jumpy behavior
a link to the complete code
import Foundation
import UIKit
import MapKit
import SDWebImage
class AppleCustomAnnotationView: MKAnnotationView {
private let boxInset = CGFloat(10)
private let interItemSpacing = CGFloat(10)
private let maxContentWidth = CGFloat(90)
private let contentInsets = UIEdgeInsets(top: 10, left: 30, bottom: 20, right: 20)
private lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [label, imageView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .top
stackView.spacing = interItemSpacing
return stackView
}()
private lazy var label: UILabel = {
let label = UILabel(frame: .zero)
label.textColor = UIColor.white
label.lineBreakMode = .byWordWrapping
label.backgroundColor = UIColor.clear
label.numberOfLines = 2
label.font = UIFont.preferredFont(forTextStyle: .caption1)
return label
}()
private lazy var imageView: UIImageView = {
let imageView = UIImageView(image: nil)
return imageView
}()
private var imageHeightConstraint: NSLayoutConstraint?
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
backgroundColor = UIColor.clear
translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
// Anchor the top and leading edge of the stack view to let it grow to the content size.
stackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: contentInsets.left).isActive = true
stackView.topAnchor.constraint(equalTo: self.topAnchor, constant: contentInsets.top).isActive = true
// Limit how much the content is allowed to grow.
imageView.widthAnchor.constraint(lessThanOrEqualToConstant: maxContentWidth).isActive = true
label.widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
label.text = nil
}
override func prepareForDisplay() {
super.prepareForDisplay()
/*
If using the same annotation view and reuse identifier for multiple annotations, iOS will reuse this view by calling `prepareForReuse()`
so the view can be put into a known default state, and `prepareForDisplay()` right before the annotation view is displayed. This method is the view's opportunity to update itself to display content for the new annotation.
*/
if let annotation = annotation as? ImageAnnotation {
label.text = annotation.title
let placeHolder = #imageLiteral(resourceName: "DSC00042")
self.imageView.sd_setImage(with: annotation.photoURL, placeholderImage: placeHolder, options: SDWebImageOptions.refreshCached, completed: {(image,error,imageCacheType,storageReference) in
if let error = error{
print("Uh-Oh an error has occured: \(error.localizedDescription)" )
}
guard let image = image else{
return
}
if let heightConstraint = self.imageHeightConstraint {
self.imageView.removeConstraint(heightConstraint)
}
let ratio = image.size.height / image.size.width
self.imageHeightConstraint = self.imageView.heightAnchor.constraint(equalTo: self.imageView.widthAnchor, multiplier: ratio, constant: 0)
self.imageHeightConstraint?.isActive = true
})
}
// Since the image and text sizes may have changed, require the system do a layout pass to update the size of the subviews.
setNeedsLayout()
}
override func layoutSubviews() {
super.layoutSubviews()
// The stack view will not have a size until a `layoutSubviews()` pass is completed. As this view's overall size is the size
// of the stack view plus a border area, the layout system needs to know that this layout pass has invalidated this view's
// `intrinsicContentSize`.
invalidateIntrinsicContentSize()
// The annotation view's center is at the annotation's coordinate. For this annotation view, offset the center so that the
// drawn arrow point is the annotation's coordinate.
let contentSize = intrinsicContentSize
centerOffset = CGPoint(x: 0, y: -contentSize.height/2)
// Now that the view has a new size, the border needs to be redrawn at the new size.
setNeedsDisplay()
}
override var intrinsicContentSize: CGSize {
var size = stackView.bounds.size
size.width += contentInsets.left + contentInsets.right
size.height += contentInsets.top + contentInsets.bottom + 30
return size
}
override func draw(_ rect: CGRect) {
super.draw(rect)
// Used to draw the rounded background box and pointer.
UIColor.darkGray.setFill()
// let path2 = UIBezierPath(ovalIn: CGRect(x: rect.width/2, y: rect.height - 10, width: 10, height: 10))
//
// let shapeLayer2 = CAShapeLayer()
// shapeLayer2.path = path2.cgPath
// shapeLayer2.fillColor = UIColor.purple.cgColor
// shapeLayer2.strokeColor = UIColor.white.cgColor
// shapeLayer2.lineWidth = 1
//
// layer.addSublayer(shapeLayer2)
// shapeLayer2.position = CGPoint(x: -10, y: 15)
// Draw the pointed shape.
// let pointShape = UIBezierPath(ovalIn: CGRect(x: rect.width/2, y: rect.height - 10, width: 10, height: 10))
// pointShape.move(to: CGPoint(x: 14, y: 0))
// pointShape.addLine(to: CGPoint.zero)
// pointShape.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height))
// pointShape.fill()
// Draw the rounded box.
let box = CGRect(x: boxInset, y: 0, width: rect.size.width - boxInset, height: rect.size.height - 30)
let roundedRect = UIBezierPath(roundedRect: box, cornerRadius: 5)
roundedRect.lineWidth = 2
roundedRect.fill()
UIColor.purple.setFill()
let circleDot = UIBezierPath(ovalIn: CGRect(x: box.midX - 7.5, y: rect.size.height - 15, width: 15, height: 15))
circleDot.lineWidth = 2
circleDot.fill()
}
}
Delegate
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation{
return nil
}
if let annotation = annotation as? ImageAnnotation{
guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "Apple", for: annotation) as? AppleCustomAnnotationView else{
fatalError("Unexpected annotation view type")
}
annotationView.annotation = annotation
annotationView.clusteringIdentifier = MKMapViewDefaultClusterAnnotationViewReuseIdentifier
annotationView.collisionMode = .rectangle
return annotationView
}else if let cluster = annotation as? MKClusterAnnotation{
guard let view = mapView.dequeueReusableAnnotationView(withIdentifier: "ClusterAnnotationView", for: annotation) as? AppleClusterAnnotationView else{
fatalError("Wrong type for cluster annotationview")
}
return view
}
return nil
}
There is one very unusual thing you do: you override init and you are doing your main layout work there.
This makes no sense, since an MKAnnotationView can be reused to display different annotations.
You make use of this reuse feature in the delegate.
This way of implementing it is also not shown in the Appl project you are citing.
What you should do in your MKAnnotationView is:
override var annotation: MKAnnotation? {
willSet {
// do what you did in override init(...) {...} before.
// be aware that Apple sets annotation to nil if it is not used any more.
}
}
in your delegate, after dequeueing only do
annotationView.annotation = annotation
make sure you don't display anything in case of view.annotation == nil.
This makes sure the same layout code is used for the dequeue case and when you create a new MKAnnotationView
Hope this helps

Swift - Custom UIView for MKAnnotationView map pin

How can I set a custom view for MKAnnotationView? I want my map pins to look unique via a UIView. That UIView subclass could have other views in it I want to customize.
There are many examples online on how to set the annotations image, but not how to actually change that annotation:
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?
{
if !(annotation is MKPointAnnotation) {
return nil
}
let annotationIdentifier = "AnnotationIdentifier"
var annotationView = mapView.dequeueReusableAnnotationViewWithIdentifier(annotationIdentifier)
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: annotationIdentifier)
annotationView!.canShowCallout = true
}
else {
annotationView!.annotation = annotation
}
let pinImage = UIImage(named: "customPinImage")
annotationView!.image = pinImage
return annotationView
}
MKAnnotationView is a subclass of UIView that can be subclassed itself.
So you would just need to subclass MKAnnotationView.
Custom Subview
Here a simple example that shows a blue triangle. Since you mentioned that the UIView custom subclass should have other views in it I added a label that should show a number.
class CustomAnnotationView: MKAnnotationView {
private let annotationFrame = CGRect(x: 0, y: 0, width: 40, height: 40)
private let label: UILabel
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
self.label = UILabel(frame: annotationFrame.offsetBy(dx: 0, dy: -6))
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
self.frame = annotationFrame
self.label.font = UIFont.systemFont(ofSize: 24, weight: .semibold)
self.label.textColor = .white
self.label.textAlignment = .center
self.backgroundColor = .clear
self.addSubview(label)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) not implemented!")
}
public var number: UInt32 = 0 {
didSet {
self.label.text = String(number)
}
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.beginPath()
context.move(to: CGPoint(x: rect.midX, y: rect.maxY))
context.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
context.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
context.closePath()
UIColor.blue.set()
context.fillPath()
}
}
MKMapViewDelegate method
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard annotation is MKPointAnnotation else { return nil }
let customAnnotationView = self.customAnnotationView(in: mapView, for: annotation)
customAnnotationView.number = arc4random_uniform(10)
return customAnnotationView
}
Custom Annoation View
private func customAnnotationView(in mapView: MKMapView, for annotation: MKAnnotation) -> CustomAnnotationView {
let identifier = "CustomAnnotationViewID"
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? CustomAnnotationView {
annotationView.annotation = annotation
return annotationView
} else {
let customAnnotationView = CustomAnnotationView(annotation: annotation, reuseIdentifier: identifier)
customAnnotationView.canShowCallout = true
return customAnnotationView
}
}
Result
The result would look like this:
I previously used a UIView to annotate a MKAnnotationView. I did this by adding a the view as a subview to the MKAnnotationView but soon found out that this caused a whole load of memory issues when rendering a lot of annotations on my map. Instead I reverted to building a UIView comprised of my different subviews and then converting it into a UIImage and assigning it to the image property of MKAnnotationView.
Here is a link to a Stack Overflow answer that will help with the UIView to UIImage conversion.

Why doesn't my MKPointAnnotation appear when the view loads?

In my app, the user saves some data, including a map coordinate. In my code, a pin is dropped at the saved map coordinate. Here is my code-
override func viewDidLoad() {
super.viewDidLoad()
self.mapView.delegate = self
// Data loading
itemNameTextField.delegate = self
itemDescriptionLabel.delegate = self
itemLocationTextView.delegate = self
// Press recognizer
if let item = item {
itemNameTextField.text = item.itemName
itemDescriptionLabel.text = item.itemDescription
itemLocationTextView.text = item.itemPlace
let dropPin = MKPointAnnotation()
dropPin.coordinate = mapView.convert(item.mapPoint, toCoordinateFrom: mapView)
dropPin.title = "Location of \(item.itemName)"
self.mapView.addAnnotation(dropPin)
print("Set the location of item pin to \(String(describing: dropPin.coordinate))")
}
// Styles
itemInfoView.layer.cornerRadius = 3
itemInfoView.layer.shadowColor = UIColor(red:0/255.0, green:0/255.0, blue:0/255.0, alpha: 1.0).cgColor
itemInfoView.layer.shadowOffset = CGSize(width: 0, height: 1.75)
itemInfoView.layer.shadowRadius = 1.7
itemInfoView.layer.shadowOpacity = 0.45
itemLocationView.layer.cornerRadius = 3
itemLocationView.layer.shadowColor = UIColor(red:0/255.0, green:0/255.0, blue:0/255.0, alpha: 1.0).cgColor
itemLocationView.layer.shadowOffset = CGSize(width: 0, height: 1.75)
itemLocationView.layer.shadowRadius = 1.7
itemLocationView.layer.shadowOpacity = 0.45
locationAdressTextview.layer.cornerRadius = 2
locationAdressTextview.layer.shadowColor = UIColor(red:0/255.0, green:0/255.0, blue:0/255.0, alpha: 1.0).cgColor
locationAdressTextview.layer.shadowOffset = CGSize(width: 0, height: 1.50)
locationAdressTextview.layer.shadowRadius = 1.6
locationAdressTextview.layer.shadowOpacity = 0.3
}
I know that the app does save the pin coordinates as a CGPoint, and I know that it converts it from a CGPoint to a CLLocationCoordinate2D, because of the print statements I placed. However, when the screen loads, the print statement shows a valid coordinate, but there is no pin on the map, and I get no errors. Can somebody please help me? Thanks!
Whenever the map should display an annotation the MKMapViewDelegate method viewForAnnotation is called so you have to implement that method and return a view according to your needs
Sample code:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "myAnnotationView")
// configure the view
return annotationView
}

Swift - Custom MKAnnotationView, set label title

I am trying to customise the MKAnnotationView for my mapView callout bubbles. I am fine with setting the annotation title when the annotation is created, and also customising the MKAnnotationView to add labels or images e.t.c (in the viewForAnnotation delegate), but how do I change the label created in the viewForAnnotation delegate, so that the title of it is different for each pin?
The other issue I have is that if I don't add a title or subtitle to the annotation when it is created in the viewDidLoad method, but I still try and create one by leaving self.map.addAnnotation(annotation), when I run the app and tap the pin no callout bubble is displayed.
In the end I would like to have totally customised callout bubbles, with individual labels on them for each pin. So what i really ned to know is how to access the viewForAnnotation delegate when the annotation is created to change properties of it for each pin.
override func viewDidLoad() {
super.viewDidLoad()
var countries: [String] = ["Germany","Germany","Poland","Denmark"]
var practiceRoute: [CLLocationCoordinate2D] = [CLLocationCoordinate2DMake(50, 10),CLLocationCoordinate2DMake(52, 9),CLLocationCoordinate2DMake(53, 20),CLLocationCoordinate2DMake(56, 14)]
for vari=0; i<practiceRoute.count; i++ {
var annotation = MKPointAnnotation
annotation.title = countries[i]
annotation.coordinate = practiceRoute[i]
self.map.addAnnotation(annotation)
}
}
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if annotation is MKUserLocation {
return nil
}
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId) as? MKPinAnnotationView
if(pinView==nil){
pinView=MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
pinView!.canShowCallout = true
let base = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
base.backgroundColor = UIColor.lightGrayColor()
let label1 = UILabel(frame: CGRect(x: 30, y: 10, width: 60, height: 15))
label1.textColor = UIColor.blackColor()
label1.text = "12 photos"
base.addSubview(label1)
pinView!.leftCalloutAccessoryView = base
pinView!.pinColor = .Red
}
return pinView
}
Make your custom annotation view
There is no public API allowing you to access the label in the pop up directly. What you need to do is make a subclass of MKPinAnnotationView and do whatever customization you want there. As an example,
class CustomAnnotationView : MKPinAnnotationView
{
let selectedLabel:UILabel = UILabel.init(frame:CGRectMake(0, 0, 140, 38))
override func setSelected(selected: Bool, animated: Bool)
{
super.setSelected(false, animated: animated)
if(selected)
{
// Do customization, for example:
selectedLabel.text = "Hello World!!"
selectedLabel.textAlignment = .Center
selectedLabel.font = UIFont.init(name: "HelveticaBold", size: 15)
selectedLabel.backgroundColor = UIColor.lightGrayColor()
selectedLabel.layer.borderColor = UIColor.darkGrayColor().CGColor
selectedLabel.layer.borderWidth = 2
selectedLabel.layer.cornerRadius = 5
selectedLabel.layer.masksToBounds = true
selectedLabel.center.x = 0.5 * self.frame.size.width;
selectedLabel.center.y = -0.5 * selectedLabel.frame.height;
self.addSubview(selectedLabel)
}
else
{
selectedLabel.removeFromSuperview()
}
}
}
Other Notes
Use this custom view in the map view:
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
var anno = mapView.dequeueReusableAnnotationViewWithIdentifier("Anno")
if anno == nil
{
anno = CustomAnnotationView.init(annotation: annotation, reuseIdentifier: "Anno")
}
return anno;
}
Since the title property of the annotation is not set, you will have to call the map view function selectAnnotation yourself. Add the following to the CustomAnnotationView class:
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
mapView?.selectAnnotation(self.annotation!, animated: true)
}
If you want to have more than one marker on the map:
Usually just draw the annotation simply during initialization. In setSelected just return false (meaning "show all annotations all the time").
class DotAnnotationView : MKPinAnnotationView {
let dot: UILabel = UILabel.init(frame:CGRect(x: 0, y: 0, width: 20, height: 20))
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
_setup()
}
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
_setup()
}
override func prepareForReuse() {
dot.text = "you forgot to set the text value?"
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(false, animated: animated)
}
func _setup() {
dot.textAlignment = .center
.. etc
}
}
You set the string (or other values - say color of the panel) for each annotation in mapView#viewFor. It's like populating a cell in a UITableView.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let textForThisItem = annotation.title!!
// or, just use index#of to determine which row this is in your data array
if annotation.isEqual(mkMap.userLocation) {
// skip the user-position indicator
return nil
}
var anno = mapView.dequeueReusableAnnotationView(withIdentifier: "anno")
if anno == nil {
anno = DotAnnotationView.init(annotation: annotation, reuseIdentifier: "anno")
}
(anno as! DotAnnotationView).dot.text = textForThisItem
return anno
}
Finally note that somewhat confusingly, if you very simply change the class of CustomAnnotationView from MKPinAnnotationView to MKAnnotationView, everything works the same but it replaces "all of the pin" rather than just the annotation.
Updated the code for the latest swift.
This is the new subclass of MKPinAnnotationView which you can copy paste to test:
import UIKit
import MapKit
class CustomAnnotationView : MKPinAnnotationView
{
let selectedLabel:UILabel = UILabel.init(frame:CGRect(x:0, y:0, width:140, height:38))
override func setSelected(_ selected: Bool, animated: Bool)
{
super.setSelected(false, animated: animated)
if(selected)
{
// Do customization, for example:
selectedLabel.text = "Hello World!!"
selectedLabel.textAlignment = .center
selectedLabel.font = UIFont.init(name: "HelveticaBold", size: 15)
selectedLabel.backgroundColor = UIColor.gray
selectedLabel.layer.borderColor = UIColor.darkGray.cgColor
selectedLabel.layer.borderWidth = 2
selectedLabel.layer.cornerRadius = 5
selectedLabel.layer.masksToBounds = true
selectedLabel.center.x = 0.5 * self.frame.size.width;
selectedLabel.center.y = -0.5 * selectedLabel.frame.height;
self.addSubview(selectedLabel)
}
else
{
selectedLabel.removeFromSuperview()
}
}
}

Resources