I've been working on an app to annotate an MKMapView with all nightlife locations near a user. I have implemented a recenter function that is called by a SwiftUI button. The recenter works fine based on CLLocationManager.location, but after any MKLocalSearch query, the setRegion stops working. It must be the setRegion because the correct lat/long are still printed. I also know it is not due to the annotations because the same bug happens when the annotation add/remove part is commented out. Does it possibly have something to do with linking the searchRequest.region and map.region? manager is the CLLocationManager instance, map is the MapView
func recenter(){
print("Recenter called")
guard let center = manager.location?.coordinate else{
print("Could not get location for recenter")
return
}
let newRegion = MKCoordinateRegion(center: center, latitudinalMeters: 1000, longitudinalMeters: 1000)
print("\(center.latitude) and \(center.longitude)")
map.setRegion(newRegion, animated: true)
}
func queryAndAnnotate(){
let searchRequest = MKLocalSearch.Request()
searchRequest.naturalLanguageQuery = "nightlife"
searchRequest.region = map.region
let search = MKLocalSearch(request: searchRequest)
search.start{response, error in
guard let response = response else {
Alert(title:Text("Alert"), message: Text("Error: \(error?.localizedDescription ?? "Unknown Error")"))
return
}
var mapItemPlacemarks: [MKPointAnnotation] = []
self.barsList = []
for i in response.mapItems{
let x = MKPointAnnotation()
x.coordinate = i.placemark.coordinate
x.title = i.name
mapItemPlacemarks.append(x)
}
self.map.removeAnnotations(mapItemPlacemarks)
self.map.addAnnotations(mapItemPlacemarks)
}
}
MKLocalSearch.start calls the completionHandler in background task.
In this handler you are updating your mapView instance in background task.
Make sure to call all update methods of mapView in main task (removeAnnotations, addAnnotations, setRegion)
btw in
self.map.removeAnnotations(mapItemPlacemarks)
self.map.addAnnotations(mapItemPlacemarks)
you are removing mapItemPlacemarks that do not exist in the mapView. This makes no sense, but it is not your problem.
As it turns out the problem was coming from a dynamic update to a ScrollView. The ScrollView was within a slide up drawer that sat above the MapView on the Z axis. When the ScrollView was updated with the results from the local search, the MapView object lost track of userLocation (read 0,0) and the region could not be programmatically changed. I ended up fixing the problem by ditching the recenter() function altogether and instead implementing an MKUserTrackingButton within its own UIViewRepresentable (for SwiftUI). The MKUserTrackingButton is how the system maps cycles through user tracking modes. You can read the docs for it here: https://developer.apple.com/documentation/mapkit/mkusertrackingbutton
Related
I'm relatively new to iOS and Mapbox development. I'm working on an app where a user can freely manipulate a map full of places they have saved.
When they reach a zoom-level that is completely filled by the geography of a city, I would like to display the name of the city which they are viewing in a banner-style view, even if a city label is not within view on the map (as is often the case when zoomed in).
Here's a screenshot of the UI for context.
I'm trying to query the Mapbox tileset for the city name using the following code:
func mapViewRegionIsChanging(_ mapView: MGLMapView) {
let zoomLevel = mapView.zoomLevel
if zoomLevel >= 14.0 {
// layer identifier taken from layer name in Mapbox Studio
let layerIdentifier = "place-city-lg-n"
let screenRect = UIScreen.main.bounds
let cityName = mapView.visibleFeatures(in: screenRect, styleLayerIdentifiers: Set([layerIdentifier]))
print(cityName)
}
I think this code doesn't work because the label is not onscreen at the specified zoom level.
I'm wondering if using visibleFeaturesInRect is the best approach for my need—is there a better way to retrieve city name regardless of visible elements and zoom level?
For this task I'd recommend using MapboxGeocoder from Mapbox. It is for getting information about the city/village.
you can install pod:
pod 'MapboxGeocoder.swift', '~> 0.12'
and use this code:
let geocoder = Geocoder.shared
func mapViewRegionIsChanging(_ mapView: MGLMapView) {
let geocodeOptions = ReverseGeocodeOptions(coordinate: mapView.centerCoordinate)
geocodeOptions.allowedScopes = [.place]
let _ = geocoder.geocode(geocodeOptions) { (placemarks, attribution, error) in
guard let placemark = placemarks?.first else { return }
print(placemark.name)
print(placemark.qualifiedName)
}
}
you can add your conditions and it really helps to solve your task
I want to show annotations on the locations I get from my geofire database but even after writing the code and not getting any error no annotations are visible on the map.
My variable is
var vendorStore: Dictionary<String,Int>
My code in viewDidLoad
for (key,_) in KeyValue{
geoFireKeyValue.getLocationForKey(key, withCallback: { (location, error) in
if (error != nil) {
print("An error occurred getting the location for \"firebase-hq\": \(String(describing: error?.localizedDescription))")
} else if location != nil {
let annotation = MKPointAnnotation()
self.lat = (location?.coordinate.latitude)!
self.long = (location?.coordinate.longitude)!
annotation.coordinate = CLLocationCoordinate2D(latitude: self.lat, longitude: self.long)
self.mapView.addAnnotation(annotation)
self.mapView.showAnnotations(self.mapView.annotations, animated: true)
} else {
print("GeoFire does not contain a location for \"firebase-hq\"")
}
})
}
I am successfully getting the latitude and longitude in print statement as well but still no annotations are showing up. I tried moving the code else outside the for loop (just to check if any annotation show up) with some hard coded lat long coordinates and the annotation showed successfully. But still they aren't showing up when set in the geofire function within the loop (also not showing up in the loop outside the geofire function even with hard coded values). So kindly help me in resolving this issue.
I found the issue. The main problem was that I was sending the data from the previous view controller. That view controller was changed before the data was sent from the previous controller. So the solution is to make sure that data is set in the variable of the next view controller before the view actually appears. That's what I am trying to achieve now.
I posted this issue on GitHub, though it has been over a week and no response from the developers, so hoping to get an answer here.
Using the example code, plus adding a bit to show placemarks returned from ForwardGeocodeOptions, I came up with this testing code:
(Swift 3, Xcode 8)
func mapView(_ mapView: MGLMapView, regionDidChangeAnimated animated: Bool) {
geocodingDataTask?.cancel()
self.outputText.text = ""
// Variables.userLat and Variables.userLng are set through locationManager
let options = ReverseGeocodeOptions(coordinate: CLLocationCoordinate2D(latitude: Variables.userLat, longitude: Variables.userLng))
geocodingDataTask = geocoder.geocode(options) { [unowned self] (placemarks, attribution, error) in
if let error = error {
NSLog("%#", error)
} else if let placemarks = placemarks, !placemarks.isEmpty {
self.resultsLabel.text = placemarks[0].qualifiedName
let foptions = ForwardGeocodeOptions(query: self.inputText.text!)
// To refine the search, you can set various properties on the options object.
foptions.allowedISOCountryCodes = ["US"]
foptions.focalLocation = CLLocation(latitude: Variables.userLat, longitude: Variables.userLng)
let neLat = Variables.userLat + 1.0
let neLng = Variables.userLng + 1.0
foptions.allowedRegion?.northEast = CLLocationCoordinate2D(latitude: neLat, longitude: neLng)
let swLat = Variables.userLat - 1.0
let swLng = Variables.userLng - 1.0
foptions.allowedRegion?.southWest = CLLocationCoordinate2D(latitude: swLat, longitude: swLng)
foptions.allowedScopes = [.address, .pointOfInterest]
let _ = geocoder.geocode(foptions) { (placemarks, attribution, error) in
guard let placemark = placemarks?.first else {
return
}
let coordinate = placemark.location.coordinate
print("\(coordinate.latitude), \(coordinate.longitude)")
self.inputLat.text = coordinate.latitude.description
self.inputLng.text = coordinate.longitude.description
var string = ""
for mark in placemarks! {
if string != "" {
string += "\n"
}
string += mark.qualifiedName
}
self.outputText.text = string
}
} else {
self.resultsLabel.text = "No results"
}
}
}
That gives me a mini-app to test out the data that is returned when I change locations in the Xcode Simulator.
screenshot 2017-07-12 13 54 09
As you can see from this shot, I have centered the map in Jenks, OK (a small town just outside of Tulsa, OK - sort of a 'central US' location.)
When searching for a common place in that area ("Walmart" - which is based in nearby Arkansas, so there are plenty of them around), you can see that only 2 'local' Walmart's come back in the search.
Now, let's move to Bentonville, AR - the home of Walmart......
And, we get two top new results, but the others are the same (and much farther away than Tulsa, OK.....)
We found that if we add the town to the first of the search, the results are much better:
(similar results are true for every search we did - various cities around the US and with other 'common places' like Quiznos (similar results as Walmart when in their home town of Denver, CO...)
As you can see from my code, I tried using the allowedRegion?.northEast and southWest (as I understand it, those should set the search area to about 100 miles around the location, though I'm not sure I set that up right), though no difference was found from this setup (i.e., with/without I get the same results).
The only 'better' results were by putting in the town name along with the 'common' one (though, oddly, different results were returned if the town name was before or after the common one - I didn't check exactly, though I think they are 'best' (i.e., locations are all pretty near) from putting the town name after the common one)
What can I do to get better results without having to tell the user to enter the town name (not a very desirable plan! :)
Thank you in advance for tips - the lookup is a key part of the app (not the test stuff shown in the pictures! ) and users expect to pull up several 'nearby' common places (in this case, we would expect to see all 5 results within something like 20 miles - certainly no more than 100 miles away), so it is an important thing for us that this work much more reliably than we are seeing now.
I don't know about anyone else, but EventKit seems to have very little in terms resources and tutorials online for you to refer to for help.
I need to trigger an alarm when a user hits a radius of a set of coordinates, I wasn't sure of the best ay to do this, I was torn between local notifications, and EventKit reminders.
I decided to go for eventKit as I felt that I could do more with more the alarms and it was the most practical way to do it however, having not known much about EventKit i had some issues.
Anyway I've managed to get together and build a sample project which works and triggers an alert when the user leaves their current location, the only problem is, is that I almost want to do the complete opposite of that, I want to trigger an alert when the user enters a set of coordinates, I assume that most of the code is transferrable, however I seem to be stuck on one bit mainly.
// Creates an EKStructuredLocation Instance with a title of "Current Location"
let location = EKStructuredLocation(title: "Current Location")
// Uses the last location update extracted from the locations array to supply you're current location
location.geoLocation = locations.last as! CLLocation
// Location is added to a newly created alarm instance
let alarm = EKAlarm()
alarm.structuredLocation = location
// This alarm is triggered when the user moves away from the location proximity
alarm.proximity = EKAlarmProximityLeave
stationReminder.addAlarm(alarm)
I'm struggling to find how to set the location of the alarm to coordinates rather than users location.
I tried changing this
location.geoLocation = locations.last as! CLLocation
to
location.geoLocation = CLCircularRegion(circularRegionWithCenter: CLLocationCoordinate2D(latitude: 37.33233141, longitude: -122.03121860), radius: 50.0, identifier: "Location1")
but this doesn't work, i believe i am on the right track but i am throwing up this error: Cannot assign a value of type 'CLCircularRegion!' to a value of type 'CLLocation!'
I've tried loads of things with no resolve, does anybody have any experience with this and know how to help?
I also assume i'll have to change the following from this
alarm.proximity = EKAlarmProximityLeave
to this
alarm.proximity = EKAlarmProximityEnter
UPDATE
I've taken on board some comments below and tried a bunch of other things to get this to work, I feel like I am so close but somethings just missing. I cannot get this alarm to trigger. Excuse all the code comments, it's just so you can see some of the attempts i have made at fixing this.
can anyone see anything wrong with this code for the alarm?
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
// Stops location manager from sending further updates
locationManager.stopUpdatingLocation()
// Creates a new EKReminder which is named and initialised with text from the UITextField
let stationReminder = EKReminder(eventStore: appDelegate!.eventStore)
stationReminder.title = locationText.text
// Stores the previously created EKReminder in the default calendar
stationReminder.calendar = appDelegate!.eventStore!.defaultCalendarForNewReminders()
// Creates an EKStructuredLocation Instance with a title of "Current Location"
let location = EKStructuredLocation(title: "Destination: Bournemouth Station")
// Uses the last location update extracted from the locations array to supply you're current location
// location.geoLocation = locations.last as! CLLocation
// location.geoLocation = CLCircularRegion(circularRegionWithCenter: CLLocationCoordinate2D(latitude: 37.33233141, longitude: -122.03121860), radius: 50.0, identifier: "Location1")
// location.geoLocation = CLLocation(latitude: 50.742771, longitude: -1.895072)
location.radius = 50.0
location.geoLocation = CLLocation(latitude:50.742771, longitude:-1.895072)
// location.radius = 10.0 // metres
// Location is added to a newly created alarm instance
let alarm = EKAlarm()
alarm.structuredLocation = location
// This alarm is triggered when the user moves away from the location proximity
// alarm.proximity = EKAlarmProximityEnter
alarm.proximity = EKAlarmProximityEnter // "geofence": we alarm when *arriving*
// but this will have no effect until Reminders is granted Location access...
// and in iOS 8 it won't even ask for it until it is launched
// also, in iOS 8 the separate background usage pref is withdrawn;
// instead, auth of Reminders for "when in use" covers this...
// ...because it means "this app *or one of its features* is visible on screen"
stationReminder.addAlarm(alarm)
// Now we have a fully configured reminder which we save in the Event Store
var error: NSError?
appDelegate!.eventStore?.saveReminder(stationReminder,
commit: true, error: &error)
if error != nil {
println("Reminder failed with error \(error?.localizedDescription)")
}
}
Looks like EKEvent has an EKStructuredLocation, which you are using correctly. However, you need to be careful of the type of the geoLocation property. It should be a CLLocation, which is not the same as a CLCircularRegion.
Steps to fix:
check the docs for EKStructuredLocation https://developer.apple.com/library/mac/documentation/EventKit/Reference/EKStructuredLocationClassRef/index.html
set the location.geoLocation to a CLLocation that you create from latitude, longitude coordinates. (check the docs for CLLocation: https://developer.apple.com/library/mac/documentation/CoreLocation/Reference/CLLocation_Class/index.html#//apple_ref/swift/cl/CLLocation)
geoLocation = CLLocation(latitude: 37.33233141, longitude: -122.03121860)
set the geoLocation.radius separately location.radius = 50.0
at that point setting proximityEnter should work as you expected
I want a static map image from a location. I have the latitude and longitude of that location. Is there any api in iOS 6 to get the apple map static image of a gps location.
MKMapView has two properties to achieve this functionality: scrollEnabled and zoomEnabled. Set both to NO and you'll have a non-scrolling and non-zooming (thus static) map image.
I believe Google Static Maps is what you're looking for. Send it the parameters of location and it will return the image you want.
On iOS 7+ Apple MapKit allows displaying static images of a map using MKMapSnapshotter class. It is detailed in Creating a Snapshot of a Map section of MapKit documentation:
// Takes a snapshot and calls back with the generated UIImage
static func takeSnapshot(mapView: MKMapView, withCallback: (UIImage?, NSError?) -> ()) {
let options = MKMapSnapshotOptions()
options.region = mapView.region
options.size = mapView.frame.size
options.scale = UIScreen.mainScreen().scale
let snapshotter = MKMapSnapshotter(options: options)
snapshotter.startWithCompletionHandler() { snapshot, error in
guard snapshot != nil else {
withCallback(nil, error)
return
}
withCallback(snapshot!.image, nil)
}
}
just use MKMapView to display map ,you will have to use MapKit.framework for that
this link will help you...
https://github.com/kviksilver/MKMapview-annotation-grouping
Happy Coding!!!