Mapview - protection against coordinates found nil - ios

I have mapview that downloads records off CloudKit. The coordinates of each record is based on forward geocoder, where users add the address (ex: New York, NY) and lats and lons are obtained
Current Model is as follow:
class Place: NSObject
{
var name: String
var address: String
var comment: String?
var photo: UIImage?
var rating: Int
var location: CLLocation?
var identifier: String
var record: CKRecord!
init(record: CKRecord)
{
self.record = record
self.name = record.valueForKey(placeName) as! String
self.address = record.valueForKey(placeAddress) as! String
self.comment = record.valueForKey(placeComment) as? String
if let photoAsset = record.valueForKey(placePhoto) as? CKAsset
{
self.photo = UIImage(data: NSData(contentsOfURL: photoAsset.fileURL)!)
}
self.rating = record.valueForKey(placeRating) as! Int
self.location = record.valueForKey(placeLocation) as? CLLocation
self.identifier = record.recordID.recordName
}
// MARK: Map Annotation
var coordinate: CLLocationCoordinate2D {
get {
return location!.coordinate
}
}
This is my method to place each pin on the mapview.
func placePins()
{
for place: Place in self.places
{
let location = CLLocationCoordinate2DMake(place.coordinate.latitude, place.coordinate.longitude)
let dropPin = CustomPointAnnotation(place: place)
dropPin.pinCustomImageName = "customPin"
dropPin.coordinate = location
dropPin.title = place.title
dropPin.subtitle = place.subtitle
dropPin.name = place.name
dropPin.image = place.photo
mapView.addAnnotation(dropPin)
}
}
How do i fix them to protect against any record that doesn't have coordinates since forward geocoder is not the most reliable way?

What about
for place: Place in self.places
{
if (place.location == nil) {
continue;
}
...
}
Not sure what is the issue there

Related

I get an empty CLLocationCoordinates array when loading data from user defaults

I'm trying to store to UserDefaults an array of CCLocationCoordinates from the tracking portion of my app paired with the name of the tracked route as key, to be able to recall it later on to use it within a function.
The problem is that when I call that function I get the index out of range error. I checked and the array is empty.
As I'm new to user defaults I tried to see other similar posts but they're all about NSUserDefaults and didn't find a solution.
Heres the code for the functions for storing and recalling the array:
func stopTracking2() {
self.trackingIsActive = false
self.trackigButton.backgroundColor = UIColor.yellow
locationManager.stopUpdatingLocation()
let stopRoutePosition = RouteAnnotation(title: "Route Stop", coordinate: (locationManager.location?.coordinate)!, imageName: "Route Stop")
self.actualRouteInUseAnnotations.append(stopRoutePosition)
print(actualRouteInUseCoordinatesArray)
print(actualRouteInUseAnnotations)
drawRoutePolyline() // draw line to show route
// checkAlerts2() // check if there is any notified problem on our route and marks it with a blue circle, now called at programmed checking
saveRouteToUserDefaults()
postRouteToAnalitics() // store route anonymously to FIrebase
}
func saveRouteToUserDefaults() {
// save actualRouteInUseCoordinatesArray : change for function
// userDefaults.set(actualRouteInUseCoordinatesArray, forKey: "\(String(describing: userRoute))")
storeCoordinates(actualRouteInUseCoordinatesArray)
}
// Store an array of CLLocationCoordinate2D
func storeCoordinates(_ coordinates: [CLLocationCoordinate2D]) {
let locations = coordinates.map { coordinate -> CLLocation in
return CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
}
let archived = NSKeyedArchiver.archivedData(withRootObject: locations)
userDefaults.set(archived, forKey: "\(String(describing: userRoute))")
userDefaults.synchronize()
}
func loadRouteFromUserDefaults() {
// gets entry from userRouteArray stored in userDefaults and append them into actualRouteInUseCoordinatesArray
actualRouteInUseCoordinatesArray.removeAll()
actualRouteInUseCoordinatesArray = userDefaults.object(forKey: "\(String(describing: userRoute))") as? [CLLocationCoordinate2D] ?? [CLLocationCoordinate2D]() // here we get the right set of coordinates for the route we are about to do the check on
// load route coordinates from UserDefaults
// actualRouteInUseCoordinatesArray = loadCoordinates()! //error found nil
}
// Return an array of CLLocationCoordinate2D
func loadCoordinates() -> [CLLocationCoordinate2D]? {
guard let archived = userDefaults.object(forKey: "\(String(describing: userRoute))") as? Data,
let locations = NSKeyedUnarchiver.unarchiveObject(with: archived) as? [CLLocation] else {
return nil
}
let coordinates = locations.map { location -> CLLocationCoordinate2D in
return location.coordinate
}
return coordinates
}
}
extension NewMapViewController {
// ALERTS :
func checkAlerts2() {
loadRouteFromUserDefaults() //load route coordinates to check in
// CHECK IF ANY OBSTACLE IS OUN OUR ROUTE BY COMPARING DISTANCES
while trackingCoordinatesArrayPosition != ( (actualRouteInUseCoordinatesArray.count) - 1) {
print("checking is started")
print(actualRouteInUseCoordinatesArray)
let trackingLatitude = actualRouteInUseCoordinatesArray[trackingCoordinatesArrayPosition].latitude
let trackingLongitude = actualRouteInUseCoordinatesArray[trackingCoordinatesArrayPosition].longitude
let alertLatitude = alertNotificationCoordinatesArray[alertNotificationCoordinatesArrayPosition].latitude
let alertLongitude = alertNotificationCoordinatesArray[alertNotificationCoordinatesArrayPosition].longitude
let coordinateFrom = CLLocation(latitude: trackingLatitude, longitude: trackingLongitude)
let coordinateTo = CLLocation(latitude: alertLatitude, longitude: alertLongitude)
let coordinatesDistanceInMeters = coordinateFrom.distance(from: coordinateTo)
// CHECK SENSITIVITY: sets the distance in meters for an alert to be considered an obstacle
if coordinatesDistanceInMeters <= 10 {
print( "found problem")
routeObstacle.append(alertNotificationCoordinatesArray[alertNotificationCoordinatesArrayPosition]) // populate obstacles array
trackingCoordinatesArrayPosition = ( trackingCoordinatesArrayPosition + 1)
}
else if alertNotificationCoordinatesArrayPosition < ((alertNotificationCoordinatesArray.count) - 1) {
alertNotificationCoordinatesArrayPosition = alertNotificationCoordinatesArrayPosition + 1
}
else if alertNotificationCoordinatesArrayPosition == (alertNotificationCoordinatesArray.count - 1) {
trackingCoordinatesArrayPosition = ( trackingCoordinatesArrayPosition + 1)
alertNotificationCoordinatesArrayPosition = 0
}
}
findObstacles()
NewMapViewController.checkCounter = 0
displayObstacles()
}
In the extension you can see the function that uses the array.
Right after the print of the array I get the index out of range error.
Thanks as usual to the community.
After trying various solutions offered I decided to rewrite the whole thing.
So after finding a post on how to code/decode my array to string I decided it was the way to go. It shouldn't be heavy on the system as it's a string that gets saved. Please let me know what you think of this solution.
Thank to #Sh_Khan to point out it was a decoding issue, and to #Moritz to point out I was performing a bad practice.
So the code is:
func storeRoute() {
// first we code the CLLocationCoordinate2D array to string
// second we store string into userDefaults
userDefaults.set(encodeCoordinates(coords: actualRouteInUseCoordinatesArray), forKey: "\(String(describing: NewMapViewController.userRoute))")
}
func loadRoute() {
//first se load string from user defaults
let route = userDefaults.string(forKey: "\(String(describing: NewMapViewController.userRoute))")
print("loaded route is \(route!))")
//second we decode it into CLLocationCoordinate2D array
actualRouteInUseCoordinatesArray = decodeCoordinates(encodedString: route!)
print("decoded route array is \(actualRouteInUseCoordinatesArray))")
}
func encodeCoordinates(coords: [CLLocationCoordinate2D]) -> String {
let flattenedCoords: [String] = coords.map { coord -> String in "\(coord.latitude):\(coord.longitude)" }
let encodedString: String = flattenedCoords.joined(separator: ",")
return encodedString
}
func decodeCoordinates(encodedString: String) -> [CLLocationCoordinate2D] {
let flattenedCoords: [String] = encodedString.components(separatedBy: ",")
let coords: [CLLocationCoordinate2D] = flattenedCoords.map { coord -> CLLocationCoordinate2D in
let split = coord.components(separatedBy: ":")
if split.count == 2 {
let latitude: Double = Double(split[0]) ?? 0
let longitude: Double = Double(split[1]) ?? 0
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
} else {
return CLLocationCoordinate2D()
}
}
return coords
}
Rather than using heavy-weight objectiv-c-ish NSKeyed(Un)Archiver and making a detour via CLLocation I recommend to extend CLLocationCoordinate2D to adopt Codable
extension CLLocationCoordinate2D : Codable {
public init(from decoder: Decoder) throws {
var arrayContainer = try decoder.unkeyedContainer()
if arrayContainer.count == 2 {
let lat = try arrayContainer.decode(CLLocationDegrees.self)
let lng = try arrayContainer.decode(CLLocationDegrees.self)
self.init(latitude: lat, longitude: lng)
} else {
throw DecodingError.dataCorruptedError(in: arrayContainer, debugDescription: "Coordinate array must contain two items")
}
}
public func encode(to encoder: Encoder) throws {
var arrayContainer = encoder.unkeyedContainer()
try arrayContainer.encode(contentsOf: [latitude, longitude])
}
}
and replace the methods to load and save data with
func storeCoordinates(_ coordinates: [CLLocationCoordinate2D]) throws {
let data = try JSONEncoder().encode(coordinates)
UserDefaults.standard.set(data, forKey: String(describing: userRoute))
}
func loadCoordinates() -> [CLLocationCoordinate2D] {
guard let data = UserDefaults.standard.data(forKey: String(describing: userRoute)) else { return [] }
do {
return try JSONDecoder().decode([CLLocationCoordinate2D].self, from: data)
} catch {
print(error)
return []
}
}
storeCoordinates throws it hands over a potential encoding error
Load the data with
actualRouteInUseCoordinatesArray = loadCoordinates()
and save it
do {
try storeCoordinates(actualRouteInUseCoordinatesArray)
} catch { print(error) }
Your problem is that you save it as data and try to read directly without unarchiving , You can try
let locations = [CLLocation(latitude: 123, longitude: 344),CLLocation(latitude: 123, longitude: 344),CLLocation(latitude: 123, longitude: 344)]
do {
let archived = try NSKeyedArchiver.archivedData(withRootObject: locations, requiringSecureCoding: true)
UserDefaults.standard.set(archived, forKey:"myKey")
// read savely
if let data = UserDefaults.standard.data(forKey: "myKey") {
let saved = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! [CLLocation]
print(saved)
}
}
catch {
print(error)
}

Strange behaviour in showing annotations images on map using data coming from Firebase. SWIFT 4.1

The strange behaviour is that when I add a new annotation, either tapped or user location, it gets displayed with the right chosen icon. When MapVC load for the first time, the posts retrieved from Firebase have all the same icon, ( the icon name of the latest one posted. If, after posting a new one, I exit mapViewVc to the menuVC and re enter mapViewVC than every icon is displaying the same icon again, now being my previously posted one.
a Few times it happened the the icons were two different icons, randomly chosen.
I don't understand why the coordinates are taken right but the image is not.
The app flow is:
I have a mapView vc where I can either double tap on screen and get coordinate or code user location coordinate via a button and then get to an chooseIconVc where I have all available icons to choose for the annotation. Once I select one, the icon name get passed back in in mapViewVC in unwindHere() that stores icon name into a variable and coordinates into another. In postAlertNotification those variables get posted to Firebase.
In displayAlerts() the data from Firebase gets stored into variables to initialise an annotation and gets added to mapView.
chosen icon:
#IBAction func unwindHere(sender:UIStoryboardSegue) { // data coming back
if let sourceViewController = sender.source as? IconsViewController {
alertNotificationType = sourceViewController.dataPassed
if tapCounter > 0 {
alertNotificationLatitude = String(describing: alertCoordinates.latitude)
alertNotificationLongitude = String(describing: alertCoordinates.longitude)
postAlertNotification() // post new notification to Firebase
} else {
alertCoordinates = self.trackingCoordinates
alertNotificationLatitude = String(describing: self.trackingCoordinates!.latitude)
alertNotificationLongitude = String(describing: self.trackingCoordinates!.longitude)
postAlertNotification() // post new notification to Firebase
}
}
}
than post:
func postAlertNotification() {
// to set next notification id as the position it will have in array ( because first position is 0 ) we use the array.count as value
let latitude = alertNotificationLatitude
let longitude = alertNotificationLongitude
let alertType = alertNotificationType
let post: [String:String] = [//"Date" : date as! String,
//"Time" : time as! String,
"Latitude" : latitude as! String,
"Longitude" : longitude as! String,
"Description" : alertType as! String]
var ref: DatabaseReference!
ref = Database.database().reference()
ref.child("Community").child("Alert Notifications").childByAutoId().setValue(post)
}
retrieve and display:
func displayAlerts() {
ref = Database.database().reference()
databaseHandle = ref?.child("Community").child("Alert Notifications").observe(.childAdded, with: { (snapshot) in
// defer { self.dummyFunctionToFoolFirebaseObservers() }
guard let data = snapshot.value as? [String:String] else { return }
guard let firebaseKey = snapshot.key as? String else { return }
// let date = data!["Date"]
// let time = data!["Time"]
let dataLatitude = data["Latitude"]!
let dataLongitude = data["Longitude"]!
self.alertIconToDisplay = data["Description"]!
let doubledLatitude = Double(dataLatitude)
let doubledLongitude = Double(dataLongitude)
let recombinedCoordinate = CLLocationCoordinate2D(latitude: doubledLatitude!, longitude: doubledLongitude!)
print("Firebase post retrieved !")
print("Longitude Actual DataKey is \(String(describing: firebaseKey))")
print("fir long \((snapshot.value!, snapshot.key))")
self.userAlertAnnotation = UserAlert(type: self.alertIconToDisplay!, coordinate: recombinedCoordinate, firebaseKey: firebaseKey)
self.mapView.addAnnotation(self.userAlertAnnotation)
})
}
and
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let annotationView = MKAnnotationView(annotation: userAlertAnnotation, reuseIdentifier: "") // CHANGE FOR NEW ANNOTATION : FULL DATA
//added if statement for displaying user location blue dot
if annotation is MKUserLocation{
return nil
} else {
annotationView.image = UIImage(named: alertIconToDisplay!) // choose the image to load
let transform = CGAffineTransform(scaleX: 0.27, y: 0.27)
annotationView.transform = transform
return annotationView
}
}
the variables declarations :
var alertIconToDisplay: String?
var userAlertAnnotation: UserAlert!
var alertNotificationType: String?
var alertNotificationLatitude: String?
var alertNotificationLongitude: String?
UPDATE:
annotation cLass:
import MapKit
class UserAlert: NSObject , MKAnnotation {
var type: String?
var firebaseKey: String?
var coordinate = CLLocationCoordinate2D()
var image: UIImage?
override init() {
}
init(type:String, coordinate:CLLocationCoordinate2D, firebaseKey: String) {
self.type = type
self.firebaseKey = firebaseKey
self.coordinate = coordinate
}
}
After understanding where the problem I was explained how to changed the displayAlert() into
func displayAlerts() { // rajish version
ref = Database.database().reference()
databaseHandle = ref?.child("Community").child("Alert Notifications").observe(.childAdded, with: { (snapshot) in
// defer { self.dummyFunctionToFoolFirebaseObservers() }
guard let data = snapshot.value as? [String:String] else { return }
guard let firebaseKey = snapshot.key as? String else { return }
// let date = data!["Date"]
// let time = data!["Time"]
let dataLatitude = data["Latitude"]!
let dataLongitude = data["Longitude"]!
let type = data["Description"]!
let id = Int(data["Id"]!)
let doubledLatitude = Double(dataLatitude)
let doubledLongitude = Double(dataLongitude)
let recombinedCoordinate = CLLocationCoordinate2D(latitude: doubledLatitude!, longitude: doubledLongitude!)
print("Firebase post retrieved !")
print("Longitude Actual DataKey is \(String(describing: firebaseKey))")
print("fir long \((snapshot.value!, snapshot.key))")
var userAlertAnnotation = UserAlert(type: type, coordinate: recombinedCoordinate, firebaseKey: firebaseKey, title: type,id: id!)
self.userAlertNotificationArray.append(userAlertAnnotation) // array of notifications coming from Firebase
print("user alert array after append from Firebase is : \(self.userAlertNotificationArray)")
self.alertNotificationArray.append(recombinedCoordinate) // array for checkig alerts on route
self.mapView.addAnnotation(userAlertAnnotation)
})
}
and the mapView to:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { // rajish version
let annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "")
if annotation is MKUserLocation{
return nil
} else {
print(annotation.coordinate)
annotationView.image = UIImage(named:(annotationView.annotation?.title)! ?? "")
// annotationView.canShowCallout = true
let transform = CGAffineTransform(scaleX: 0.27, y: 0.27)
annotationView.transform = transform
return annotationView
}
}
that solved it.

