Sort NSFetchRequest by calculation result - ios

So I have a dataset of points with latitude and longitude values and I want to retrieve them and sort them by the distance to the user's current location. Currently, I have the following:
mainMoc.performBlockAndWait {
// Fetching data from CoreData
let fetchRequest = NSFetchRequest()
fetchRequest.predicate = NSPredicate(format: "pointLatitude BETWEEN {%f,%f} AND pointLongitude BETWEEN {%f,%f}", (latitude-0.03), (latitude+0.03), (longitude-0.03), (longitude+0.03))
let entity = NSEntityDescription.entityForName("PointPrimary", inManagedObjectContext: self.mainMoc)
fetchRequest.entity = entity
let sortDescriptor = NSSortDescriptor(key: "pointTitle", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
points = try self.mainMoc.executeFetchRequest(fetchRequest) as! [PointPrimary]
} catch {
let jsonError = error as NSError
NSLog("\(jsonError), \(jsonError.localizedDescription)")
abort()
}
}
So currently I'm only sorting it based on the title. But how would I proceed if I wanted to calculate the distance to say a CLLocationCoordinate2D and sort the fetchRequest results based on that?
Thanks a bunch!

NSFetchRequest can't use properties other than those defined in the model (and stored in the database) as sort descriptors, so you'll have to resort to in-memory sorting. Define a distance method on your PointPrimary class that performs the appropriate computation and do:
let sortedPoints = points.sortedArrayUsingDescriptors([NSSortDescriptor(key: "distance", ascending: true)])

This should work. Basically you'll use an NSSortDescriptor with custom comparator.
The trick is to use "self" as key for the NSSortDescriptor, which will pass the fetched objects into the comparator.
var userLocation : CLLocation // get that from somewhere
var distanceCompare : NSComparator = {
(obj1: AnyObject!, obj2: AnyObject!) -> NSComparisonResult in
let lng1 = obj1.valueForKey("pointLongitude") as! CLLocationDegrees
let lat1 = obj1.valueForKey("pointLatitude") as! CLLocationDegrees
let p1Location : CLLocation = CLLocation(latitude: lat1, longitude: lng1)
let p1DistanceToUserLocation = userLocation.distanceFromLocation(p1Location)
let lng2 = obj2.valueForKey("pointLongitude") as! CLLocationDegrees
let lat2 = obj2.valueForKey("pointLatitude") as! CLLocationDegrees
let p2Location : CLLocation = CLLocation(latitude: lat1, longitude: lng1)
let p2DistanceToUserLocation = userLocation.distanceFromLocation(p2Location)
if (p1DistanceToUserLocation > p2DistanceToUserLocation) {
return .OrderedDescending
} else if (p1DistanceToUserLocation < p2DistanceToUserLocation) {
return .OrderedAscending
} else {
return .OrderedSame
}
}
var distanceSortDescriptor = NSSortDescriptor(key: "self", ascending: true, comparator: distanceCompare)
fetchRequest.sortDescriptors = [distanceSortDescriptor]

Related

Why is "self" Blocking Data from CloudKit in my QueryOperation

