Run action when iOS device is near some map point - ios

I have the mobile app that uses user's location when the app is running to show some places on a map (now it is Google Map).
I want to add a new feature - App in the background should notify the user when the device is near some map point or entering the predefined area.
How can it be done? Are there different approaches?
Where to start from?
What if my points stored on the server - should I save them on the device first and what if can't do it and new map points can be added when the app is not in memory?
What will be the most challenging in creating such feature?
p.s. I know about Ray's geofencing tutorial but I want to know - are there any different approaches and also about pitfalls of such feature

First of all you must enable location updates and remote notifications in background modes(Your Target->Capabilities), then you must write in your location update function some code to detect that user is close to some location exapmle:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let userLocation = locations.first {
if location.coordinate.langtitude <= <some value> ||
location.coordinate.langtitude >= <some value> || ... {
runAction()
}
}
}
func runAction() {
//show local notification here
}

Related

CLLocation Manager giving incorrect location sometime

I am using core location API for iOS (CLLocation manager) and its sometimes giving me wrong location like if I am in U.S it gives me latitude and longitude of Spain. Searched stack over flow for the same but couldn't get a relevant answer for my query. Being novice to the use maps and locations I am a bit confused. Any help would be highly appreciated.
I assume that you are using a physical device and not the simulator to test this, and you did not select the option to simulate location in xcode. Sometimes CLLocation manager can return for example an old cached location, so perhaps that is what happened to you. Before you do anything with received CLLocation object I recommend checking its horizontalAccuracy and timestamp properties.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let lastLocation = locations.last, lastLocation.horizontalAccuracy < 1000, abs(lastLocation.timestamp.timeIntervalSinceNow) < 60 else {
// location not accurate or to old (cached previously) so do not do anything
return
}
// do something
}
Actually, Apple use wifi devices around for location detection. So on "cold" start gps chip needs some time to start and find satellites, and during that time device use location based on router's mac addresses around. If your (or your neighbor's) router ever been in Spain, than that's the reason - Apple database of mac addresses refresh very slowly, it could take 1-3 months for that.

Perform task (code execution/ network call) in background when user enters/exits a location region in iOS

System: In my application, I am using geofencing (monitoring a region). Whenever user enters or exits the monitored area or region, the app shows a local notification if the app is in the background or even terminated. This is working perfectly fine. The app is able to show local notificaiton.
Now I also need to submit this information (if the user is inside or outside of the monitored area) via HTTP POST call to app's backend server.
Problem: App makes API call in LocationManager's delegate methods but sometimes it works and sometimes it does not. It seems that code execution stops randomly if the app is not in foreground state.
Code sample
// MARK: - Location Manager Delegate
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
if region is CLCircularRegion {
showLocalNotification(forRegion: region)// Works
updateUserEntryAPICall(region: region) // Sometimes works
}
}
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
if region is CLCircularRegion {
showLocalNotification(forRegion: region) // Works
updateUserExitAPICall(region: region) // sometime works
}
}
I did not turn on background mode capability in iOS. Still, the app is able to show local notifications. Do I need to turn it on in order to make network call to work?
Please help.
This looks like you're using a regular URLSession to make your API request. You need to make sure that you're doing this on a session that handles running in the background, e.g. by initializing it like so:
let session = URLSession(configuration: .background(withIdentifier: "foo"))

CLLocationManager Region Monitoring: Detect Arrival in Suspended State

