Custom CalloutView with Swift 2.0 Best Approach? - ios

I want to create a custom CalloutView from xib. I created a xib and custom AnnotationView class. I want to use my MapViewController segue which name is showDetail segue because this segue is a Push segue. When the user taps the button in my calloutView it should perform my showDetail segue.
I searched all documents, tutorials, guides and questions but I could
not find a any solution with Swift. There are no custom libraries also. I couldn't find any solution it as been 3 days.
My CalloutView.xib file View is my CustomAnnotationView which name is CalloutAnnotationView.swift and File's Owner is MapViewController it allows me to use MapViewController segues.
There is my CalloutAnnotationView.swift
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, withEvent: event)
if (hitView != nil) {
self.superview?.bringSubviewToFront(self)
}
return hitView
}
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
let rect = self.bounds
var isInside = CGRectContainsPoint(rect, point)
if (!isInside) {
for view in self.subviews {
isInside = CGRectContainsPoint(view.frame, point)
if (isInside) {
}
}
}
return isInside
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
let calloutViewController = MapViewController(nibName: "CalloutView", bundle: nil)
if selected {
calloutViewController.view.clipsToBounds = true
calloutViewController.view.layer.cornerRadius = 10
calloutViewController.view.center = CGPointMake(self.bounds.size.width * 0.5, -calloutViewController.view.bounds.size.height * 0.5)
self.addSubview(calloutViewController.view)
} else {
// Dismiss View
calloutViewController.view.removeFromSuperview()
}
}
My MapViewController.swift codes;
// MARK: - MapKit Delegate Methods
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if let annotation = annotation as? Veterinary { // Checking annotations is not User Location.
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(Constants.MapViewAnnotationViewReuseIdentifier) as? CalloutAnnotationView
if pinView == nil { // If it is nil it created new Pin View
pinView = CalloutAnnotationView(annotation: annotation, reuseIdentifier: Constants.MapViewAnnotationViewReuseIdentifier)
pinView?.canShowCallout = false
} else { pinView?.annotation = annotation } // If it is NOT nil it uses old annotations.
// Checking pin colors from Veterinary Class PinColor Method
pinView?.image = annotation.pinColors()
//pinView?.rightCalloutAccessoryView = UIButton(type: .DetailDisclosure)
return pinView
}
return nil
}
These codes are working fine to load my xib file. I created IBAction from a button and It performs segues from MapViewControllerto my DetailViewController with showDetail segue. But problem is It doesn't remove my customCalloutView from superview so I can't dismiss callout views.
How can I solve this problem or What is the best way to create custom calloutView for use my MapViewController push segues ?
Thank you very much.

In your MKMapView delegate see if this gets called when segueing:
func mapView(mapView: MKMapView, didDeselectAnnotationView view: MKAnnotationView) {
if let annotation = view.annotation as? Veterinary {
mapView.removeAnnotation(annotation)
}
}

I hope this helps you with the problem
func mapView(mapView: MKMapView, didDeselectAnnotationView view: MKAnnotationView) {
if view.isKindOfClass(AnnotationView)
{
for subview in view.subviews
{
subview.removeFromSuperview()
}
}
}

Related

Don't animate MKMarkerAnnotationView when selected

I would like to capture taps on an MKMarkerAnnotationView, and not animate the view when this happens.
Through a delegate for my MKMapView, I can capture selection and deselection that are roughly equivalent to taps on the MKMarkerAnnotationView (the selection also happens when tapping the label that is not part of the MKMarkerAnnotationView)
I am trying to remove the default animation. I didn't find a straightforward solution.
I tried:
1/ setting the view to not selected during the selection. This does cancel the animation but further taps are not captured.
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
view.setSelected(false, animated: false)
// or
view.isSelected = false
handleTap(view)
}
2/ adding another tap gesture recognizer on the view and preventing other gesture recognizers from receiving the touch. This works well unless I tap the label instead of the annotation view.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let view = dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) as? MKMarkerAnnotationView ??
MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
view.annotation = annotation
view.gestureRecognizers?.forEach { view.removeGestureRecognizer($0) }
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTap(gesture:)))
gestureRecognizer.delegate = self
view.addGestureRecognizer(gestureRecognizer)
return view
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
You can try override class:
final class CustomMarkerAnnotationView: MKMarkerAnnotationView {
var onSelect: (() -> Void)?
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(false, animated: false)
if selected {
onSelect?() // or catch this in delegate
}
}
}
Overriding did not work for me. Here's what ultimately worked for me (Objc):
- (void)mapView:(MKMapView *)mapView
didSelectAnnotationView:(MKAnnotationView *)view
{
//stops animation
for( CALayer *sublayer in view.layer.sublayers )
{
[sublayer removeAllAnimations];
}
//stops selection
[view setSelected:NO animated:NO];
}
For my own purposes I used the below instead, as it allows for properly deselecting the pin when tapping the pin again immediately after selection.
- (void)mapView:(MKMapView *)mapView
didSelectAnnotationView:(MKAnnotationView *)view
{
//stops animation
for( CALayer *sublayer in view.layer.sublayers )
{
[sublayer removeAllAnimations];
}
//stops selection
[mapView deselectAnnotation:view.annotation animated:NO];
}