I have multiple CKRecords in my database in Cloudkit. I turn those CKRecords into annotations for my map. Ever since I had to add self before my variable var annotation = MKPointAnnotation() in my queryoperation, it has only been loading one annotation to my Map. Why is that and how do I fix that??? Any help would be amazing!
How I fetch the records -
var points: [MKPointAnnotation] = []
var annotation = MKPointAnnotation()
let database = CKContainer.default().publicCloudDatabase
func fetchTruck() {
let truePredicate = NSPredicate(value: true)
let eventQuery = CKQuery(recordType: "User", predicate: truePredicate)
let queryOperation = CKQueryOperation(query: eventQuery)
queryOperation.recordFetchedBlock = { (record) in
self.points.append(self.annotation)
self.annotation.title = record["username"] as? String
self.annotation.subtitle = record["hours"] as? String
if let location = record["location"] as? CLLocation {
self.annotation.coordinate = location.coordinate
}
print("recordFetchedBlock: \(record)")
self.mapView.addAnnotation(self.annotation)
}
self.database.add(queryOperation)
}
After reviewing in more detail your code I think that the problem is that you are using always the same annotation. MKPointAnnotation is a class, a reference value, that means that every time you assing a value to self.annotation you're changing the reference, not creating a new one.
You're modifiying your app UI (mapView) inside the CKQueryOperation closure. Try to run the modification code in the main thread
Try something like...
var points: [MKPointAnnotation] = []
let database = CKContainer.default().publicCloudDatabase
func fetchTruck()
{
let truePredicate = NSPredicate(value: true)
let eventQuery = CKQuery(recordType: "User", predicate: truePredicate)
let queryOperation = CKQueryOperation(query: eventQuery)
queryOperation.recordFetchedBlock = { (record) in
var annotation = MKPointAnnotation()
annotation.title = record["username"] as? String
annotation.subtitle = record["hours"] as? String
if let location = record["location"] as? CLLocation
{
annotation.coordinate = location.coordinate
}
self.points.append(annotation)
DispatchQueue.main.async
{
self.mapView.addAnnotation(annotation)
}
print("recordFetchedBlock: \(record)")
}
self.database.add(queryOperation)
}

How to Make an Array for Annotations

Not all my annotations will show up in my map because I can't make var annotation = MKPoinatAnnotation the same MKPointAnnotation in fetching the CKrecords(annotation), and in getting directions for an annotation. I'm confused on how to make an array for annotations so I can be able to load all my annotations from the CloudKit database and be able to get directions when the annotation is selected.
let annotation = MKPointAnnotation()
let database = CKContainer.default().publicCloudDatabase
var truck: [CKRecord] = []
func fetch() {
let truePredicate = NSPredicate(value: true)
let eventQuery = CKQuery(recordType: "User", predicate: truePredicate)
let queryOperation = CKQueryOperation(query: eventQuery)
queryOperation.recordFetchedBlock = { (record : CKRecord!) in
self.truck.append(record)
self.annotation.title = record["username"] as? String
self.annotation.subtitle = record["hours"] as? String
if let location = record["location"] as? CLLocation {
self.annotation.coordinate = location.coordinate
}
print("recordFetchedBlock: \(record)")
self.mapView.addAnnotation(self.annotation)
}
self.database.add(queryOperation)
}
How I get directions -
#IBAction func getDirections(_ sender: Any) {
let view = annotation.coordinate
print("Annotation: \(String(describing: view ))")
let currentLocMapItem = MKMapItem.forCurrentLocation()
let selectedPlacemark = MKPlacemark(coordinate: view, addressDictionary: nil)
let selectedMapItem = MKMapItem(placemark: selectedPlacemark)
let mapItems = [selectedMapItem, currentLocMapItem]
let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]
MKMapItem.openMaps(with: mapItems, launchOptions:launchOptions)
}
I'm assuming the contents of the array change depending on the search query. If that is the case, make an array of type MKPointAnnotation:
var points: [MKPointAnnotation] = []
Then fill the array however you fill it and run it through a loop whereby each iteration adds a point to the map:
for point in points {
mapView.addAnnotation(point)
}
If you have a problem with varying types of annotations, make the array of type CLLocationCoordinate2D and then you can fill the array by accessing MKPointAnnotation.coordinate, for example.
Does this help?

Swift add integers from CKRecord return