how can I create objects of my class in swift based on json fetched by swiftyJson?

I have a class as follows:
import Foundation
import MapKit
class SingleRequest: NSObject, MKAnnotation {
var title: String?
let created: String
let discipline: String
let coordinate: CLLocationCoordinate2D
var items = NSMutableArray()
init(title: String, created: String, discipline: String, coordinate: CLLocationCoordinate2D) {
self.title = title
self.created = created
self.discipline = discipline
self.coordinate = coordinate
super.init()
}
}
I also have a json that looks like:
[{"_id":"56c9d44fbcb42e075f7d49b1",
"username":"Ms. Brynlee Quitzon DDS",
"photo":"photo.jpg",
"number":"one",
"description":"Maiores rerum beatae molestiae autem. Voluptatem magni aspernatur est voluptas.",
"__v":0,
"updated_at":"2016-02-21T15:14:23.123Z",
"created_at":"2016-02-21T15:14:23.116Z",
"location":{
"type":"Point",
"coordinates":[5.300567929507009,44.04127433959841]}
},
etc.
and now I want to fetch all json entries and create SingleRequest object for each of them.
So far I created a method in this class:
class func getAllRequests() {
print("getAllRequests")
RestApiManager.sharedInstance.getRequests { json in
let results = json//["username"]
for (index: String, subJson: JSON) in results {
let user: AnyObject = JSON.object
var title = user["description"]
let created = user["created_at"]
let discipline = user["number"]
let latitude = (user[""]).doubleValue
let longitude = (user[""]).doubleValue
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
return SingleRequest(title: title, created: created!, discipline: discipline!, coordinate: coordinate)
}
}
}
And now 2 questions:
1) as you can see above, I left those two fields empty:
let latitude = (user[""]).doubleValue
let longitude = (user[""]).doubleValue
that's because I don't know how to refer to the long/lat values from my json, since they are embeded in the coordinates field...
How can I fill it?
2) will this function create needed objects? or should I for example change the declaration to mark some return value:
class func getAllRequests()
? Thanks!
For your first question, you need to first get the array out of user["coordinates"] and then downcast it to Array, user["coordinates"] as? Array<Double>
For your second question, it should return an array of SingleRequest, Array<SingleRequest>
class func getAllRequests() -> Array<SingleRequest> {
var requests: Array<SingleRequest> = []
RestApiManager.sharedInstance.getRequests { json in
let results = json//["username"]
for (index: String, subJson: JSON) in results {
let user: AnyObject = JSON.object
var title = user["description"]
let created = user["created_at"]
let discipline = user["number"]
guard let coordinates = user["coordinates"] as? Array<Double> else { print("no lat/long") }
let latitude = coordinates[0]
let longitude = coordinates[1]
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
requests.append(SingleRequest(title: title, created: created!, discipline: discipline!, coordinate: coordinate))
}
}
return requests
}
var singleRequestsArray = [SingleRequest]()
class func getAllRequests() {
RestApiManager.sharedInstance.getRequests { json in
for (index: String, subJson: JSON) in son {
let user: AnyObject = subjson.object
let title = user["description"]
let created = user["created_at"]
let discipline = user["number"]
var coordinate = CLLocationCoordinate2D()
if let coordinates = user["coordinates"].array{
let latitude = coordinates.first?).doubleValue
let longitude = coordinates.array.last?).doubleValue
coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
singleRequestsArray.append(SingleRequest(title: title, created: created!, discipline: discipline!, coordinate: coordinate))
}
}
}
Hope this will help you!