I'm looking for a way that I can track that a user has arrived near a designated set of co-ordinates. The functionality needs to work while the application is in the background (preferably within 100 metres). Also, to preserve the battery, I ideally do not want to get too many co-ordinate readings (perhaps a reading every 10 minutes for no longer than a couple of hours).
There are a couple of ways that I have tried to accomplish this task, but have been unable to obtain the desired result:
Background Timer:
I had added a background task in (App.delegate)
func applicationDidEnterBackground(_ application: UIApplication)
Which executed a repeated Timer.scheduledTimer to get co-ordinates and process
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
to detect if the user was within range. This method worked if applied in the short-term, but only until the application was suspended, which was about 3 minutes. Ideally, I would not want to get co-ordinates this frequently.
Region Monitoring:
I had initialised the CLLocationManager as shown below:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
locationManager.delegate = self
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.activityType = .otherNavigation
locationManager.requestAlwaysAuthorization()
}
The LocationManager starts when the application enters into the background:
func applicationDidEnterBackground(_ application: UIApplication) {
self.monitorRegionAtLocation(center: CLLocationCoordinate2D(latitude: x, longitude: y), identifier: id)
locationManager.startUpdatingLocation()
}
Code for monitoring of region:
func monitorRegionAtLocation(center: CLLocationCoordinate2D, identifier: String ) {
// Make sure the app is authorized.
if CLLocationManager.authorizationStatus() == .authorizedAlways {
// Make sure region monitoring is supported.
if CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) {
// Register the region.
let maxDistance = 200.0
let region = CLCircularRegion(center: center,
radius: maxDistance, identifier: identifier)
region.notifyOnEntry = true
region.notifyOnExit = false
locationManager.startMonitoring(for: region)
}
}
}
And I added a didEnterRegion function block for CLLocationManager:
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
if let region = region as? CLCircularRegion {
let identifier = region.identifier
print("FOUND: " + identifier)
}
}
The code appears to work for detecting entry into a region, however the co-ordinates are not updating while in the background.
Additional Information
I have the Background Modes of Location Updates and Background Fetch enabled
I have supplied values for 'Location Always Usage Description' and 'Location When in Use Usage Description' in the Info.plist
The App Settings shows 'Always' permission against the Location
I believe that there has to be a better way of operating these kinds of checks in the background, but I haven't discovered any method of detecting other movements in the background.
Any direction on this matter would be greatly appreciated, and if you need any more information, please let me know and I'll provide what I can.
UPDATE:
I have modified the approach following the advice of comments below to use Region Monitoring.
Any location update/monitoring requires it's location manager to be configured properly so that it can work to the best to provide the desired location update. It's important to check some point when doing background location update:
1. Check background modes of location updates and background fetch should be enable
2. Check 'Location Always Usage Description' and 'Location When in Use Usage Description' in the Info.plist should be provided
3. Check if you want to pause in between location update - if yes then you need to provide activity type so that location manager can determine best way to pause location update for you
4. Check if you want to apply distance filter - you want user(device) to move some minimum amount for location manager to send updated location
5. Check if you want desired accuracy- This may cause power drain for certain accuracy type
In your code I can see location manager is configured with some of the parameter but missing accuracy and distance filter for background mode.
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.activityType = .otherNavigation
Also, if you see pause location update property in Apple doc it says:
For apps that have in-use authorization, a pause to location updates
ends access to location changes until the app is launched again and
able to restart those updates. If you do not wish location updates to
stop entirely, consider disabling this property and changing location
accuracy to kCLLocationAccuracyThreeKilometers when your app moves to
the background. Doing so allows you to continue receiving location
updates in a power-friendly manner.
Essentially it tells that if you want disable pause then you have to keep accuracy level (kCLLocationAccuracyThreeKilometers). Which I guess is missing in your approach.
Also, you can check this link which actually starts background task and then starts location manager monitoring inside the background task.
Hope it helps.
The question says "CLLocationManager Region Monitoring: Detect Arrival in Background". And this is very much possible, but detecting anything after being killed is not possible (from iOS 7).
Whenever user swipe ups your app app-switcher, iOS takes it as the user doesn't wish the app to be running in the background, and so all the call-backs are stopped.
This answer, this answer and this answer also says the same thing. However Apple doc is a little confusing.
My personal observation is that app gets called even in Killed mode but very rarely.
And about getting the location, whenever the delegate method of geofencing is called, you can get location easily.
And the background modes are really not needed for your requirement.
And unfortunately (fortunately for iOS user as they save battery) we don't really have a way to get location just for 1 hr after app being killed.