I have a CKRecord that returns a list of 1s and -1s. I need to add all of these together to get a net value, but can't figure it out.
Here's a bit of the code:
let freezerQuery = CKQuery(recordType: "PumpingEntry", predicate: predicate)
let freezerSort = NSSortDescriptor.init(key: "FreezerQuantity", ascending: false)
freezerQuery.sortDescriptors = [freezerSort]
var freezerOperation = CKQueryOperation(query: freezerQuery)
freezerOperation.desiredKeys = ["FreezerQuantity"]
freezerOperation.recordFetchedBlock = { (record: CKRecord!) in
let freezerInteger: Int = record.object(forKey: "FreezerQuantity") as! Int
let freezerTotel = ?????????????
You need your freezerTotal variable outside of the block. Then simply add freezerInteger to freezerTotal.
var freezerTotal = 0
freezerOperation.recordFetchedBlock = { (record: CKRecord!) in
let freezerInteger: Int = record.object(forKey: "FreezerQuantity") as! Int
freezerTotal += freezerInteger
}
And then you can make use of the total in the operation's completion block:
freezerOperation.queryCompletionBlock = { (cursor, error) in
// do something with the total
}

Saving location coordinates to Core data- Swift 3

I want to save location coordinates into Core Data- what is the right way of storing these and retrieving them?
At the moment I am doing this and getting errors:
var newPlaceCoordinate = CLLocationCoordinate2D()
var newPlaceLatitude = CLLocationDegrees()
var newPlaceLongitude = CLLocationDegrees()
I get my coordinates from using Google maps API using the autocomplete widget.
let newPlaceCoordinate = place.coordinate
self.newPlaceCoordinate = place.coordinate
let newPlaceLatitude = place.coordinate.latitude
print(newPlaceLatitude)
self.newPlaceLatitude = place.coordinate.latitude
let newPlaceLongitude = place.coordinate.longitude
print(newPlaceLongitude)
self.newPlaceLongitude = place.coordinate.longitude
Then to store the coordinates I use:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let newPlace = NSEntityDescription.insertNewObject(forEntityName: "StoredPlace", into: context)
newPlace.setValue(newPlaceCoordinate, forKey: "coordinate")
newPlace.setValue(newPlaceLatitude, forKeyPath: "latitude")
newPlace.setValue(newPlaceLongitude, forKeyPath: "longitude")
And I have the attributes all set as String types.
Then to retrieve in my ViewDidLoad I have:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "StoredPlace")
request.returnsObjectsAsFaults = false
if let coordinate = result.value(forKey: "coordinate") as? String
{
let latitude = (coordinate as NSString).doubleValue
let longitude = (coordinate as NSString).doubleValue
let markers = GMSMarker()
markers.position = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
markers.icon = GMSMarker.markerImage(with: UIColor.yellow)
markers.tracksViewChanges = true
markers.map = vwGMap
}
So this is not working and producing an error of 'attribute: property = "coordinate"; desired type = NSString; given type = NSConcreteValue'. I have a few questions.
How do I save a coordinate data appropriately?
Do I save it as a CLLocationDegrees or CLLocationCoordinate2D?
How do I convert these objects into NSString or should I be using a different type in my attributes? (I tried changing it to Double or Integer but it also produced errors). I have multiple location coordinates which I would want to store and retrieve from core data.
If you are going to deal with lots of data, then CoreData is the best bet.
These are my attributes in StorePlace entity, its best to save only latitude and longitude and create coordinates from them when needed in your implementation.
#NSManaged public var newPlaceLatitude: Double
#NSManaged public var newPlaceLongitude: Double
And to insert new data, I will do
class func insert(latitude: Double, longitude: Double) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let managedObjectContext = appDelegate.managedObjectContext
let entity = NSEntityDescription.entity(forEntityName: "StoredPlace", in:managedObjectContext)
let newItem = StoredPlace(entity: entity!, insertInto: managedObjectContext)
newItem.newPlaceLatitude = latitude
newItem.newPlaceLongitude = longitude
do {
try managedObjectContext.save()
} catch {
print(error)
}
}
After retrieving latitude, longitude you can use coordinate in your implementations as
let newItem = getCoordinateFromCoreData() // your retrieval function
let coordinate = CLLocationCoordinate2D(latitude: newItem.latitude, longitude: newItem.longitude)
You need to convert the co-ordinate to double/NSdata type and then store it in CoreData. Use NSKeyedArchiver and NSKeyedUnarchiver to store and retrieve the values. CoreData can't store CLLocation object directly.
// Convert CLLocation object to Data
let newPlaceLatitudeArchived = NSKeyedArchiver.archivedDataWithRootObject(newPlaceLatitude)
//CoreData Piece
newPlace.setValue(newPlaceLatitudeArchived, forKeyPath: "latitude")
// fetch the latitude from CoreData as Archive object and pass it to unarchive to get the CLLocation
let newPlaceLatitudeUnArchived = NSKeyedUnarchiver.unarchiveObjectWithData(archivedLocation) as? CLLocation
You will need to convert you attribute to binary type in CoreData in this case.

