Custom Callout View Button to Map - ios

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.

Related

Not able to get non zero location if the UITapGestureRecognizer is installed at top level UIWindow keyWindow

The following code able to detect tap location
Able to detect tap location
class ViewController: UIViewController {
private lazy var gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tap))
#objc func tap(_ gestureRecognizer: UITapGestureRecognizer) {
let location = gestureRecognizer.location(in: gestureRecognizer.view)
print("location \(location)")
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
self.view.addGestureRecognizer(gestureRecognizer)
}
}
However, we want to detection done on the top level Window, globally, without tied to a single view controller's view.
Not able to detect tap location
extension UIWindow {
static var key: UIWindow! {
if #available(iOS 13, *) {
return UIApplication.shared.windows.first { $0.isKeyWindow }
} else {
return UIApplication.shared.keyWindow
}
}
}
class ViewController: UIViewController {
private lazy var gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tap))
#objc func tap(_ gestureRecognizer: UITapGestureRecognizer) {
let location = gestureRecognizer.location(in: gestureRecognizer.view)
print("location \(location)")
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
//self.view.addGestureRecognizer(gestureRecognizer)
UIWindow.key.addGestureRecognizer(gestureRecognizer)
}
}
Doesn't matter where do you tap, we always get
location (0.0, 0.0)
location (0.0, 0.0)
Does anyone know why it is so, and how can we resolve such? Thanks.
I don't know exactly why passing the window to location(in:) always gives you (0, 0). This is as if the window is not in the same view hierarchy, as if you did location(in: UIView()).
Anyway, note that the documentation says that you should pass nil to location(in:) to indicate that you want the location in the coordinate system of the window:
Parameters
view
A UIView object on which the gesture took place. Specify nil to indicate the window.
I suppose this could be interpreted as "you should not pass the window to location(in:), use nil to do that instead".
This works:
let location = gestureRecognizer.location(in: nil)
print("location \(location)")
You always need a view to add the gesture to , Replace
UIWindow.key.addGestureRecognizer(gestureRecognizer)
with
UIWindow.key.subviews.last?.addGestureRecognizer(gestureRecognizer)

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

How to get the UIView instance for a Google Map marker?

I want to show a VC as a pop up when the user taps on one of the markers on the Google Map.
The reason that I want to do this is because I want to control the view that pops up when the marker is tapped. I tried using the mapView(mapView: GMSMapView, markerInfoWindow marker: GMSMarker) delegate method. But I don't know how to create a view controller in that method that controls the marker info window's view.
To present a VC as a pop over, I did this:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("MarkerInfoController")
vc.modalInPopover = true
vc.modalPresentationStyle = .Popover
print(marker.iconView)
vc.popoverPresentationController!.sourceView = marker.iconView
self.presentVC(vc) // this is from EZSwiftExtensions. Don't worry about it
The problem arises when I try to set the sourceView of the UIPopoverPresentationController. I thought using the iconView property would work, but no. There is always an error saying that sourceView is not set.
How can I get the UIView instance for the marker, so that I can assign it to sourceView?
P.S. This is how a marker is created:
func mapView(mapView: GMSMapView, didLongPressAtCoordinate coordinate: CLLocationCoordinate2D) {
let marker = GMSMarker(position: coordinate)
marker.map = mapView
}
Output:
Code:
import UIKit
import GoogleMaps
class MapViewController: UIViewController {
#IBOutlet weak var mapView: GMSMapView!
var sourceView: UIView?
}
extension MapViewController: GMSMapViewDelegate {
func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
mapCenterPinImage.fadeOut(0.25)
// Adding a delay becuase when click on marker Camera changes it position
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
let location = marker.accessibilityActivationPoint
self.sourceView = UIView(frame: CGRect(x: location.x, y: location.y, width: 1, height: 1))
self.view.addSubview(self.sourceView!)
let popController = MyPopUpViewController()
popController.modalPresentationStyle = UIModalPresentationStyle.popover
popController.preferredContentSize = CGSize(width: 200, height: 200)
popController.popoverPresentationController?.delegate = self
popController.popoverPresentationController?.sourceView = self.sourceView
self.present(popController, animated: true)
}
return false
}
}
extension MapViewController: UIPopoverPresentationControllerDelegate{
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
func popoverPresentationControllerShouldDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) -> Bool {
sourceView?.removeFromSuperview()
sourceView = nil
return true
}
}
What I've done basically created a UIView and added it to ViewController at runtime and set this as a source of Popup and make it nil when it's dismissed.
marker.accessibilityActivationPoint is the source of X and Y according to device's screen
You can just customise the infoView of the GMS Marker instead of showing a viewControlle and perform your desired operations there. You can have a look at this article over here. I faced something similar to your issue and this helped me out. Hope it helps you out as well. :D
https://medium.com/#matschmidy/how-to-implement-custom-and-dynamic-map-marker-info-windows-for-google-maps-ios-e9d993ef46d4
You can do this on below is a gmsMapView delegate mehtod
func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool
here you will get the marker which you tapped , and for source view you can access
marker.iconView
which I think will solve your problem, hope it helps :)

How to catch the click on the button in added XIB view (annotation)

1) I have the ViewController with the MapKit
1.1) I have added some pin's to Map
class ViewController: UIViewController, MKMapViewDelegate
2) I write the new classes for custom pin Callout and Annotation
class CustomPointAnnotation: MKPointAnnotation {
class CustomCalloutView: UIView {
3) I haved created .xib for my custom pin callout
4) I created the button in my .xib, this button must do something, for example
#IBAction func clickTest(sender: AnyObject) {
print("aaaaa")
}
5) This button doesn't work
What is usually practice for this? I want to make my button working.
Button is blue:
All project you can see on Github: https://github.com/genFollowMe1/forStack
Thank you, sorry for English )
for Objective C:
https://github.com/nfarina/calloutview
Update
for swift
subclass the MKAnnotationView for custom call out and override the hitTest: and pointInside: methods.
import UIKit
import MapKit
class CustomCalloutView: MKAnnotationView {
#IBOutlet weak var name: UILabel!
#IBAction func goButton(sender: AnyObject) {
print("button clicked sucessfully")
}
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, withEvent: event)
if hitView != nil {
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 subviews {
isInside = CGRectContainsPoint(view.frame, point)
if isInside {
break
}
}
}
return isInside
}
}
The way you have to do it's to make a reference of the button from your .xib into the associated .swift file.
In your ViewController you do something like this :
let button:UIButton!
[...]
button.targetForAction("tappedButton:", withSender: self)
func tappedButton() {
print("Taped !")
}

Custom CalloutView with Swift 2.0 Best Approach?

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()
}
}
}

Resources