How do I include location data using AVCapturePhoto? - ios

I'm trying to write an iOS app in Swift that will let the user take a photo, and then I'm going to overlay some additional data onto the image. I would like the images to include location data. I'm using AVCapturePhoto, and I can see in the documentation that it has some metadata variables, but I can't find any info on how to use them. When I take a photo now with my app, it has no location data in the EXIF info.
How can I set the capture sessions to embed the location data?

I haven't dealt with the new IOS 11 AVCapturePhoto object so this answer has to make a couple of assumptions about how to access data but in theory all of this should work.
Before adding location data you need to ask the user if you can use their location. Add the "Privacy - Location When In Use" tag to your Info.plist. Then add the following code somewhere in your initialisation.
// Request authorization for location manager
switch CLLocationManager.authorizationStatus() {
case .authorizedWhenInUse:
break
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
locationManagerStatus = CLLocationManager.authorizationStatus()
default:
locationManagerStatus = .denied
}
Now the following will create a dictionary with location information in it.
// create GPS metadata properties
func createLocationMetadata() -> NSMutableDictionary? {
guard CLLocationManager.authorizationStatus() == .authorizedWhenInUse else {return nil}
if let location = locationManager.location {
let gpsDictionary = NSMutableDictionary()
var latitude = location.coordinate.latitude
var longitude = location.coordinate.longitude
var altitude = location.altitude
var latitudeRef = "N"
var longitudeRef = "E"
var altitudeRef = 0
if latitude < 0.0 {
latitude = -latitude
latitudeRef = "S"
}
if longitude < 0.0 {
longitude = -longitude
longitudeRef = "W"
}
if altitude < 0.0 {
altitude = -altitude
altitudeRef = 1
}
let formatter = DateFormatter()
formatter.dateFormat = "yyyy:MM:dd"
gpsDictionary[kCGImagePropertyGPSDateStamp] = formatter.string(from:location.timestamp)
formatter.dateFormat = "HH:mm:ss"
gpsDictionary[kCGImagePropertyGPSTimeStamp] = formatter.string(from:location.timestamp)
gpsDictionary[kCGImagePropertyGPSLatitudeRef] = latitudeRef
gpsDictionary[kCGImagePropertyGPSLatitude] = latitude
gpsDictionary[kCGImagePropertyGPSLongitudeRef] = longitudeRef
gpsDictionary[kCGImagePropertyGPSLongitude] = longitude
gpsDictionary[kCGImagePropertyGPSDOP] = location.horizontalAccuracy
gpsDictionary[kCGImagePropertyGPSAltitudeRef] = altitudeRef
gpsDictionary[kCGImagePropertyGPSAltitude] = altitude
if let heading = locationManager.heading {
gpsDictionary[kCGImagePropertyGPSImgDirectionRef] = "T"
gpsDictionary[kCGImagePropertyGPSImgDirection] = heading.trueHeading
}
return gpsDictionary;
}
return nil
}
This is where I have to do some guessing as I haven't dealt with IOS 11 AVPhotoCapture. You will need a file data representation of your image data. I assume
AVCapturePhoto.fileDataRepresentation()
returns this. Also you will need the original file metadata. I'll take a guess that
AVCapturePhoto.metadata
contains this. With those assumptions the following function will give you a file data representation with additional location data. There may be newer IOS 11 methods to do this in a cleaner way.
func getFileRepresentationWithLocationData(photo : AVCapturePhoto) -> Data {
// get image metadata
var properties = photo.metadata
// add gps data to metadata
if let gpsDictionary = createLocationMetadata() {
properties[kCGImagePropertyGPSDictionary as String] = gpsDictionary
}
// create new file representation with edited metadata
return photo.fileDataRepresentation(withReplacementMetadata:properties,
replacementEmbeddedThumbnailPhotoFormat:photo.embeddedThumbnailPhotoFormat,
replacementEmbeddedThumbnailPixelBuffer:photo.previewPixelBuffer,
replacementDepthData:photo.depthData)
}

