I have been having a real tough time making multiple annotations on a MapView. So far I have been able to create the relevant classes to download, parse and store the data into an array that can be used. Yet I am still struggling to use the said data and make the annotations required.
HomeModel Class - download and parse the required information from the server
import UIKit
import Foundation
protocol HomeModelProtocol: class {
func itemsDownloaded(items: NSArray)
}
class HomeModel: NSObject, URLSessionDataDelegate {
weak var delegate: HomeModelProtocol!
var data = Data()
let urlPath: String = "https://FAKEDATABASEURL.XYZ"
func downloadItems() {
let url: URL = URL(string: urlPath)!
let defaultSession = Foundation.URLSession(configuration: URLSessionConfiguration.default)
let task = defaultSession.dataTask(with: url) { (data, response, error) in
if error != nil {
print("Failed to download data")
} else {
print("Data downloaded")
self.parseJSON(data!)
}
}
task.resume()
}
func parseJSON(_ data:Data) {
var jsonResult = NSArray()
do {
jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as! NSArray
} catch let error as NSError {
print(error)
}
var jsonElement = NSDictionary()
let locations = NSMutableArray()
for i in 0 ..< jsonResult.count {
jsonElement = jsonResult[i] as! NSDictionary
let location = LocationModel()
if let name = jsonElement["Name"] as? String,
let address = jsonElement["Address"] as? String,
let latitude = jsonElement["Latitude"] as? String,
let longitude = jsonElement["Longitude"] as? String {
location.name = name
location.address = address
location.latitude = latitude
location.longitude = longitude
}
locations.add(location)
}
DispatchQueue.main.async(execute: { ()-> Void in
self.delegate.itemsDownloaded(items: locations)
})
}
}
LocalModel Class - To store the data into an array to be used by the app
import UIKit
import Foundation
class LocationModel: NSObject {
// Properties
var name: String?
var address: String?
var latitude: String?
var longitude: String?
// Empty constructor
override init() { }
// Construct with #name, #address, #latitude and #longitude.
init(name: String, address: String, latitude: String, longitude: String) {
self.name = name
self.address = address
self.latitude = latitude
self.longitude = longitude
}
// Print the object's current state
override var description: String {
return "Name: \(String(describing: name)), Address:\(String(describing: address)), Latitude: \(String(describing: latitude)), Longitude: \(String(describing: longitude))"
}
}
Map View Controller - Controls the map for the application
import UIKit
import MapKit
import CoreLocation
class HotPlacesViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
var isFirstTime = true
var locationManager = CLLocationManager()
let newPin = MKPointAnnotation()
var selectedLocation:LocationModel?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// Setup the location services delegate in this class.
locationManager.delegate = self
// This little method requests the users permission for location services whilst in this view controller.
if CLLocationManager.authorizationStatus() == .notDetermined {
self.locationManager.requestAlwaysAuthorization()
let alert = UIAlertController(title: "You can change this option in the Settings App", message: "So keep calm your selection is not permanent. 🙂",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
// Drops the pin on the users current location.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
mapView.removeAnnotation(newPin)
let location = locations.last! as CLLocation
let center = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
if(self.isFirstTime) {
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
// Set the region on the map.
mapView.setRegion(region, animated: true)
self.isFirstTime = false
}
newPin.coordinate = location.coordinate
mapView.addAnnotation(newPin)
}
}
Make your let locations = NSMutableArray() array accessible by other classes so that you can use this array in your HotPlacesViewController class.
Then in your HotPlacesViewController class declare a property for holding locations data. And load the data inside viewDidLoad() like:
class HotPlacesViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {
// declare a property which will hold the locations data
override func viewDidLoad() {
...
...
locations = // load your data here
}
}
Then, for multiple annotations follow this logic:
for location in locations {
let annotation = MKPointAnnotation()
annotation.title = location.name
annotation.coordinate = CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)
mapView.addAnnotation(annotation)
}
Related
I am creating my first IOS app and am having a very difficult time understanding why the CalloutAccessoryControlTapped Control function is not working.
The Info button is appearing on the annotation, but when the user taps on the Info button, nothing happens. See screenshot here: Screenshot of Annotation.
I created a function in the View Controller to add the Callout accessory view and suspect there is something wrong with the function.
Any help would be greatly appreciated!
Here is the Function:
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
let annotation = view.annotation as? Fire
let status = "Fire Status: Active"
let distance = "Fire Location: 25 miles away"
let ac = UIAlertController(title: status, message: distance, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Close", style: .default))
self.present(ac, animated: true, completion: nil)
}
Here is the code from my View Controller -- updated 4/20/2021:
import MapKit
import UIKit
import CoreLocation
class FireMapViewController: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var FireMapView: MKMapView!
let mapView = MKMapView()
var locationManager = CLLocationManager()
var lat = Double()
var lon = Double()
var fires: [Fire] = []
override func viewDidLoad() {
super.viewDidLoad()
setupMapView()
checkLocationServices()
mapView.register(FireMarkerView.self,forAnnotationViewWithReuseIdentifier:
MKMapViewDefaultAnnotationViewReuseIdentifier)
//Initiate URL Session to get Fire data and add Fire data to the Map Annotations
if let url = URL(string: "https://services3.arcgis.com/T4QMspbfLg3qTGWY/arcgis/rest/services/Active_Fires/FeatureServer/0/query?outFields=*&where=1%3D1&f=geojson") {
URLSession.shared.dataTask(with: url) {data, response, error in
if let data = data {
do {
let features = try MKGeoJSONDecoder().decode(data)
.compactMap { $0 as? MKGeoJSONFeature }
let validWorks = features.compactMap(Fire.init)
self.fires.append(contentsOf: validWorks)
DispatchQueue.main.async {
self.mapView.addAnnotations(self.fires)
}
}
catch let error {
print(error)
}
}
}.resume()
}
}
func setupMapView() {
view.addSubview(mapView)
mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
mapView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
mapView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
}
func checkLocationServices() {
if CLLocationManager.locationServicesEnabled() {
setupLocationManager()
locationManagerDidChangeAuthorization(locationManager)
} else {
// the user didn't turn it on
}
}
//Get the current location permissions for User
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .authorizedAlways , .authorizedWhenInUse:
mapView.showsUserLocation = true
followUserLocation()
locationManager.startUpdatingLocation()
break
case .notDetermined , .denied , .restricted:
locationManager.requestWhenInUseAuthorization()
break
default:
break
}
switch manager.accuracyAuthorization {
case .fullAccuracy:
break
case .reducedAccuracy:
break
default:
break
}
}
func setupLocationManager() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
let region = MKCoordinateRegion.init(center: location.coordinate, latitudinalMeters: 160934, longitudinalMeters: 160934)
mapView.setRegion(region, animated: true)
print("Fire Map User Location on Map:", location.coordinate)
// Call stopUpdatingLocation() to stop listening for location updates,
// other wise this function will be called every time when user location changes.
// Need a solution for this.
manager.stopUpdatingLocation()
}
func followUserLocation() {
if let location = locationManager.location?.coordinate {
let region = MKCoordinateRegion.init(center: location, latitudinalMeters: 4000, longitudinalMeters: 4000)
mapView.setRegion(region, animated: true)
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
locationManagerDidChangeAuthorization(locationManager)
}
//Function to display additional Fire data after User selects the Callout Info button on the Annotation
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
let annotation = view.annotation as? Fire
print("callout Accessory Tapped!")
let status = "Fire Status: Active"
let distance = "Fire Location: 25 miles away"
let ac = UIAlertController(title: status, message: distance, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Close", style: .default))
self.present(ac, animated: true, completion: nil)
}
}
Here is the Class for the Annotation:
import Foundation
import MapKit
class FireMarkerView: MKAnnotationView {
override var annotation: MKAnnotation? {
willSet {
guard let fire = newValue as? Fire else {
return
}
canShowCallout = true
calloutOffset = CGPoint(x: -5, y: 5)
rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
image = fire.image
}
}
}
Here is the Class for the Model:
import Foundation
import MapKit
class Fire: NSObject, MKAnnotation {
let title: String?
let county: String?
let city: String?
let incidentTypeCategory: String?
let coordinate: CLLocationCoordinate2D
let percentContained: Int?
let lastUpdateDateTime: Int?
init(
title: String?,
county: String?,
city: String?,
incidentTypeCategory: String?,
coordinate: CLLocationCoordinate2D,
percentContained: Int?,
lastUpdateDateTime: Int?
) {
self.title = title
self.county = county
self.city = city
self.incidentTypeCategory = incidentTypeCategory
self.coordinate = coordinate
self.percentContained = percentContained
self.lastUpdateDateTime = lastUpdateDateTime
super.init()
}
init?(feature: MKGeoJSONFeature) {
// 1
guard
let point = feature.geometry.first as? MKPointAnnotation,
let propertiesData = feature.properties,
let json = try? JSONSerialization.jsonObject(with: propertiesData),
let properties = json as? [String: Any]
else {
return nil
}
// 2
title = ((properties["IncidentName"] as? String ?? "Unknown") + " Wildfire")
county = ((properties["POOCounty"] as? String ?? "Unknown") + " County")
city = properties["POOCity"] as? String
incidentTypeCategory = properties["IncidentTypeCategory"] as? String
coordinate = point.coordinate
percentContained = properties["PercentContained"] as? Int
lastUpdateDateTime = properties["ModifiedOnDateTime_dt"] as? Int
super.init()
}
var subtitle: String? {
return (county)
}
var image: UIImage {
guard let name = incidentTypeCategory else {
return #imageLiteral(resourceName: "RedFlame")
}
switch name {
case "RX":
return #imageLiteral(resourceName: "YellowFlame")
default:
return #imageLiteral(resourceName: "RedFlame")
}
}
}
I am creating my first IOS app and am not a developer and am really stuck with Map Annotations.
I am trying to get Fire data from a GeoJSON URL end point and display the fires as Annotations on a Map using URLSession and a custom MKAnnotationView and custom fire Pins.
The problem is the Annotations with the GeoJSON Fire data from the URL end point are not appearing on the Map, although data is being returned by the URL session. However, if I manually create a single Fire annotation it is appearing correctly on the map with the custom pin.
Any help would be immensely appreciated, I have spent days trying to figure this out :(
Here is the ViewController.Swift file
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
var locationManager:CLLocationManager!
var lat = Double()
var lon = Double()
var fires: [Fire] = []
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
mapView.register(FireMarkerView.self,forAnnotationViewWithReuseIdentifier:
MKMapViewDefaultAnnotationViewReuseIdentifier)
if let url = URL(string: "https://services3.arcgis.com/T4QMspbfLg3qTGWY/arcgis/rest/services/Active_Fires/FeatureServer/0/query?outFields=*&where=1%3D1&f=geojson") {
URLSession.shared.dataTask(with: url) {data, response, error in
if let data = data {
do {
let features = try MKGeoJSONDecoder().decode(data)
.compactMap { $0 as? MKGeoJSONFeature }
let validWorks = features.compactMap(Fire.init)
self.fires.append(contentsOf: validWorks)
print([self.fires])
}
catch let error {
print(error)
}
}
}.resume()
}
//This code works an annotation appears correctly on map
/* let fire = Fire(
title: "Ford Fire",
incidentShortDescription: "Hwy 35",
incidentTypeCategory: "WF",
coordinate: CLLocationCoordinate2D(latitude: 37.7993, longitude: -122.1947))
mapView.addAnnotation(fire)*/
mapView.addAnnotations(fires)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
determineMyCurrentLocation()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .authorizedAlways , .authorizedWhenInUse:
mapView.showsUserLocation = true
followUserLocation()
locationManager.startUpdatingLocation()
break
case .notDetermined , .denied , .restricted:
locationManager.requestWhenInUseAuthorization()
break
default:
break
}
switch manager.accuracyAuthorization {
case .fullAccuracy:
break
case .reducedAccuracy:
break
default:
break
}
}
func followUserLocation() {
if let location = locationManager.location?.coordinate {
let region = MKCoordinateRegion.init(center: location, latitudinalMeters: 4000, longitudinalMeters: 4000)
mapView.setRegion(region, animated: true)
}
}
func determineMyCurrentLocation() {
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.startUpdatingLocation()
//locationManager.startUpdatingHeading()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
let userLocation = locations.first! as CLLocation
lat = userLocation.coordinate.latitude
lon = userLocation.coordinate.longitude
let region = MKCoordinateRegion.init(center: location.coordinate, latitudinalMeters: 400000, longitudinalMeters: 400000)
self.mapView.setRegion(region, animated: true)
// Call stopUpdatingLocation() to stop listening for location updates,
// other wise this function will be called every time when user location changes.
// Need a solution for this.
manager.stopUpdatingLocation()
print("user latitude = \(userLocation.coordinate.latitude)")
print("user longitude = \(userLocation.coordinate.longitude)")
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error)
{
print("Error \(error)")
}
}
Here is the Model Class, Fire.swift
import Foundation
import MapKit
class Fire: NSObject, MKAnnotation {
let title: String?
let incidentShortDescription: String?
let incidentTypeCategory: String?
let coordinate: CLLocationCoordinate2D
init(
title: String?,
incidentShortDescription: String?,
incidentTypeCategory: String?,
coordinate: CLLocationCoordinate2D
) {
self.title = title
self.incidentShortDescription = incidentShortDescription
self.incidentTypeCategory = incidentTypeCategory
self.coordinate = coordinate
super.init()
}
init?(feature: MKGeoJSONFeature) {
// 1
guard
let point = feature.geometry.first as? MKPointAnnotation,
let propertiesData = feature.properties,
let json = try? JSONSerialization.jsonObject(with: propertiesData),
let properties = json as? [String: Any]
else {
return nil
}
// 3
title = properties ["IncidentName"] as? String
incidentShortDescription = properties["IncidentShortDescription"] as? String
incidentTypeCategory = properties["IncidentTypeCategory"] as? String
coordinate = point.coordinate
super.init()
}
var subtitle: String? {
return (incidentTypeCategory)
}
var image: UIImage {
guard let name = incidentTypeCategory else {
return #imageLiteral(resourceName: "RedFlame")
}
switch name {
case "RX":
return #imageLiteral(resourceName: "YellowFlame")
default:
return #imageLiteral(resourceName: "RedFlame")
}
}
Here is the custom MKAnnotation Class: FileMarkerView.swift
import Foundation
import MapKit
class FireMarkerView: MKAnnotationView {
override var annotation: MKAnnotation? {
willSet {
guard let fire = newValue as? Fire else {
return
}
canShowCallout = true
calloutOffset = CGPoint(x: -5, y: 5)
rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
image = fire.image
}
}
}
URLSession.shared.dataTask is an asynchronous task, meaning it calls its callback function at some indeterminate time in the future. Code executed outside of its callback (the { }) will end up getting called before the data task has actually completed. Right now, you're setting the annotations outside of that callback.
To solve this, you need to set the annotations inside of that callback function. So, where you have print([self.fires]), you can do:
DispatchQueue.main.async {
self.mapView.addAnnotations(self.fires)
}
The DispatchQueue.main.async is to make sure that an update to the UI gets called on the main thread (the URL task may return on a different thread).
I tried to show the place that I want in mapView by insering latitude and longitude, but I failed to do that and the map show me always my place where I am and not the place that I want to get
these is the code that I used
class MapViewController: UIViewController, CLLocationManagerDelegate{
#IBOutlet weak var map: MKMapView!
let manager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
map.showsPointsOfInterest = true
map.showsUserLocation = true
manager.requestAlwaysAuthorization()
manager.requestWhenInUseAuthorization()
//user location stuff
if CLLocationManager.locationServicesEnabled() {
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.startUpdatingLocation()
}
}
func locationManager(_manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
let span:MKCoordinateSpan = MKCoordinateSpanMake(0.01, 0.01)
let location = CLLocationCoordinate2D(latitude: 36.1070, longitude: -112.1130)
let region:MKCoordinateRegion = MKCoordinateRegionMake(location, span)
map.setRegion(region, animated: true)
self.map.showsUserLocation = true
}
}
Comment this
map.showsUserLocation = true
And put the code of didUpdateLocations inside viewDidLoad
class MapViewController: UIViewController, CLLocationManagerDelegate{
#IBOutlet weak var map: MKMapView!
let manager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
map.showsPointsOfInterest = true
//map.showsUserLocation = true
manager.requestAlwaysAuthorization()
manager.requestWhenInUseAuthorization()
//user location stuff
if CLLocationManager.locationServicesEnabled() {
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.startUpdatingLocation()
}
let span:MKCoordinateSpan = MKCoordinateSpanMake(0.01, 0.01)
let location = CLLocationCoordinate2D(latitude: 36.1070, longitude: -112.1130)
let region:MKCoordinateRegion = MKCoordinateRegionMake(location, span)
map.setRegion(region, animated: true)
}
func locationManager(_manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
///
}
}
To have a more reliable and efficient result,
I recommend using .plist (property list) file to store the latitude and longitude dynamically it would be much easier and would cost you less time.
plist contains the pins annotation directions which are basically an XML text file that holds the essential configuration information for bundle execution.
here is how to attach it to your project:
In your main view controller write the function:
func fetchAllData(){
if let path = Bundle.main.path(forResource: "NAME OF YOUR PLIST FILE", ofType: "plist") {
////If your plist contain root as Dictionary
if let dic = NSDictionary(contentsOfFile: path) as? [String: Any] {
let keys=dic.keys
for dataOfKey in keys {
if let object=dic[dataOfKey] as? [[String:Any]]{
locationsArray.append(contentsOf: object)
}
}
}
for location in self.locationsArray{
let newPin = MKPointAnnotation()
newPin.coordinate = CLLocationCoordinate2D.init(latitude: Double(location["latitude"] as! String)!, longitude: Double(location["longitude"] as! String)!)
self.mapView.addAnnotation(newPin)
}
}
}
When I add the annotations to the map they sometimes show and sometimes not depending on how close they are to each other. If they are in the same house lets say one won't show. How do I make both of them show? Do I need to make a custom annotation class? I heard ios11 has a clumping feature, do I need to use that? Here is the code(abridged):
import UIKit
import MapKit
import Firebase
class GameViewController: UIViewController {
#IBOutlet weak var mapView: MKMapView!
fileprivate var locations = [CLLocation]()
fileprivate var userLocations = [(loc: CLLocation, name: String, team: String)]()
fileprivate var userAnnotations = [MKAnnotation]()
fileprivate var hasBeenUP = false
var ref: FIRDatabaseReference!
let uid = FIRAuth.auth()!.currentUser!.uid
var timer = Timer()
var timeLeft = 0.0
var firstTimer = Timer()
var name = ""
var team = ""
override func viewDidLoad() {
super.viewDidLoad()
let center = CLLocationCoordinate2D(latitude: 47.786769, longitude: -20.413634)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
self.mapView.setRegion(region, animated: true)
mapView.mapType = .hybrid
locationManager.startUpdatingLocation()
ref = FIRDatabase.database().reference()
setupULSending()
getMetaInfo()
ref.child("realtimeLocations").observe(FIRDataEventType.value, with: { (snapshot) in
self.userLocations = []
for rest in snapshot.children.allObjects as! [FIRDataSnapshot] {
guard let snapshotValue = snapshot.value as? NSDictionary, let snapVal = snapshotValue[rest.key] as? NSDictionary else {
break
}
let name = snapVal["name"] as! String
let team = snapVal["team"] as? String ?? ""
if let lat = snapVal["lat"] as? Double,
let long = snapVal["long"] as? Double {
let location = CLLocation(latitude: lat, longitude: long)
self.userLocations.append((loc: location, name: name, team: team))
}else {
}
}
DispatchQueue.main.async {
self.updateUserLocation()
}
})
}
private lazy var locationManager: CLLocationManager = {
let manager = CLLocationManager()
manager.allowsBackgroundLocationUpdates = true
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
manager.requestAlwaysAuthorization()
return manager
}()
func updateUserLocation() {
for an in self.mapView.annotations {
mapView.removeAnnotation(an)
}
for loc in userLocations {
let annotation = MKPointAnnotation()
annotation.coordinate = loc.loc.coordinate
annotation.title = loc.name
annotation.subtitle = "local"
mapView.addAnnotation(annotation)
}
}
}
// MARK: - CLLocationManagerDelegate
extension GameViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations
locations: [CLLocation]) {
let location = locations.last as! CLLocation
self.locations.append(location)
}
}
On the MKAnnotationView, you have to set the MKFeatureDisplayPriority to 'required'. You can modify the annotation views by implementing MKMapViewDelegate and mapView(MKMapView, viewFor: MKAnnotation). Something like this:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
var view = mapView.dequeueReusableAnnotationView(withIdentifier: "yourIdentifier")
if view == nil {
view = MKMarkerAnnotationView(annotation: nil, reuseIdentifier: "yourIdentifier")
}
view?.displayPriority = .required
return view
}
More options for this are explained in the WWDC 2017 video 237 "What's New in MapKit"
So I am trying to create a clone of Uber using Firebase but I keep getting an error in my MapViewController this is my code:
import UIKit
import MapKit
import CoreLocation
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var callUberBtn: UIButton!
#IBOutlet weak var myMap: MKMapView!
private var locationManager = CLLocationManager();
private var userLocation: CLLocationCoordinate2D?;
private var driverLocation: CLLocationCoordinate2D?;
private var timer = Timer();
private var canCallUber = true;
private var riderCanceledRequest = false;
private var appStartedForTheFirstTime = true;
override func viewDidLoad() {
super.viewDidLoad()
initializeLocationManager();
UberHandler.Instance.observeMessagesForRider();
UberHandler.Instance.delegate = self; //Cannot assign value of type 'mapViewController' to type 'UberController?'
}
private func initializeLocationManager() {
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
locationManager.requestWhenInUseAuthorization();
locationManager.startUpdatingLocation();
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// if we have the coordinates from the manager
if let location = locationManager.location?.coordinate {
userLocation = CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)
let region = MKCoordinateRegion(center: userLocation!, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01));
myMap.setRegion(region, animated: true);
myMap.removeAnnotations(myMap.annotations);
if driverLocation != nil {
if !canCallUber {
let driverAnnotation = MKPointAnnotation();
driverAnnotation.coordinate = driverLocation!;
driverAnnotation.title = "Driver Location";
myMap.addAnnotation(driverAnnotation);
}
}
let annotation = MKPointAnnotation();
annotation.coordinate = userLocation!;
annotation.title = "Drivers Location";
myMap.addAnnotation(annotation);
}
}
func updateRidersLocation() {
UberHandler.Instance.updateRiderLocation(lat: userLocation!.latitude, long: userLocation!.longitude);
}
func canCallUber(delegateCalled: Bool) {
if delegateCalled {
callUberBtn.setTitle("Cancel Uber", for: UIControlState.normal);
canCallUber = false;
} else {
callUberBtn.setTitle("Call Uber", for: UIControlState.normal);
canCallUber = true;
}
}
func driverAcceptedRequest(requestAccepted: Bool, driverName: String) {
if !riderCanceledRequest {
if requestAccepted {
alertTheUser(title: "Uber Accepted", message: "\(driverName) Accepted Your Uber Request")
} else {
UberHandler.Instance.cancelUber();
timer.invalidate();
alertTheUser(title: "Uber Canceled", message: "\(driverName) Canceled Uber Request")
}
}
riderCanceledRequest = false;
}
func updateDriversLocation(lat: Double, long: Double) {
driverLocation = CLLocationCoordinate2D(latitude: lat, longitude: long);
}
#IBAction func callUber(_ sender: Any) {
if userLocation != nil {
if canCallUber {
UberHandler.Instance.requestUber(latitude: Double(userLocation!.latitude), longitude: Double(userLocation!.longitude))
timer = Timer.scheduledTimer(timeInterval: TimeInterval(10), target: self, selector: #selector(MapViewController.updateRidersLocation), userInfo: nil, repeats: true);
} else {
riderCanceledRequest = true;
UberHandler.Instance.cancelUber();
timer.invalidate();
}
}
}
private func alertTheUser(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert);
let ok = UIAlertAction(title: "OK", style: .default, handler: nil);
alert.addAction(ok);
present(alert, animated: true, completion: nil);
}
}
UberController? was declared in another Swift file and this is the code for that:
import Foundation
import FirebaseDatabase
protocol UberController: class {
func canCallUber(delegateCalled: Bool);
func driverAcceptedRequest(requestAccepted: Bool, driverName: String);
func updateDriversLocation(lat: Double, long: Double);
}
class UberHandler {
private static let _instance = UberHandler();
weak var delegate: UberController?;
var rider = "";
var driver = "";
var rider_id = "";
static var Instance: UberHandler {
return _instance;
}
func observeMessagesForRider() {
// RIDER REQUESTED UBER
DBProvider.Instance.requestRef.observe(DataEventType.childAdded) { (snapshot: DataSnapshot) in
if let data = snapshot.value as? NSDictionary {
if let name = data[Constants.NAME] as? String {
if name == self.rider {
self.rider_id = snapshot.key;
self.delegate?.canCallUber(delegateCalled: true);
}
}
}
}
// RIDER CANCELED UBER
DBProvider.Instance.requestRef.observe(DataEventType.childRemoved) { (snapshot: DataSnapshot) in
if let data = snapshot.value as? NSDictionary {
if let name = data[Constants.NAME] as? String {
if name == self.rider {
self.delegate?.canCallUber(delegateCalled: false);
}
}
}
}
// DRIVER ACCEPTED UBER
DBProvider.Instance.requestAcceptedRef.observe(DataEventType.childAdded) { (snapshot: DataSnapshot) in
if let data = snapshot.value as? NSDictionary {
if let name = data[Constants.NAME] as? String {
if self.driver == "" {
self.driver = name;
self.delegate?.driverAcceptedRequest(requestAccepted: true, driverName: self.driver);
}
}
}
}
// DRIVER CANCELED UBER
DBProvider.Instance.requestAcceptedRef.observe(DataEventType.childRemoved) { (snapshot:DataSnapshot) in
if let data = snapshot.value as? NSDictionary {
if let name = data[Constants.NAME] as? String {
if name == self.driver {
self.driver = "";
self.delegate?.driverAcceptedRequest(requestAccepted: false, driverName: name);
}
}
}
}
// DRIVER UPDATING LOCATION
DBProvider.Instance.requestAcceptedRef.observe(DataEventType.childChanged) { (snapshot: DataSnapshot) in
if let data = snapshot.value as? NSDictionary {
if let name = data[Constants.NAME] as? String {
if name == self.driver {
if let lat = data[Constants.LATITUDE] as? Double {
if let long = data[Constants.LONGITUDE] as? Double {
self.delegate?.updateDriversLocation(lat: lat, long: long);
}
}
}
}
}
}
}
func requestUber(latitude: Double, longitude: Double) {
let data: Dictionary<String, Any> = [Constants.NAME: rider, Constants.LATITUDE: latitude, Constants.LONGITUDE: longitude];
DBProvider.Instance.requestRef.childByAutoId().setValue(data);
} // request uber
func cancelUber() {
DBProvider.Instance.requestRef.child(rider_id).removeValue();
}
func updateRiderLocation(lat: Double, long: Double) {
DBProvider.Instance.requestRef.child(rider_id).updateChildValues([Constants.LATITUDE: lat, Constants.LONGITUDE: long]);
}
}
My error is: Cannot assign value of type 'mapViewController' to type 'UberController?' in the mapViewController. I don't know what I'm doing wrong. Any ideas?
You have to declare your MapViewController to conform to UberController protocol, just like you declare conformance to MapViewDelegate and CLLocationManagerDelegate protocols.
Either:
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate, UberController {
...
}
Or, even better:
class MapViewController: UIViewController {
...
}
extension MapViewController: MKMapViewDelegate {
// MKMapViewDelegate methods here
}
extension MapViewController: CLLocationManagerDelegate {
// CLLocationManagerDelegate methods here
}
extension MapViewController: UberController {
// UberController methods here
}