How can I get trueHeading just once in iOS?

As a fresh iOS developer, I am confused to get trueHeading data recently.
class AR: CLLocationManagerDelegate{
var heading:Float!
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
heading = Float(newHeading.trueHeading)
}
}
Via above code, I can continuously get current device's direction. However, can I just get the trueHeading just once? In addition, I tested the trueHeading, it will be accurate after seconds. Can I get the trueHeading at a particular time?
Thank you for any help!
Whenever you need heading you can call
[self.locationManager startUpdatingHeading];
Once you have received the heading, you can do a
[self.locationManager stopUpdatingHeading];
To receive the latest derived heading,
self.locationManager.heading;
So ideally, you call startUpdatingHeading, show a loader or something to the user.
Once you get a heading in didUpdateHeading, relay a message to the controller via a notification or something, and stopupdating heading.
You cannot set the accuracy of the Heading, but you can set the headingFilter to only update when there is a large distance.
/* Notify heading changes when heading is > 5.
* Default value is kCLHeadingFilterNone: all movements are reported.
*/
self.locationManager.headingFilter = 5;
Ideally, you do not stop ever, you continue accessing the heading, with a filter of 5, ensuring it does not drain battery.
You store this in the defaults lets say, and use it whenever needed and update it.
If you need it just for once, then you may do a quick fix by waiting for some duration or lets say some hits to the function, but that is not an ideal solution.
Maybe this can also help, but it is primarily for distance, and not heading. Heading is supposed to be constantly updated.
self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
If you want this on app launch, do it in the app delegate.

Which one to use "locations array" or "manager.location.coordinate" in Swift in iOS to get user location?

I want to get the user's location. It might be approximate position, that's fine.
In didUpdateLocations method, i saw two ways to get the coordinates.
Using manager.location.coordinate
Using locations array
Which one should I go for? I am thinking locations array will contain user's recent location as I am starting startUpdatingLocation() as soon as user opens the app.
So which one should i use to get the coordinates? And what does manager.location.coordinate do in this case?
Note: Accuracy of location need not be accurate to 10 meters. Rough estimation is enough.
Here is my code:
func initLocationUpdates() {
if CLLocationManager.locationServicesEnabled() {
print("going to get location")
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
else {
print("location services disabled")
}
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let locValue:CLLocationCoordinate2D = manager.location!.coordinate
print("locations = \(locValue.latitude) \(locValue.longitude)")
/*
let userLocation:CLLocation = locations[0] // or maybe even locations.last
let long = userLocation.coordinate.longitude;
let lat = userLocation.coordinate.latitude;
print("lat: ", lat, " --- long: ", long)
*/
locationManager.stopUpdatingLocation()
}
I'd prefer the array over the instance variable, because of the way desiredAccuracy works - it doesn't guarantee that the location updates you receive will have the accuracy you request - merely that the system will make its best effort to provide them. Depending on how the location manager behaves, you might get a number of updates in the locations array, with differing levels of accuracy. You can then filter the array to find the most accurate one.
The key point is that if multiple updates arrive in the same didUpdateLocations: call, and you just use manager.location, you might be missing an update. The missing update would definitely not be the most recent one, but it might be the most accurate one received so far.
Since you have set desiredAccuracy to a very high level (ten meters), I'm guessing precision is more important to you than timing - it will take a while for the system to deliver an update of that level of accuracy (and it may never arrive if you are indoors or otherwise blocked from using GPS). So the likelihood of the scenario above occurring is reasonable.
As for the purpose of the instance variable location, the docs suggest that it's intended to be used in certain circumstances when the app restarts:
In iOS 4.0 and later, this property may contain a more recent location object at launch time. Specifically, if significant location updates are running and your app is terminated, this property is updated with the most recent location data when your app is relaunched (and you create a new location manager object). This location data may be more recent than the last location event processed by your app.

Resources