#adamfowlerphoto gave by far the best answer I could find on this subject!
AVCapturePhoto.fileDataRepresentation(withReplacementMetadata:replacementEmbeddedThumbnailPhotoFormat:replacementEmbeddedThumbnailPixelBuffer:replacementDepthData:)
was deprecated in iOS 12. For the new API you must implement the AVCapturePhotoFileDataRepresentationCustomizerProtocol with the method replacementMetadata(). Here is the updated code:
extension CameraViewController: AVCapturePhotoFileDataRepresentationCustomizer {
// create GPS metadata properties
func createLocationMetadata() -> NSMutableDictionary? {
if let location = locationManager.location {
let gpsDictionary = NSMutableDictionary()
var latitude = location.coordinate.latitude
var longitude = location.coordinate.longitude
var altitude = location.altitude
var latitudeRef = "N"
var longitudeRef = "E"
var altitudeRef = 0
if latitude < 0.0 {
latitude = -latitude
latitudeRef = "S"
}
if longitude < 0.0 {
longitude = -longitude
longitudeRef = "W"
}
if altitude < 0.0 {
altitude = -altitude
altitudeRef = 1
}
let formatter = DateFormatter()
formatter.dateFormat = "yyyy:MM:dd"
gpsDictionary[kCGImagePropertyGPSDateStamp] = formatter.string(from:location.timestamp)
formatter.dateFormat = "HH:mm:ss"
gpsDictionary[kCGImagePropertyGPSTimeStamp] = formatter.string(from:location.timestamp)
gpsDictionary[kCGImagePropertyGPSLatitudeRef] = latitudeRef
gpsDictionary[kCGImagePropertyGPSLatitude] = latitude
gpsDictionary[kCGImagePropertyGPSLongitudeRef] = longitudeRef
gpsDictionary[kCGImagePropertyGPSLongitude] = longitude
gpsDictionary[kCGImagePropertyGPSDOP] = location.horizontalAccuracy
gpsDictionary[kCGImagePropertyGPSAltitudeRef] = altitudeRef
gpsDictionary[kCGImagePropertyGPSAltitude] = altitude
if let heading = locationManager.heading {
gpsDictionary[kCGImagePropertyGPSImgDirectionRef] = "T"
gpsDictionary[kCGImagePropertyGPSImgDirection] = heading.trueHeading
}
return gpsDictionary;
}
return nil
}
func getFileRepresentationWithLocationData(photo : AVCapturePhoto) -> Data {
// get image metadata
var properties = photo.metadata
// add gps data to metadata
if let gpsDictionary = createLocationMetadata() {
properties[kCGImagePropertyGPSDictionary as String] = gpsDictionary
}
// create new file representation with edited metadata
return photo.fileDataRepresentation(with: self) ?? Data()
}
func replacementMetadata(for photo: AVCapturePhoto) -> [String : Any]? {
var properties = photo.metadata
// add gps data to metadata
if let gpsDictionary = createLocationMetadata() {
properties[kCGImagePropertyGPSDictionary as String] = gpsDictionary
}
return properties
}
}
To print the result you could use this:
static func printEXIFData(imageData: Data) {
var exifData: CFDictionary? = nil
imageData.withUnsafeBytes {
let bytes = $0.baseAddress?.assumingMemoryBound(to: UInt8.self)
if let cfData = CFDataCreate(kCFAllocatorDefault, bytes, imageData.count),
let source = CGImageSourceCreateWithData(cfData, nil) {
exifData = CGImageSourceCopyPropertiesAtIndex(source, 0, nil)
print(exifData)
}
}
}
Don't try to convert your imageData into JPEG representation because it will discard the GPS metadata. (Source: https://stackoverflow.com/a/10339278/647644)
let data = jpegData(compressionQuality: 1.0) // NOOOOO

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)
}

SwiftyJSON parse json query

I am writing a code in swift 3 for parse a query in json format result from http request.
the json format is:
JSON: {
base = stations;
coord = {
lat = "23.9";
lon = "42.89";
};
weather = (
{
description = mist;
icon = 50n;
id = 701;
main = Mist;
},
{
description = fog;
icon = 50n;
id = 741;
main = Fog;
}
);
wind = {
deg = "222.506";
speed = "1.72";
};}
My code is:
Alamofire.request(url).responseJSON { response in
if let a = response.result.value {
let jsonVar = JSON(a)
if let resDati = jsonVar["base"].string {
print(resDati as String) // <- OK
}
if let dati2 = jsonVar["weather"].array {
for item in dati2 {
print(" > \(item["main"])") // <- OK
}
}
} else {
print(Error.self)
}
}
The problem is on "coord" and "wind" data i have try:
if let dati4 = jsonVar["wind"].array {
for item in dati4 {
print("-- \(item)")
} }
I cannot print the data relatives to "wind" and "coord" in json format.
How can I resolve this.
Thank you.
The key wind contains a dictionary, not an array, you can get the deg and speed values using SwiftyJSON with this code:
if let wind = jsonVar["wind"].dictionary,
let deg = wind["deg"]?.double,
let speed = wind["speed"]?.double {
print(deg, speed)
}
coord works accordingly
if let coord = jsonVar["coord"].dictionary,
let lat = coord["lat"]?.double,
let lon = coord["lon"]?.double {
print(lat, lon)
}
Note: All values are of type Double, the json format is misleading.

Remove Object From Array Using Enumeration

