Swift: Add Directions button to pin annotation - swift

I have an app that displays the company location using an MKMapView. When the user visits the contact page a map appears at the top of the screen where a pin drops and it shows our location. When the map pin is clicked it shows the company name in the annotation.
I am pulling the company address into the MKMApView from data stored in my Parse backend using back4app.
I have this all working perfectly, however, when I click the map pin and it shows the company name, I am trying to get a car icon button in there also so the user can get directions to our company.
Here is the code I have to set the annotation:
func addPinOnMap(_ address: String) {
var hotelClass = PFObject(className: HOTEL_CLASS_NAME)
hotelClass = hotelArray[0]
if mapView.annotations.count != 0 {
annotation = mapView.annotations[0]
mapView.removeAnnotation(annotation)
}
// Make a search on the Map
localSearchRequest = MKLocalSearchRequest()
localSearchRequest.naturalLanguageQuery = address
localSearch = MKLocalSearch(request: localSearchRequest)
localSearch.start { (localSearchResponse, error) -> Void in
// Add PointAnnonation text and a Pin to the Map
self.pointAnnotation = MKPointAnnotation()
self.pointAnnotation.title = "\(hotelClass[HOTEL_NAME]!)"
self.pointAnnotation.coordinate = CLLocationCoordinate2D( latitude: localSearchResponse!.boundingRegion.center.latitude, longitude:localSearchResponse!.boundingRegion.center.longitude)
self.pinView = MKPinAnnotationView(annotation: self.pointAnnotation, reuseIdentifier: nil)
self.mapView.centerCoordinate = self.pointAnnotation.coordinate
self.mapView.addAnnotation(self.pinView.annotation!)
// Zoom the Map to the location
self.region = MKCoordinateRegionMakeWithDistance(self.pointAnnotation.coordinate, 1000, 1000);
self.mapView.setRegion(self.region, animated: true)
self.mapView.regionThatFits(self.region)
self.mapView.reloadInputViews()
}
}
I simply do not know how I can get the button icon in the annotation and show directions upon click of the button. either using the current map view or to open the users maps app on their device, either way is fine.
Thank you.