Create MapKit Circle overlay from multiple CloudKit records

I've been trying to add a new map view to my app which shows an overlay of all of the Geofenced regions in my CloudKit database.
At the moment I'm able to create pins from each of the locations with the following code.
func fetchData() {
let predicate = NSPredicate(format: "TRUEPREDICATE", argumentArray: nil)
let query = CKQuery(recordType: "Collection", predicate: predicate)
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["Location"]
operation.recordFetchedBlock = { (record : CKRecord) in
self.collectionLocation = record.objectForKey("Location") as? CLLocation
print(self.collectionLocation?.coordinate.latitude)
self.buildBubbles()
}
publicDB!.addOperation(operation)
operation.queryCompletionBlock = {(cursor, error) in
dispatch_async(dispatch_get_main_queue()) {
if error == nil {
} else {
print("error description = \(error?.description)")
}
}
}
}
func buildBubbles() {
if CLLocationManager.isMonitoringAvailableForClass(CLCircularRegion.self) {
let intrepidLat: CLLocationDegrees = (self.collectionLocation?.coordinate.latitude)!
let intrepidLong: CLLocationDegrees = (self.collectionLocation?.coordinate.longitude)!
let title = "Item"
let coordinate = CLLocationCoordinate2DMake(intrepidLat, intrepidLong)
let regionRadius = 300.0
let region = CLCircularRegion(center: CLLocationCoordinate2D(latitude: coordinate.latitude,
longitude: coordinate.longitude), radius: regionRadius, identifier: title)
self.locationManager.startMonitoringForRegion(region)
let restaurantAnnotation = MKPointAnnotation()
restaurantAnnotation.coordinate = coordinate;
restaurantAnnotation.title = "\(title)"
self.mapView.addAnnotation(restaurantAnnotation)
// Overlay code goes here
}
else {
print("System can't track regions")
}
}
But when I go to add the overlay:
let circle = MKCircle(centerCoordinate: coordinate, radius: regionRadius)
self.mapView.addOverlay(circle)
The app fails with error:
"This application is modifying the autolayout engine from a background
thread, which can lead to engine corruption and weird crashes. This
will cause an exception in a future release."
My guess is that I'm doing too much inside the background thread but when I move the "buildBubbles" function into the main queue it adds the circle overlay but only adds one of the Locations to the map.
Thanks for taking the time to look I would really appreciate any help.
Your interface into the bubbles function only provides for holding one location. Try changing the interface, such as to an array, and then see what you get. You will also need to worry about how you actually synchronize one versus the other
I did as Feldur suggested and created an array from the CloudKit Data then moved the MapKit set up from the background thread.
func fetchBubble() {
let query = CKQuery(recordType: "Collection", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicDB!.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil {
for collection in results! {
let collectionLocation = collection.valueForKey("Location") as? CLLocation
let collectionName = collection.valueForKey("Name") as! String
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if CLLocationManager.isMonitoringAvailableForClass(CLCircularRegion.self) {
let intrepidLat: CLLocationDegrees = (collectionLocation?.coordinate.latitude)!
let intrepidLong: CLLocationDegrees = (collectionLocation?.coordinate.longitude)!
let title = collectionName
let coordinate = CLLocationCoordinate2DMake(intrepidLat, intrepidLong)
let regionRadius = 50.0
let region = CLCircularRegion(center: CLLocationCoordinate2D(latitude: coordinate.latitude,
longitude: coordinate.longitude), radius: regionRadius, identifier: title)
self.locationManager.startMonitoringForRegion(region)
let restaurantAnnotation = MKPointAnnotation()
self.mapView.addAnnotation(restaurantAnnotation)
restaurantAnnotation.coordinate = coordinate
let circle = MKCircle(centerCoordinate: coordinate, radius: regionRadius)
self.mapView.addOverlay(circle)
self.numberOfObjectsInMyArray()
}
else {
print("System can't track regions")
}
})
}
}
else {
print(error)
}
}
}

Resources