Custom View Xib for MKAnnotationView button tap

I'm making a custom MKAnnotationView and i have a button inside the xib, inwhich i want to get when is touchedup inside.
Although there are questions with the same issue, none of them is working with my code.
I have the following.
class CustomCalloutView: MKAnnotationView {
var didTapGoButton: (() -> Void)?
// MARK: - Detect taps on callout
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let rect = self.bounds;
var isInside: Bool = rect.contains(point);
if(!isInside)
{
for view in self.subviews
{
isInside = view.frame.contains(point);
if isInside
{
break;
}
}
}
return isInside;
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if (hitView != nil)
{
self.superview?.bringSubview(toFront: self)
}
return hitView
}
#IBAction func viewEventDetails(_ sender: Any) {
didTapGoButton?()
}
func configureCell(events: Events) {
eventName.text = events.eventName
}
}
ViewController
class EventAnnotation : MKPointAnnotation {
var myEvent:Events?
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let annotation = view.annotation else { return }
if view.annotation!.isKind(of: MKUserLocation.self){
return
}
if let eventAnnotation = view.annotation as? EventAnnotation {
let theEvent = eventAnnotation.myEvent
let customView = (Bundle.main.loadNibNamed("CustomCalloutView", owner: self, options: nil))?[0] as! CustomCalloutView;
let calloutViewFrame = customView.frame;
customView.frame = CGRect(x: -calloutViewFrame.size.width/2.23, y: -calloutViewFrame.size.height-7, width: 315, height: 298)
customView.configureCell(events: theEvent!)
customView.didTapGoButton = { [weak mapView ] in
print(theEvent?.eventName ?? "noname")
}
view.addSubview(customView)
}
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView)
{
for childView:AnyObject in view.subviews{
childView.removeFromSuperview();
}
}
So when i tap the button nothing is being printed !
Any idea where is my mistake in the code?
I solved it , the idea is to position the customView with in the boundary of the pinAnnotationView image as any click out side of it will be considered a didSelect method call , so play with frame until click detected , it will work either in custom view or viewController
customView.frame = CGRect(x: -calloutViewFrame.size.width/2.23, y: calloutViewFrame.size.height, width: 315, height: 298)
and here is a demo to illustrate the situation
customPinAnnotationButton

Custom Callout View Button to Map