I have an array of dictionaries. Each dictionary contains latitude and longitude so I'm getting the distance of each item from the current user location. If the distance in miles is greater than 20, that particular dictionary should be removed from the array. If the dictionary is not removed from the array, an annotation is created and added to an annotation array which is then used to add annotations to a map once the enumeration is finished. I'm only getting one annotation added when I should be getting three so I know I'm doing something wrong in my enumeration.
func checkDistanceAndAddPins() {
for gym in gyms {
var index = 0
let gymLatitude = gym["latitude"]!!.doubleValue
let gymLongitude = gym["longitude"]!!.doubleValue
let gymLocation = CLLocation(latitude: gymLatitude, longitude: gymLongitude)
let distance = gymLocation.distanceFromLocation(myLocation!)
let distanceInMeters = NSNumber(double: distance)
let metersDouble = distanceInMeters.doubleValue
let miles = metersDouble * 0.00062137
if miles > maxDistance {
gyms.removeAtIndex(index)
} else {
let location = CLLocationCoordinate2D(latitude: gymLatitude, longitude: gymLongitude)
gymAnnotation.title = gym["Name"] as? String
gymAnnotation.subtitle = gym["Address"] as? String
gymAnnotation.coordinate = location
gymAnnotation.gymPhoneNumber = gym["Phone"] as? String
if let website = gym["Website"] as? String {
gymAnnotation.gymWebsite = website
}
gymLocations.append(gymAnnotation)
}
index += 1
}
dispatch_async(dispatch_get_main_queue()) {
self.gymMap.addAnnotations(self.gymLocations)
}
}

How do I iterate through JSON co-ordinates and build annotations as one function?

I am struggling to get the annotations being placed using JSON data. I have tried iterating the coordinates from the JSON into a new array but when I try pass an array to where I need the coordinates it fails because it cannot take arrays. How can I fix this?
Can anyone help?
Alamofire.request(.GET, "https://demo1991046.mockable.io/score/locations").responseJSON { (responseData) -> Void in
let swiftyJsonVar = JSON(responseData.result.value!)
if let resData = swiftyJsonVar["users"].arrayObject as? [NSArray] {
self.newArray = (resData as? [NSArray])
}
print("\([self.newArray])")
for var i = 0; i < self.newArray!.count; ++i {
self.longitude.append(self.newArray[i]["lon"] as! String!)
print("longitude: \(self.longitude)")
self.latitude.append(self.newArray[i]["lat"] as! String!)
print("latitude: \(self.latitude)")
}
let doubleLat = self.latitude.map {
Double(($0 as NSString).doubleValue)
}
let doubleLon = self.longitude.map {
Double(($0 as NSString).doubleValue)
}
print("doublelat: \(doubleLat)")
print("doubleLon: \(doubleLon)")
// 1
self.locationManager.delegate = self
// 2
self.locationManager.requestAlwaysAuthorization()
// 3
let theSpan:MKCoordinateSpan = MKCoordinateSpanMake(0.01 , 0.01)
let location:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: doubleLat, longitude: doubleLon) // <- here is where I get an error: "Cannot convert value of type '[Double]' to expect argument type 'CLLocationDegrees' (aka 'Double")"
// print("lat: \((locationManager.location?.coordinate.latitude)!)")
// print("lon: \((locationManager.location?.coordinate.longitude)!)")
let theRegion:MKCoordinateRegion = MKCoordinateRegionMake(location, theSpan)
self.mapView.setRegion(theRegion, animated: true)
let anotation = MKPointAnnotation()
anotation.coordinate = location
anotation.title = "The Location"
anotation.subtitle = "This is the location !!!"
self.mapView.addAnnotation(anotation)
}
}
I have done soem modifies below to your code
Didn't convert the json to NSArray (by using .array instead of .arrayObject)
moved adding anotation to the map inside the for loop to add all of them.
Moved setting a region to the map out side the for loop and left it to you to set the location you like.
Alamofire.request(.GET, "https://demo1991046.mockable.io/score/locations").responseJSON { (responseData) -> Void in
let swiftyJsonVar = JSON(responseData.result.value!)
// get the users from the json var, no need to convert it to Array
guard let usersJsonArray = swiftyJsonVar["users"].array else {
// users not found in the json
return
}
// the usersJsonArray is array of json which will be much easier for work with.
// No need for 1,2 and 3 to be in the for loop.
// 1
self.locationManager.delegate = self
// 2
self.locationManager.requestAlwaysAuthorization()
// 3
let theSpan:MKCoordinateSpan = MKCoordinateSpanMake(0.01 , 0.01)
for userJson in usersJsonArray {
let longitudeString = userJson["lon"].stringValue
print("longitude: \(longitudeString)")
let latitudeString = userJson["lat"].stringValue
print("latitude: \(latitudeString)")
let doubleLat = Double(latitudeString)
let doubleLon = Double(longitudeString)
print("doublelat: \(doubleLat)")
print("doubleLon: \(doubleLon)")
// by having the next code block inside the for loop you will be able to add all the user locations to the map as anotations.
let location:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: doubleLat, longitude: doubleLon) // Now should work fine
let anotation = MKPointAnnotation()
anotation.coordinate = location
anotation.title = "The Location"
anotation.subtitle = "This is the location !!!"
self.mapView.addAnnotation(anotation)
} // for usersJson
// you need to figure out the loaction you will set for the mapView region.
let location = .... // set the location you like.
let theRegion:MKCoordinateRegion = MKCoordinateRegionMake(location, theSpan)
self.mapView.setRegion(theRegion, animated: true)
}

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