How can I rewrite this function so that it uses SwiftyJSON instead of JSON.swift?

I'm looking at the Ray Wenderlich tutorial http://www.raywenderlich.com/90971/introduction-mapkit-swift-tutorial and he is using there this function:
class func fromJSON(json: [JSONValue]) -> Artwork? {
// 1
var title: String
if let titleOrNil = json[16].string {
title = titleOrNil
} else {
title = ""
}
let locationName = json[12].string
let discipline = json[15].string
// 2
let latitude = (json[18].string! as NSString).doubleValue
let longitude = (json[19].string! as NSString).doubleValue
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
// 3
return Artwork(title: title, locationName: locationName!, discipline: discipline!, coordinate: coordinate)
}
Since I'm using SwiftyJSON in my project I would like to stay with that, so I thought about rewriting this function based on that.
If I understand correctly, this function takes one json node and creates Artwork object from it.
So how can I refer to a single json node with SwiftyJSON?
I tried doing:
class func fromJSON(JSON_: (data: dataFromNetworking))->Artwork?{
}
but it causes error use of undeclared type dataFromNetworking. On the other hand that's exactly how they use it in the documentation https://github.com/SwiftyJSON/SwiftyJSON
Could you help me with rewriting it?
My suggestion: separate the model layer from the presentation layer.
ArtworkModel
First of all you need a way to represent the data. A struct is perfect for this.
struct ArtworkModel {
let title: String
let locationName: String
let discipline: String
let coordinate: CLLocationCoordinate2D
init?(json:JSON) {
guard let
locationName = json[12].string,
discipline = json[15].string,
latitudeString = json[18].string,
latitude = Double(latitudeString),
longitueString = json[19].string,
longitude = Double(longitueString) else { return nil }
self.title = json[16].string ?? ""
self.locationName = locationName
self.discipline = discipline
self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
}
As you can see ArtworkModel is capable to initialize itself from a json.
The presentation layer
Now the Artwork (conform to MKAnnotation) becomes much easier.
class Artwork: NSObject, MKAnnotation {
private let artworkModel: ArtworkModel
init(artworkModel: ArtworkModel) {
self.artworkModel = artworkModel
super.init()
}
var title: String? { return artworkModel.title }
var subtitle: String? { return artworkModel.locationName }
var coordinate: CLLocationCoordinate2D { return artworkModel.coordinate }
}
Usage
You function now becomes
class func fromJSON(json: JSON) -> Artwork? {
guard let model = ArtworkModel(json: json) else { return nil }
return Artwork(artworkModel: model)
}
To use SwiftyJSON in this project first you have to change the method to retrieve the data from the property list file.
Note: This replacement is for Swift 2.
Replace the method loadInitialData() in ViewController with
func loadInitialData() {
do {
let fileName = NSBundle.mainBundle().pathForResource("PublicArt", ofType: "json")
let data = try NSData(contentsOfFile: fileName!, options: NSDataReadingOptions())
let jsonObject = JSON(data:data)
if let jsonData = jsonObject["data"].array {
for artworkJSON in jsonData {
if let artworkJSONArray = artworkJSON.array, artwork = Artwork.fromJSON(artworkJSONArray) {
artworks.append(artwork)
}
}
}
} catch let error as NSError {
print(error)
}
}
And then just exchange [JSONValue] in the method
class func fromJSON(json: [JSONValue]) -> Artwork? {
of the Artworkclass with [JSON], so it's now
class func fromJSON(json: [JSON]) -> Artwork? {
That's it.

Swift 2 MapKit Annotations - How to group annotations?

I have close to 8.000 annotations in my map and I'd like to group them depending of the zoom that the user do in the app.
All the latitudes and longitudes are already into the CoreData as Double.
Case I have two different annotation images in the same point, I'd like to show two groups, one with the cross and one with the heart.
When the user click over the annotation and if this annotation is a grouped annotation, I'd like to show to the user how many annotations has at this location, "forcing" the user to zoom in to see the annotations individually.
Bellow are the images of my app and the current annotations.
Thank you!
I already got it.
var zoomLevel = Double()
var iphoneScaleFactorLatitude = Double()
var iphoneScaleFactorLongitude = Double()
var CanUpdateMap: Bool = false
static func getLatitudeLongitudeLimitsFromMap(mapView: MKMapView) -> [String: Double] {
var coord = [String: Double]()
let MinLat: Double = mapView.region.center.latitude - (mapView.region.span.latitudeDelta / 2)
let MaxLat: Double = mapView.region.center.latitude + (mapView.region.span.latitudeDelta / 2)
let MinLon: Double = mapView.region.center.longitude - (mapView.region.span.longitudeDelta / 2)
let MaxLon: Double = mapView.region.center.longitude + (mapView.region.span.longitudeDelta / 2)
coord["MinLat"] = MinLat
coord["MaxLat"] = MaxLat
coord["MinLon"] = MinLon
coord["MaxLon"] = MaxLon
return coord
}
func LoadMap(mapView: MKMapView) {
// Get the limits after move or resize the map
let coord: [String: Double] = getLatitudeLongitudeLimitsFromMap(mapView)
let MinLat: Double = coord["MinLat"]! as Double
let MaxLat: Double = coord["MaxLat"]! as Double
let MinLon: Double = coord["MinLon"]! as Double
let MaxLon: Double = coord["MaxLon"]! as Double
var arrAnnotations = [MKAnnotation]()
let FilterMinLat = arrDicListPinsWithLatitudeLongitude.filter({
if let item = $0["Latitude"] as? Double {
return item > MinLat
} else {
return false
}
})
let FilterMaxLat = FilterMinLat.filter({
if let item = $0["Latitude"] as? Double {
return item < MaxLat
} else {
return false
}
})
let FilterMinLon = FilterMaxLat.filter({
if let item = $0["Longitude"] as? Double {
return item > MinLon
} else {
return false
}
})
let FilterMaxLon = FilterMinLon.filter({
if let item = $0["Longitude"] as? Double {
return item < MaxLon
} else {
return false
}
})
for Item in FilterMaxLon {
let dic:[String:AnyObject] = Item
var Name = String()
var Address = String()
var IconPNG = String()
if let Latitude = dic["Latitude"] as? Double {
if let Longitude = dic["Longitude"] as? Double {
if let item = dic["Name"] {
Name = item as! String
}
if let item = dic["Address"] {
Address = item as! String
}
if let item = dic["TypeID"] as? Int {
if item == 11 {
IconPNG = "icon-cross.png"
} else {
IconPNG = "icon-heart.png"
}
}
arrAnnotations.append(CreateAnnotation(Address, Title: Name, Latitude: Latitude, Longitude: Longitude, IconPNG: IconPNG))
}
}
}
}
// Show in the map only the annotations from that specific region
iphoneScaleFactorLatitude = mapView.region.center.latitude
iphoneScaleFactorLongitude = mapView.region.center.longitude
if zoomLevel != mapView.region.span.longitudeDelta {
filterAnnotations(arrAnnotations)
zoomLevel = mapView.region.span.longitudeDelta
CanUpdateMap = true
}
}
func filterAnnotations(arrAnnotations: [MKAnnotation]) {
let latDelta: Double = 0.04 / iphoneScaleFactorLatitude
let lonDelta: Double = 0.04 / iphoneScaleFactorLongitude
var shopsToShow = [AnyObject]()
var arrAnnotationsNew = [MKAnnotation]()
for var i = 0; i < arrAnnotations.count; i++ {
let checkingLocation: MKAnnotation = arrAnnotations[i]
let latitude: Double = checkingLocation.coordinate.latitude
let longitude: Double = checkingLocation.coordinate.longitude
var found: Bool = false
for tempPlacemark: MKAnnotation in shopsToShow as! [MKAnnotation] {
if fabs(tempPlacemark.coordinate.latitude - latitude) < fabs(latDelta) && fabs(tempPlacemark.coordinate.longitude - longitude) < fabs(lonDelta) {
found = true
}
}
if !found {
shopsToShow.append(checkingLocation)
arrAnnotationsNew.append(checkingLocation)
}
}
// Clean the map
for item: MKAnnotation in self.mapRedes.annotations {
myMap.removeAnnotation(item)
}
// Add new annotations to the map
for item: MKAnnotation in arrAnnotationsNew {
myMap.addAnnotation(item)
}
}
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// This validation should be added, because it will find all the annotations before the map resize
if CanUpdateMap == true {
LoadMap(mapView)
}
}
Just a correction to my code:
//....
func LoadMap(mapView: MKMapView) {
//....
// Show in the map only the annotations from that specific region
iphoneScaleFactorLatitude = Double(mapView.bounds.size.width / 30) // 30 = width of the annotation
iphoneScaleFactorLongitude = Double(mapView.bounds.size.height / 30) // 30 = height of the annotation
//....
}
func filterAnnotations(mapView: MKMapView, arrAnnotations: [MKAnnotation]) {
let latDelta: Double = mapView.region.span.longitudeDelta / iphoneScaleFactorLatitude
let lonDelta: Double = mapView.region.span.longitudeDelta / iphoneScaleFactorLongitude
//....
}
//....

Resources