I am working on a MapView where on click of any custom annotation pin, I am showing custom callout view (load from xib file).
From this Custom Callout I have an UIButton, I already can detect click on this button but I want to access on the Map like the : view?.rightCalloutAccessoryView in the basic callout.
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView, {
if view.annotation!.isKind(of: MKUserLocation.self){
return
}
let customView = (Bundle.main.loadNibNamed("CustomCalloutView", owner: self, options: nil))?[0] as! CustomCalloutView;
let calloutViewFrame = customView.frame;
customView.frame = CGRect(x: -calloutViewFrame.size.width/2.23, y: -calloutViewFrame.size.height+10, width: 315, height: 170)
view.addSubview(customView)
let region = MKCoordinateRegion(center: pinToZoomOn!.coordinate, span: span)
mapView.setRegion(region, animated: true)
}
The route is correctly calculated from the classic callout but I can't know how to access my map from the button of my custom callout.
My CustomCalloutViewClass :
import UIKit
import MapKit
class CustomCalloutView: MKAnnotationView {
#IBOutlet weak var goButton: UIButton!
#IBAction func goButton(_ sender: AnyObject) {
print("Button clicked sucessfully")
}
// MARK: - Detect taps on callout
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView != nil {
superview?.bringSubview(toFront: self)
}
return hitView
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let rect = self.bounds
var isInside = rect.contains(point)
if !isInside {
for view in subviews {
isInside = view.frame.contains(point)
if isInside {
break
}
}
}
return isInside
}
}
If someone have an idea it will be helpfull I'm stuck on this issue.
Thank you in advance.
Option 1: Capture a MKMapView instance in the closure passed to a CustomCalloutView
Add the closure which will be called on the button action. The closure will capture the MKMapView instance and you will be able to us is inside.
class CustomCalloutView: MKAnnotationView {
var didTapGoButton: (() -> Void)?
#IBAction func goButton(_ sender: AnyObject) {
didTapGoButton?()
}
}
Option 2: Add a weak reference to MKMapView as a property of the callout
This is not a clean solution but it may be an option under some circumstances. You only have to create a weak property storing a reference to MKMapView instance in CustomCalloutView
class CustomCalloutView: MKAnnotationView {
weak var mapView: MKMapView?
}
Configuration
This is how you can wire up the CustomCalloutView for both solutions. Remember to use swift capture list to capture a weak reference to a MKMapView instance. Without it you may create a strong reference cycle.
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
// ...
let customView = (Bundle.main.loadNibNamed("CustomCalloutView", owner: self, options: nil))?[0] as! CustomCalloutView;
// Option 1
customView.didTapGoButton = { [weak mapView ] in
print(mapView?.annotations.count)
}
// Option 2
customView.mapView = mapView
// ...
}
Thank you so much for your help !
I used the first option to capture the MKMapView instance.
If someone is stuck on the same issue you can use the first option and add this in your didSelect MapView function:
let selectedLoc = view.annotation
print("Annotation '\(String(describing: selectedLoc?.title!))' has been selected")
let location = view.annotation as! YourCustomClassType
let launchOptions = [MKLaunchOptionsDirectionsModeKey : MKLaunchOptionsDirectionsModeDriving]
customView.didTapGoButton = { location.mapItem().openInMaps(launchOptions: launchOptions) }
Thank's
var didTapGoButton: (() -> Void)? this types of closures are very helpful for navigation from the view to viewControllers through the button actions in the call layoutviews in the mkmapview.

Handle selection of custom CallOutView swift

I've created a custom callOutView, which seem to work fine. However i have problem with handling tap on the custom callOut view. I would like push to a specific viewController when it is selected. How can i achieve this?
below i've added my FBSingleClusterView, which is the custom MKAnnotationView and the bubbleView which is the custom callOutView. beside this i ofcourse have an viewController with a mapView.
FBSingleClusterView Variables
private var hitOutside:Bool = true
var preventDeselection:Bool {
return !hitOutside
}
FBSingleClusterView methods
override func setSelected(selected: Bool, animated: Bool) {
let calloutViewAdded = bubbleView?.superview != nil
if (selected || !selected && hitOutside) {
super.setSelected(selected, animated: animated)
}
self.superview?.bringSubviewToFront(self)
if (bubbleView == nil) {
bubbleView = BubbleView()
}
if (self.selected && !calloutViewAdded) {
bubbleView?.clipsToBounds = true
bubbleView?.layer.masksToBounds = true
self.addSubview(bubbleView!)
let discView = UIImageView()
discView.contentMode = UIViewContentMode.Center
discView.image = UIImage()
discView.image = UIImage(named: "Disclosure")
bubbleView?.contentView.addSubview(discView)
let nameLabel = UILabel()
nameLabel.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
nameLabel.font = UIFont.systemFontOfSize(10)
nameLabel.textColor = UIColor.whiteColor()
nameLabel.text = companyString?.uppercaseString
bubbleView?.addSubview(nameLabel)
discView.snp_makeConstraints { (make) -> Void in
make.top.equalTo(bubbleView!).offset(0)
make.height.equalTo(30)
make.width.equalTo(20)
make.right.equalTo(bubbleView!).offset(0)
}
nameLabel.snp_makeConstraints { (make) -> Void in
make.top.equalTo(bubbleView!).offset(0)
make.left.equalTo(bubbleView!).offset(10)
make.height.equalTo(30)
make.right.equalTo(bubbleView!).offset(-20)
}
let nameLabelWidth = nameLabel.requiredWidth(companyString!.uppercaseString, font: UIFont.systemFontOfSize(10)) + 35
let bubbleHeight = 35 as CGFloat
bubbleView?.frame = CGRectMake((self.frame.width/2)-(nameLabelWidth/2), -bubbleHeight-2, nameLabelWidth, bubbleHeight)
}
if (!self.selected) {
bubbleView?.removeFromSuperview()
}
}
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
var hitView = super.hitTest(point, withEvent: event)
if let callout = bubbleView {
if (hitView == nil && self.selected) {
hitView = callout.hitTest(point, withEvent: event)
}
}
hitOutside = hitView == nil
return hitView;
}
bubbleView methods
override public func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let viewPoint = superview?.convertPoint(point, toView: self) ?? point
// let isInsideView = pointInside(viewPoint, withEvent: event)
let view = super.hitTest(viewPoint, withEvent: event)
return view
}
override public func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
return CGRectContainsPoint(bounds, point)
}
}
Have you tried using instantiateViewControllerWithIdentifier? You can trigger this when tapping a button, in the viewDidLoad or in whatever other method you want it to trigger and push a new a viewController.
Firstly, set the "Storyboard ID" for the viewController in your storyboard. Like this:
Example:
Here's an example of how you can push to a new viewController when tapping a cell. As mentioned you can use this method in different functions to push to another viewController :
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let yourNewView = self.storyboard!.instantiateViewControllerWithIdentifier("yourStoryBoardID") as! ViewControllerYourePushingTo
self.presentViewController(yourNewView, animated: true, completion: nil)
}
In your case this is in the hitTest func.
I asked a similar question not too long ago. Here's the reference: Swift: Triggering TableViewCell to lead to a link in a UIWebView in another ViewController
Here's the link from Apple Dev: InstantiateViewControllerWithIdentifier
You can add gesture on custom annotation view in didselectannotation and navigate to other view controllor from gesture action