I propose you a sample method to had a button on your pin and to give to the user the direction of the pin with the apple map app.
First, after your pin's declaration, you should declare a button witch give you the opportunity to interact with the user in the pin (we will show later how to initialize the button in the pin).
You can declare your button like this:
let smallSquare = CGSize(width: 30, height: 30)
let button = UIButton(frame: CGRect(origin: CGPointZero, size: smallSquare))
Note, smallSquare give to the button the correct size of your future pin's button.
You can add a image in the pin's button with the method: button.setBackgroundImage(UIImage(named: "car"), forState: .Normal).
Apple developer give to us some documentation: Apple developer web site.
Then you can had a button action with this method: button.addTarget(self, action: #selector(ViewController.getDirections), forControlEvents: .TouchUpInside).
So, like this, if the button is selected normally (.TouchUpInside), the program calls the function getDirection ().
Now, you can initialize the button in your pin with the method: pinView?.leftCalloutAccessoryView = button.
Note this method set the button in the left of the pin. You can put the button in the right like this: pinView?.rightCalloutAccessoryView = button.
Apple developer give some informations on the subject.
However, I do not know how to initialize the button in the totally surface of the pin.
Finally, you should have the function getDirections ()to give to the user the "direction" of the pin.
I propose you to use the apple map app for this purpose.
My function getDirections () look like this:
func getDirections(){
guard let selectedPin = selectedPin else { return }
let mapItem = MKMapItem(placemark: selectedPin)
let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]
mapItem.openInMapsWithLaunchOptions(launchOptions)
print ("GET DIRECTION")
}
I find this function in this web site. I think it can be explain this better than me.
At the end my program looks like this:
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?{
guard !(annotation is MKUserLocation) else { return nil }
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId) as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
}
pinView?.pinTintColor = UIColor.orangeColor()
pinView?.canShowCallout = true
//The initialization of the pin. Here your program looks great.
// And after the code as seen above.
let smallSquare = CGSize(width: 30, height: 30)
let button = UIButton(frame: CGRect(origin: CGPointZero, size: smallSquare))
button.setBackgroundImage(UIImage(named: "car"), forState: .Normal)
button.addTarget(self, action: #selector(ViewController.getDirections), forControlEvents: .TouchUpInside)
pinView?.leftCalloutAccessoryView = button
return pinView
}
func getDirections(){
guard let selectedPin = selectedPin else { return }
let mapItem = MKMapItem(placemark: selectedPin)
let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]
mapItem.openInMapsWithLaunchOptions(launchOptions)
print ("GET DIRECTION")
}
Normally, your program should look like this:
func addPinOnMap(_ address: String) {
var hotelClass = PFObject(className: HOTEL_CLASS_NAME)
hotelClass = hotelArray[0]
if mapView.annotations.count != 0 {
annotation = mapView.annotations[0]
mapView.removeAnnotation(annotation)
}
// Make a search on the Map
localSearchRequest = MKLocalSearchRequest()
localSearchRequest.naturalLanguageQuery = address
localSearch = MKLocalSearch(request: localSearchRequest)
localSearch.start { (localSearchResponse, error) -> Void in
// Add PointAnnonation text and a Pin to the Map
self.pointAnnotation = MKPointAnnotation()
self.pointAnnotation.title = "\(hotelClass[HOTEL_NAME]!)"
self.pointAnnotation.coordinate = CLLocationCoordinate2D( latitude: localSearchResponse!.boundingRegion.center.latitude, longitude:localSearchResponse!.boundingRegion.center.longitude)
self.pinView = MKPinAnnotationView(annotation: self.pointAnnotation, reuseIdentifier: nil)
self.mapView.centerCoordinate = self.pointAnnotation.coordinate
self.mapView.addAnnotation(self.pinView.annotation!)
let smallSquare = CGSize(width: 30, height: 30)
let button = UIButton(frame: CGRect(origin: CGPointZero, size: smallSquare))
button.setBackgroundImage(UIImage(named: "car"), forState: .Normal)
button.addTarget(self, action: #selector(ViewController.getDirections), forControlEvents: .TouchUpInside)
self.pinView?.leftCalloutAccessoryView = button
// Zoom the Map to the location
self.region = MKCoordinateRegionMakeWithDistance(self.pointAnnotation.coordinate, 1000, 1000);
self.mapView.setRegion(self.region, animated: true)
self.mapView.regionThatFits(self.region)
self.mapView.reloadInputViews()
}
func getDirections(){
guard let selectedPin = selectedPin else { return }
let mapItem = MKMapItem(placemark: selectedPin)
let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]
mapItem.openInMapsWithLaunchOptions(launchOptions)
print ("GET DIRECTION")
}
Excuse me, I can not try your code because I have not your totally code like the pinView declaration.
Note, you can need to add this line of code to accept than the pin can show callout pinView?.canShowCallout = true.
You can consult this webs sites if you want more details or tell me a comment.
Thorn technologies
Apple developer

Related

Swift - UIPanGestureRecognizer selecting all layers when only want the top layer selected

I have function which creates a drag line to connect 2 buttons to each other. This works fine but if some buttons overlap each other, it will select both if I drag over where they overlap. I only want to connect the top button.
I think the issue is with the sender.location selecting layers on top and below. Is there a way to tell the sender.location to only select the top view? Thanks for any input and direction
func addPanReconiser(view: UIView){
let pan = UIPanGestureRecognizer(target: self, action: #selector(DesignViewController.panGestureCalled(_:)))
view.addGestureRecognizer(pan)
}
#objc func panGestureCalled(_ sender: UIPanGestureRecognizer) {
let currentPanPoint = sender.location(in: self.view)
switch sender.state {
case .began:
panGestureStartPoint = currentPanPoint
self.view.layer.addSublayer(lineShape)
case .changed:
let linePath = UIBezierPath()
linePath.move(to: panGestureStartPoint)
linePath.addLine(to: currentPanPoint)
lineShape.path = linePath.cgPath
lineShape.path = CGPath.barbell(from: panGestureStartPoint, to: currentPanPoint, barThickness: 2.0, bellRadius: 6.0)
for button in buttonArray {
let point = sender.location(in: button)
if button.layer.contains(point) {
button.layer.borderWidth = 4
button.layer.borderColor = UIColor.blue.cgColor
} else {
button.layer.borderWidth = 0
button.layer.borderColor = UIColor.clear.cgColor
}
}
case .ended:
for button in buttonArray {
let point = sender.location(in: button)
if button.layer.contains(point){
//DO my Action here
lineShape.path = nil
lineShape.removeFromSuperlayer()
}
}
default: break
}
}
}
Note: some of the lines of codes are from custom extensions. I kept them in as they were self explanatory.
Thanks for the help
There is a way to walk around. It seems like you simply want your gesture end up at one button above all the others, thus by adding a var outside the loop and each time a button picked, comparing with the var of its level at z.
case .ended:
var pickedButton: UIButton?
for button in buttonArray {
let point = sender.location(in: button)
if button.layer.contains(point){
if pickedButton == nil {
pickedButton = button
} else {
if let parent = button.superView, parent.subviews.firstIndex(of: button) > parent.subviews.firstIndex(of: pickedButton!) {
pickedButton = button
}
}
}
}
//DO my Action with pickedButton here
lineShape.path = nil
lineShape.removeFromSuperlayer()
A UIView has a property called subViews where elements with higher indexes are in front of the ones with lower indexes. For instance, subView at index 1 is in front of subView with index 0.
That being said, to get the button that's on top, you should sort your buttonArray the same way subViews property of UIView is organized. Assuming that your buttons are all siblings of the same UIView (this might not be necessarily the case, but you can tweak them so you get them sorted correctly):
var buttonArray = view.subviews.compactMap { $0 as? UIButton }
Thus, keeping your buttonArray sorted that way, the button you want is the one that contains let point = sender.location(in: button) with higher index in the array.

