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

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

Related

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?

Can't get placemark out of reverseGeocodeLocation func Swift 3

I'm using the function reverseGeocodeLocation to turn coordinates (which I use for pinpoints) to turn into an address.
I've come up with this code:
func displayMarkers(/*completion: #escaping (CLPlacemark!)->()*/)
{
let annotationView = MKAnnotationView()
var integerCount = 0
let detailButton: UIButton = UIButton(type: .detailDisclosure)
annotationView.rightCalloutAccessoryView = detailButton
let geoCoder = CLGeocoder()
getFromDatabase { (locs) in
// Hier is "locs" de [CLLocationCoordinate2D] array
for location in locs{
let loca = CLLocation(latitude: location.latitude, longitude: location.longitude)
geoCoder.reverseGeocodeLocation(loca){placemarks, error in
var placemark : CLPlacemark!
placemark = placemarks?[0]
//let streetname = (placemark.addressDictionary?["Street"])
//let city = (placemark.addressDictionary?["City"])
//let cityAndStreet = "\(streetname!) \(city!)"
//completion(placemark)
}
//self.displayMarkers { (allPlacemarks) in
//let streetname = (allPlacemarks.addressDictionary?["Street"])
//let city = (allPlacemarks.addressDictionary?["City"])
//let cityAndStreet = "\(streetname!) \(city!)"
let annotation = MKPointAnnotation()
annotation.coordinate = location
annotation.title = "Taxi \(integerCount)"
annotation.subtitle = ""
self.mapView.addAnnotation(annotation)
}
integerCount = integerCount + 1
}
}
My question is, I can't get the completion working (so I commented it out).
When I use this completion like this, I get an error in my viewdidload where I call displayMarkers(), the error says I need to put in an argument which I don't have.
Is there any other way how I can get usable information out of it so I can put the address at annotation.subtitle ? I would be really glad to here it!
Try this,
fix
func displayMarkers(completion: #escaping (CLPlacemark!)->()) {}
to
func displayMarkers(completion: #escaping (_ placemark:CLPlacemark)->()) {}
if it wroks, however, you might better guard the error must to be nil, than you can get the placemarks safely.

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

Sort NSFetchRequest by calculation result

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]

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

Resources