How to display customer callout screens on a map using Swift and Xcode

I am fairly new to IOS development, and I am working on creating a map to display different venues from a JSON file which works like a charm. Now, what I want to do is, once a user clicks a pin, I want a callout view to be displayed at the bottom of the map that shows the Venue's logo on the left, and the following on the right: a phone number that's clickable so call them directly, a website url that's clickable to access the website, and finally, a directions button to open the map to show directions to the Venue. Once a user clicks a different pin, then the information about that new venue is displayed.

Here is the portion of the code that I think needs edits, and below is the full code

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?
    {
        if let annotation = annotation as? Venue {
            let identifier = "pin"
            var view: MKPinAnnotationView
            if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView {
                dequeuedView.annotation = annotation
                view = dequeuedView
            } else {
                view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
                view.canShowCallout = true
                view.calloutOffset = CGPoint(x: -5, y: 5)
                view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure) as UIView
            }

            return view
        }

        return nil
    }

Please help me with step by step instructions. I am having a hard time getting around Swift.

THANKS A BUNCH!

import UIKit
import CoreLocation
import MapKit
//import SwiftyJSON


struct Place: Codable {
    let id: Int
    let name, address, number, imageURL: String
    let lat, long: Double

    enum CodingKeys: String, CodingKey {
        case id, name, address, number
        case imageURL = "imageUrl"
        case lat, long
    }
}


class YogaViewController: UIViewController {

    // MARK: - Properties


    var locationManager: CLLocationManager!
    var mapView: MKMapView!

    let centerMapButton: UIButton = {
        let button = UIButton(type: .system)
        button.setImage(#imageLiteral(resourceName: "location-arrow-flat").withRenderingMode(.alwaysOriginal), for: .normal)
        button.addTarget(self, action: #selector(handleCenterLocation), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()




    // MARK: - Init

    override func viewDidLoad() {
        super.viewDidLoad()
        configureLocationManager()
        configureMapView()
        enableLocationServices()



    //    mapView.addAnnotations(venues)

//         //JSON STUFF
          let jsonUrlString = "https://www.elev8dfw.com/Gyms.json"
      //  let jsonUrlString = "https://www.elev8dfw.com/Venues.json"
//   let jsonUrlString = "https://api.letsbuildthatapp.com/jsondecodable/course"

        guard let url = URL(string: jsonUrlString) else {return}

           URLSession.shared.dataTask(with: url) { (data, response, err) in

            guard let data = data else { return }

            do {
                let places = try JSONDecoder().decode([Place].self, from: data)


                for place in places {
        //            print(place.lat)
       //             print(place.long)
                    let sampleStarbucks = Venue(title: place.name, locationName: place.address, coordinate: CLLocationCoordinate2D(latitude: place.lat, longitude: place.long))
self.mapView.addAnnotation(sampleStarbucks)
                }

            } catch let jsonErr{
                print("Error serializing json: ", jsonErr)
            }

        }.resume()

               mapView.delegate = self
    }

    // MARK: - Selectors

    @objc func handleCenterLocation() {
        centerMapOnUserLocation()
        centerMapButton.alpha = 0
    }

    // MARK: - Helper Functions

    func configureLocationManager() {
        locationManager = CLLocationManager()
        locationManager.delegate = self
    }

    func configureMapView() {
        mapView = MKMapView()
        mapView.showsUserLocation = true
        mapView.delegate = self
        mapView.userTrackingMode = .follow

        view.addSubview(mapView)
        mapView.frame = view.frame

        view.addSubview(centerMapButton)
        centerMapButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100).isActive = true
        centerMapButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20).isActive = true
        centerMapButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
        centerMapButton.widthAnchor.constraint(equalToConstant: 50).isActive = true
        centerMapButton.layer.cornerRadius = 50 / 2
        centerMapButton.alpha = 0
    }

    func centerMapOnUserLocation() {
        guard let coordinate = locationManager.location?.coordinate else { return }
        let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 4000, longitudinalMeters: 4000)
        mapView.setRegion(region, animated: true)
    }
}

// MARK: - MKMapViewDelegate

extension YogaViewController: MKMapViewDelegate {

   func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
        UIView.animate(withDuration: 0.5) {
            self.centerMapButton.alpha = 1
        }
    }

     func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?
    {
        if let annotation = annotation as? Venue {
            let identifier = "pin"
            var view: MKPinAnnotationView
            if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView {
                dequeuedView.annotation = annotation
                view = dequeuedView
            } else {
                view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
                view.canShowCallout = true
                view.calloutOffset = CGPoint(x: -5, y: 5)
                view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure) as UIView
            }

            return view
        }

        return nil
    }
    func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
        let location = view.annotation as! Venue
        let launchOptions = [MKLaunchOptionsDirectionsModeKey : MKLaunchOptionsDirectionsModeDriving]
        location.mapItem().openInMaps(launchOptions: launchOptions)
    }

}

// MARK: - CLLocationManagerDelegate

extension YogaViewController: CLLocationManagerDelegate {

    func enableLocationServices() {
        switch CLLocationManager.authorizationStatus() {
        case .notDetermined:
            print("Location auth status is NOT DETERMINED")
            locationManager.requestWhenInUseAuthorization()
            locationManager.startUpdatingLocation()
            centerMapOnUserLocation()
        case .restricted:
            print("Location auth status is RESTRICTED")
        case .denied:
            print("Location auth status is DENIED")
       case .authorizedAlways:
            print("Location auth status is AUTHORIZED ALWAYS")
        case .authorizedWhenInUse:
            print("Location auth status is AUTHORIZED WHEN IN USE")
            locationManager.startUpdatingLocation()
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
            centerMapOnUserLocation()
        }

    }
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        self.mapView.showsUserLocation = true
    }
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        guard locationManager.location != nil else { return }
        centerMapOnUserLocation()
    }


}

1 answer

  • answered 2020-01-14 02:23 DrewG23

    If you are attempting to open a custom UIView coming up from the bottom upon tapping a annotation you could utilize didSelect and do something like the following:

    final func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
        let location = view.annotation as! Venue
    
        // Update your new custom view with the Venue here.
    
        // Now lets show the view
        self.bottomViewConstraint.constant = 0
    
        UIView.animate(withDuration: 0.2) {
             self.view.layoutIfNeeded()
        }
    }
    

    So in your storyboard. Create a UIView, set its height and width constraints. Set its bottom constraint to be a negative number so it is not showing on the screen. Create an IBOutlet to the bottom constraint (I named it bottomViewConstraint in the example code). This is a very basic example, untested, to get you started.