Will this statement always evaluate to nil in Swift?

open var buttonInit: ((_ index: Int) -> UIButton?)?
...
if let button: UIButton = self.buttonInit?(i) {
finButton = button
}else {
let button = UIButton(type: .custom)
button.setTitleColor(button.tintColor, for: [])
button.layer.borderColor = button.tintColor.cgColor
button.layer.borderWidth = 1
button.layer.cornerRadius = buttonHeight/2
finButton = button
}
I don't find any function description about buttonInit in AZDialogViewController. Does it mean button: UIButton = self.buttonInit?(i) will always be nil and finButton = button will not be executed?
The latter part of the code you quoted is in the setUpButton method:
fileprivate func setupButton(index i:Int) -> UIButton{
if buttonHeight == 0 {buttonHeight = CGFloat(Int(deviceHeight * 0.07))}
let finButton: UIButton
if let button: UIButton = self.buttonInit?(i) {
finButton = button
}else {
let button = UIButton(type: .custom)
button.setTitleColor(button.tintColor, for: [])
button.layer.borderColor = button.tintColor.cgColor
button.layer.borderWidth = 1
button.layer.cornerRadius = buttonHeight/2
finButton = button
}
This method is called here:
open func addAction(_ action: AZDialogAction){
actions.append(action)
if buttonsStackView != nil{
let button = setupButton(index: actions.count-1)
self.buttonsStackView.addArrangedSubview(button)
//button.frame = buttonsStackView.bounds
button.center = CGPoint(x: buttonsStackView.bounds.midX,y: buttonsStackView.bounds.maxY)
animateStackView()
}
}
From this we can see that buttonInit seems to be used to let the user of the library specify what kind of button they want as the action buttons. Another piece of evidence is that buttonInit is declared open, so it is likely that it is the client code who should set this, not the AZDialogViewController.
Plus, the README file showed this usage:
Use custom UIButton sub-class:
dialog.buttonInit = { index in
//set a custom button only for the first index
return index == 0 ? HighlightableButton() : nil
}
So to answer your question, the if branch will be executed if you set buttonInit.
#Huwell,
the documentation in the repository states to initialize the button in the following manner:
dialog.buttonInit = { index in
//set a custom button only for the first index
return index == 0 ? HighlightableButton() : nil
}
The button should be part of your DialogViewController.

How to delay callout from showing when annotation selected in MKMapView? Swift 4

(This is my first stack overflow question haha)
UPDATE:
From this link - Center MKMapView BEFORE displaying callout
I implemented the solution from Thermometer, however selecting and deselecting the annotation makes it look like my application is glitching.
I think the best way would be to delay the callOut (detail pop up) from appearing by a few seconds so the map has time to move first.
Here is my code:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let annotation = view.annotation else {
return
}
let currentAnnotation = view.annotation as? MapMarker
Global.currentAnnotation = currentAnnotation
findRelatinoshipLines()
if Global.showLifeStoryLines {
var locations = lifeStoryAnnotations.map { $0.coordinate }
let polyline = MKPolyline(coordinates: &locations, count: locations.count)
Global.finalLineColor = Global.lifeStoryColor
mapView.addOverlay(polyline)
}
if Global.showFatherLines {
var fatherLocations = fatherTreeAnnotations.map { $0.coordinate }
let fatherPolyline = MKPolyline(coordinates: &fatherLocations, count: fatherLocations.count)
Global.finalLineColor = Global.fatherLineageColor
mapView.addOverlay(fatherPolyline)
}
if Global.showMotherLines {
var motherLocations = motherTreeAnnotations.map { $0.coordinate }
let motherPolyline = MKPolyline(coordinates: &motherLocations, count: motherLocations.count)
Global.finalLineColor = Global.motherLineageColor
mapView.addOverlay(motherPolyline)
}
if Global.showSpouseLines {
var locations = spouseAnnotations.map { $0.coordinate }
let polyline = MKPolyline(coordinates: &locations, count: locations.count)
Global.finalLineColor = Global.spouseColor
mapView.addOverlay(polyline)
}
if Global.zoomChange == true {
Global.zoomChange = false
} else {
let currentRegion = mapView.region
let span = currentRegion.span
let location = currentAnnotation!.coordinate
let region = MKCoordinateRegion(center: location, span: span)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
mapView.setCenter(annotation.coordinate, animated: true)
//mapView.setRegion(region, animated: true)
}
}
}
CONTINUED:
Basically I'm working on a family genealogy application that displays events from relatives on a map.
When I click an annotation (event) the details (who event belongs to, where and when, etc) pops up above with an information button to show the selected person.
I have it set up to set the MKMapView region so that the selected annotation is centered each time a new annotation is clicked.
The problem is when I click an event that is on the edge of the screen, my annotation title/description pops up off centered so that it fits on my screen because it doesn't know that I plan on re-centering the map view around said annotation.
I was wondering if there was any way to make the title/description appear centered directly above the selected annotation so that when I move the map everything is centered and fits on the screen.
Here are some screenshots of what I'm talking about:
Before and After
Solved it by calling setCenter with a slight delay in mapView(_:didSelect:):
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let annotation = view.annotation else {
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
mapView.setCenter(annotation.coordinate, animated: true)
}
}