Customizing callout view for MKAnnotation in Swift

I need a completely customizable callout for for MKAnnotation. I have subclassed MKAnnotationView and an UIView. My code:
class CustomAnnotationView: MKAnnotationView {
class var reuseIdentifier:String {
return "CustomAnnotationView"
}
private var calloutView:MapPinCallOut?
private var hitOutside:Bool = true
var preventDeselection:Bool {
return !hitOutside
}
convenience init(annotation:MKAnnotation!) {
self.init(annotation: annotation, reuseIdentifier: CustomAnnotationView.reuseIdentifier)
//false?
canShowCallout = true;
}
override func setSelected(selected: Bool, animated: Bool) {
let calloutViewAdded = calloutView?.superview != nil
if (selected || !selected && hitOutside) {
super.setSelected(selected, animated: animated)
}
self.superview?.bringSubviewToFront(self)
if (calloutView == nil) {
calloutView = MapPinCallOut()
}
if (self.selected && !calloutViewAdded) {
addSubview(calloutView!)
}
if (!self.selected) {
calloutView?.removeFromSuperview()
}
}
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
var hitView = super.hitTest(point, withEvent: event)
if let callout = calloutView {
if (hitView == nil && self.selected) {
hitView = callout.hitTest(point, withEvent: event)
}
}
hitOutside = hitView == nil
return hitView;
}
}
Here is my UIView:
class MapPinCallOut: UIView {
override func hitTest(var point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let viewPoint = superview?.convertPoint(point, toView: self) ?? point
let isInsideView = pointInside(viewPoint, withEvent: event)
var view = super.hitTest(viewPoint, withEvent: event)
return view
}
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
return CGRectContainsPoint(bounds, point)
}
}
I have also created a nib file and changed the file's owner to "MapPinCallOut"
Then in my viewDidLoad of my viewController, I am adding a pin like this:
let location = CLLocationCoordinate2D(
latitude: 25.0336,
longitude: 121.5650
)
let span = MKCoordinateSpanMake(0.05, 0.05)
let region = MKCoordinateRegion(center: location, span: span)
mapView.setRegion(region, animated: true)
let string = "title"
let annotation = CustomAnnotation(location: location, title: string, subtitle: string)
mapView.addAnnotation(annotation)
and then in my viewForAnnotation function:
var pinView: CustomAnnotationView = CustomAnnotationView()
pinView.annotation = annotation
pinView.canShowCallout = true
return pinView
After all that, I ran my code and when I select the annotation, the callout does appear but it is still the standard annotation. What am I missing to create my own custom callout view?
Set pinView.canShowCallout to false in your viewForAnnotation. That may help you out.

Resources