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)
}
}
}
Related
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)
}
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?
Idea :
App lets drivers see the closest shop/restaurants to customers.
What I have :
Coordinates saved as strings
let clientLat = "24.449384"
let clientLng = "56.343243"
a function to find all the shops in my local area
I tried to save all the coordinates of a shop in my local area and I succeeded:
var coordinates: [CLLocationCoordinate2D] = [CLLocationCoordinate2D]()
func performSearch() {
coordinates.removeAll()
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = "starbucks"
request.region = mapView.region
let search = MKLocalSearch(request: request)
search.start(completionHandler: {(response, error) in
if error != nil {
print("Error occured in search: \(error!.localizedDescription)")
} else if response!.mapItems.count == 0 {
print("No matches found")
} else {
print("Matches found")
for item in response!.mapItems {
self.coordinates.append(item.placemark.coordinate)
// need to sort coordinates
// need to find the closest
let annotation = MKPointAnnotation()
annotation.coordinate = item.placemark.coordinate
annotation.title = item.name
self.mapView.addAnnotation(annotation)
}
}
})
}
What I need:
I wish to loop through the coordinates and find the closest shop (kilometers) to the lat and long strings then put a pin on it.
UPDATE
func performSearch() {
coordinates.removeAll()
let request = MKLocalSearchRequest()
request.naturalLanguageQuery = "starbucks"
request.region = mapView.region
let search = MKLocalSearch(request: request)
search.start(completionHandler: {(response, error) in
if error != nil {
print("Error occured in search: \(error!.localizedDescription)")
} else if response!.mapItems.count == 0 {
print("No matches found")
} else {
print("Matches found")
for item in response!.mapItems {
self.coordinates.append(item.placemark.coordinate)
let pointToCompare = CLLocation(latitude: 24.741721, longitude: 46.891440)
let storedCorrdinates = self.coordinates.map({CLLocation(latitude: $0.latitude, longitude: $0.longitude)}).sorted(by: {
$0.distance(from: pointToCompare) < $1.distance(from: pointToCompare)
})
self.coordinate = storedCorrdinates
}
let annotation = MKPointAnnotation()
annotation.coordinate = self.coordinate[0].coordinate
self.mapView.addAnnotation(annotation)
}
})
}
Thank you #brimstone
You can compare distances between coordinates by converting them to CLLocation types and then using the distance(from:) method. For example, take your coordinates array and map it to CLLocation, then sort that based on the distance from the point you are comparing them to.
let coordinates = [CLLocationCoordinate2D]()
let pointToCompare = CLLocation(latitude: <#yourLat#>, longitude: <#yourLong#>)
let sortedCoordinates = coordinates.map({CLLocation(latitude: $0.latitude, longitude: $0.longitude)}).sorted(by: {
$0.distance(from: pointToCompare) < $1.distance(from: pointToCompare)
})
Then, to set your annotation's coordinate to the nearest coordinate, just subscript the sortedCoordinates array.
annotation.coordinate = sortedCoordinates[0].coordinate
I would like to share my solution :)
1) In my case, I upload data from the API, so I need to create a model.
import MapKit
struct StoresMap: Codable {
let id: Int?
let title: String?
let latitude: Double?
let longitude: Double?
let schedule: String?
let phone: String?
let ukmStoreId: Int?
var distanceToUser: CLLocationDistance?
}
The last variable is not from API, but from myself to define distance for each store.
2) In ViewController I define:
func fetchStoresList() {
NetworkManager.downloadStoresListForMap(firstPartURL: backendURL) { (storesList) in
self.shopList = storesList
let initialLocation = self.locationManager.location!
for i in 0..<self.shopList.count {
self.shopList[i].distanceToUser = initialLocation.distance(from: CLLocation(latitude: self.shopList[i].latitude!, longitude: self.shopList[i].longitude!))
}
self.shopList.sort(by: { $0.distanceToUser! < $1.distanceToUser!})
print("Closest shop - ", self.shopList[0])
}
}
3) Don't forget to call the function in viewDidLoad() and import MapView framework :)
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.
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)
}