How to remove underline from UIBarButtonItem? (Swift)

I created a UIBarButtonItem programmatically and the text is underlined. Is there a way to remove the underline?
let editButton = UIButton.init(type: .Custom)
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.title = "General Information"
editButton.setTitleColor(UIColor.blueColor(), forState: .Normal)
editButton.addTarget(self, action: #selector(editButtonPressed(_:)), forControlEvents: .TouchUpInside)
editButton.frame.size = CGSize(width: 60, height: 30)
editButton.titleLabel?.adjustsFontSizeToFitWidth = true
let barButtonItem = UIBarButtonItem.init(customView: editButton)
self.tabBarController?.navigationItem.setRightBarButtonItem(barButtonItem, animated: true)
updateEditButtonTitle()
self.navigationController!.navigationItem.backBarButtonItem?.tintColor = UIColor.blackColor()
}
here is an image of the result I get, with the underline.
here is the function where I set the button's text. when it is pressed, it becomes a save button.
func updateEditButtonTitle() {
if let button = self.tabBarController?.navigationItem.rightBarButtonItem?.customView as? UIButton {
var title = ""
editButton.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.55)
editButton.layer.cornerRadius = 7.0
if isInEditMode {
title = "Save"
editButton.setTitleColor(UIColor.redColor(), forState: .Normal)
editButton.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.5)
editButton.layer.cornerRadius = 7.0
editButton.frame.size = CGSize(width: 60, height: 30)
} else {
editButton.setTitleColor(UIColor.blueColor(), forState: .Normal)
title = "Edit"
}
button.setTitle(title, forState: .Normal)
}
}
Try this code ..
var attrStr: NSMutableAttributedString = yourBtnHere.attributedTitleForState(.Normal).mutableCopy()
//or whatever the state you want
attrStr.enumerateAttributesInRange(NSMakeRange(0, attrStr.characters.count), options: .LongestEffectiveRangeNotRequired, usingBlock: {(attributes: [NSObject : AnyObject], range: NSRange, stop: Bool) -> Void in
var mutableAttributes: [NSObject : AnyObject] = [NSObject : AnyObject](dictionary: attributes)
mutableAttributes.removeObjectForKey(.AttributeName)
attrStr.setAttributes(mutableAttributes, range: range)
})
With the inspector/IB: Select your UIButton.
Show the Attributes Inspector.
The Text settings should be in Attributed. Select the text, click on the fond item remove the Underlining setting it at none.
enter image description here
But..
Let me get this straight. Apple added an accessibility feature that lets users mark buttons with underlines if they want to.
You want a way to defeat this feature, specifically designed to help people with handicaps use their devices, when the feature is something that the user has to ask for.
Why?
It is very likely not possible using standard buttons. If you did figure out a way to do it, Apple would likely reject your app because it defeats a system function meant to help the disabled.
So the answer is: Don't do that.

