Tom
Tom

Reputation: 197

MapKit map returns nil inside of function being called inside of different ViewController

I'm currently trying to implement a Map connected with a search function. For the overlay containing the table view, I've decided to go for a library called FloatingPanel.

I have to ViewControllers, namely MapViewController and SearchTableViewController - as the name already says, MapViewController contains a mapView. I assume since FloatingPanel adds SearchTableViewController (STVC) to MapViewController (MVC), that STVC is MVC's child.

Now whenever I want to call the MapViewController's function to add annotation inside of SearchTableViewController, MapViewController's mapView returns nil - calling it inside of MapViewController works fine.

class MapViewController: UIViewController, FloatingPanelControllerDelegate, UISearchBarDelegate {

    var fpc: FloatingPanelController!
    var searchVC = SearchResultTableViewController()

    let locationManager = CLLocationManager()
    let regionInMeters: Double = 10000

    @IBOutlet private var mapView: MKMapView!

    var mapItems: [MKMapItem]?

    override func viewDidLoad() {
        super.viewDidLoad()

        checkLocationServices()
        fpc = FloatingPanelController()
        fpc.delegate = self

        fpc.surfaceView.backgroundColor = .clear
        fpc.surfaceView.cornerRadius = 9.0
        fpc.surfaceView.shadowHidden = false

        searchVC = (storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as! SearchResultTableViewController)

        fpc.set(contentViewController: searchVC)
        fpc.track(scrollView: searchVC.tableView)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        fpc.addPanel(toParent: self, animated: true)
        fpc.move(to: .tip, animated: true)

        searchVC.searchController.searchBar.delegate = self
    }

    func checkLocationServices() {
        if CLLocationManager.locationServicesEnabled() {
            setupLocationManager()
            checkLocationAuthorization()
        } else {
        }
    }

    func setupLocationManager() {
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
    }

    func checkLocationAuthorization() {
        switch CLLocationManager.authorizationStatus() {
        case .authorizedWhenInUse:
            centerViewOnUserLocation()
            break
        case .denied:
            break
        case .notDetermined:
            locationManager.requestWhenInUseAuthorization()
            break
        case .restricted:
            break
        case .authorizedAlways:
            break
        @unknown default:
            break
        }
    }

    func centerViewOnUserLocation() {
        if let location = locationManager.location?.coordinate {
            let region = MKCoordinateRegion.init(center: location, latitudinalMeters: regionInMeters, longitudinalMeters: regionInMeters)
            mapView.setRegion(region, animated: true)
        }
    }

    @IBAction func click(_ sender: Any) {

    }

    func addPin(title: String, subtitle: String, coordinates: CLLocationCoordinate2D) {
        let destination = customPin(pinTitle: title, pinSubTitle: subtitle, location: coordinates)
        mapView.addAnnotation(destination)
    }

    func addAnnotationToMap() {
        guard let item = mapItems?.first else { return }
        guard let coordinates = item.placemark.location?.coordinate else { return }

        addPin(title: item.name!, subtitle: "", coordinates: coordinates)
    }
}

SearchTableViewController's function:

func passData() {
        guard let mapViewController = storyboard?.instantiateViewController(withIdentifier: "map") as? MapViewController else { return }

        guard let mapItem = places?.first else { return }

        mapViewController.mapItems = [mapItem]
        mapViewController.addAnnotationToMap()
}

Upvotes: 1

Views: 72

Answers (3)

Duncan C
Duncan C

Reputation: 131408

Several things:

  1. When you use a third party library in your project and your readers might want to know about that library in order to understand your problem, you should include a link to the library in your question. I did a Google search and was able to find what I think is the correct library.

  2. The sample code for that library has you call fpc.addPanel() in your view controller's viewDidLoad(), not in viewDidAppear().

  3. The viewDidLoad() function is only called once in the lifetime of a view controller, but viewDidAppear() is called every time a view controller get's re-shown (like when it is redisplayed after being covered by a modal and then uncovered again.) The two are not interchangeable for that reason. I suggest moving that call back to viewDidLoad().

  4. Next, as others have mentioned, your SearchTableViewController's passData() function is wrong. It creates a new, throw-away MapViewController every time it is called. It does not talk to the hosting MapViewController at all.

You should refactor your viewDidLoad() to set up the MapViewController as the delegate of the SearchTableViewController.

Define a protocol (possibly in a separate file

protocol SearchTableViewControllerDelegate {
   var mapItems: [MapItem] //Or whatever type
   func addAnnotationToMap()
}

Some changes to MapViewController

class MapViewController: 
  SearchTableViewControllerDelegate,
  UIViewController, 
  FloatingPanelControllerDelegate, 
  UISearchBarDelegate {

  //Your other code...
}
override func viewDidLoad() {
    super.viewDidLoad()

    checkLocationServices()
    fpc = FloatingPanelController()
    fpc.delegate = self

    fpc.surfaceView.backgroundColor = .clear
    fpc.surfaceView.cornerRadius = 9.0
    fpc.surfaceView.shadowHidden = false

    searchVC = (storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as! SearchResultTableViewController)

    //--------------------------
    searchVC.delegate = self  //This new line is important
    //--------------------------

    fpc.set(contentViewController: searchVC)
    fpc.track(scrollView: searchVC.tableView)
    fpc.addPanel(toParent: self)  //Probably can't be animated at this point
}

And in SearchTableViewController:

class SearchTableViewController: UITableViewController, <Other protocols> {
    weak var delegate: SearchTableViewControllerDelegate?

    // other code...

    func passData() {
        guard let mapItem = places?.first else { return }

        delegate?.mapItems = [mapItem]
        delegate?.addAnnotationToMap()
    }
}

Upvotes: 3

COSMO BAKER
COSMO BAKER

Reputation: 457

You're instantiating a new MapViewController instead if passing data to the one that exists. There are three ways to do this:

  1. Delegation
  2. Closures
  3. Notifications

An example using a closure, in SearchViewController:

var passData: ((MKMapItem) -> ())?

In MapViewController provide it a closure:

searchVC = (storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as! SearchResultTableViewController)
searchVC.passData = { mapItem in 
  self.mapItems = [mapItem]
}

In SearchViewController call the closure:

passData?(mapItem)

Upvotes: 1

Shehata Gamal
Shehata Gamal

Reputation: 100503

This

 guard let mapViewController = storyboard?.instantiateViewController(withIdentifier: "map") as? MapViewController else { return } 
 guard let mapItem = places?.first else { return }

creates a new seperate object other than the actual one that you have to access , use delegate to do it here

searchVC = (storyboard?.instantiateViewController(withIdentifier: "SearchPanel") as! SearchResultTableViewController)
searchVC.delegate = self // here
fpc.set(contentViewController: searchVC) 

then declare

weak var delegate:MapViewController?

inside SearchViewController and use it

func passData() {  
    guard let mapItem = places?.first else { return } 
    delegate?.mapItems = [mapItem]
    delegate?.addAnnotationToMap()
}

Upvotes: 3

Related Questions