swift UIButton.selected won't work

I'm trying to keep a button selected while its audio is being played. the problem is the button won't change unless I change it in a loop (while(audioPlayer.playing){button.selcted=true}). In this case I can't use the app until audio has finished(for obvious reasons)
hopefully someone can help me
func addButton(number: String, x:CGFloat, y:CGFloat){
let button = UIButton(type: UIButtonType.Custom) as UIButton
button.setTitle(number, forState: .Normal)
button.frame = CGRectMake(x, y, 68, 212)
if let image = UIImage(named: number) {
button.setImage(image, forState: .Normal)
}
button.addTarget(self, action: "play:", forControlEvents:.TouchUpInside)
self.view.addSubview(button)
}
#IBAction func play(sender: UIButton) {
button=sender
button.selected = true
let audioFilePath = NSBundle.mainBundle().pathForResource(sender.currentTitle, ofType: "WAV")
if audioFilePath != nil {
let audioFileUrl = NSURL.fileURLWithPath(audioFilePath!)
do { audioPlayer = try AVAudioPlayer(contentsOfURL: audioFileUrl, fileTypeHint: nil)} catch _ { return }
audioPlayer?.delegate = self
audioPlayer.play()
} else {
print("audio file is not found")
}
}
I just wrote up a test app to see what you are actually trying to do here, set sender.enabled = false is what you are looking for. (The button as an animation that goes to the faded out gray that is set by not enabled, then goes back to the original color of enabled, setting it to false prevents the second part from happening)
Edit: In the end, the results that were desired was an accessible button with the gray text, so we changed the text highlight state to a gray using sender.setTitleColor(UIColor.grayColor(),forState:.Highlighted) and the user will set the highlighted variable to true in the Play function.