I start my app and schedule my local notifications. This is a simplified version of the code I'm using:
let content = UNMutableNotificationContent()
content.body = "Wild IBEACON appeared!"
let region = CLBeaconRegion(proximityUUID: uuid, identifier: "iBeacon region")
let trigger = UNLocationNotificationTrigger(region: region, repeats: true)
let request = UNNotificationRequest(identifier: "iBeacon notification", content: content, trigger: trigger)
notificationCenter.add(request)
They trigger while my app is in the background. So far, so good.
Then I restart the device. I don't force-quit the app.
And now the notifications don't trigger anymore. I need to open the app again.
Is there a way to let my schedules survive the restart?
The UNLocationNotificationTrigger is a new helper classes added in iOS10 to make it easier to trigger notifications based on beacon or geofence detections. According to the documentation, it is designed to be used only when the app is in use:
Apps must request access to location services and must have when-in-use permissions to use this class. To request permission to use location services, call the requestWhenInUseAuthorization() method of CLLocationManager before scheduling any location-based triggers.
https://developer.apple.com/reference/usernotifications/unlocationnotificationtrigger
Based on the above permissions, the app will only trigger when in use. The documentation does not explicitly say that it won't work in the background, so you might try requesting always location permission with the requestAlwaysAuthorization() instead of requestWhenInUseAuthorization() (be sure you put the correct key in your plist if you do this), to see if this helps.
An alternative would be to not use this helper class and instead manually start up CoreLocation and beacon monitoring, then create your own UILocalNotification manually when you get the region entry callback:
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
if let region = region as? CLBeaconRegion {
let notificationMessage = "Wild IBEACON appeared!"
let notification = UILocalNotification()
notification.alertBody = notificationMessage
notification.alertAction = "OK"
UIApplication.shared.presentLocalNotificationNow(notification)
}
}
The above approach is known to work across app restarts.
Related
Still very new to Swift. I have come from an Android background where there is BroadcastReceiver that can deliver location info to a service even though the app isn't running.
So I was looking for something similar in iOS/Swift and it appears that before this wasn't possible but it may be now. I am developing for iOS 10 but would be great if it was backwards compatible.
I found
startMonitoringSignificantLocationChanges
which I can execute to start delivering location updates, although this raises a few questions. Once I call this and my app is NOT running, are the updates still being sent ? And how would the app wake up to respond ?
Also restarting the phone and when it return, does this mean I still need call startMonitoringSignificantLocationChanges again meaning that I would have to wait for the user to execute my app. Or does it remember the setting after reboot ?
Still a little confused how to get around this, here's a brief explanation of what I am trying to do.
I would like to update the location of the phone even though the app is not running, this would be sent to a rest service every so often.
This way on the backend services I could determine if somebody is within X meters of somebody also and send them a push notification.
It may or may not be a good solution but if I were you I would have used both startMonitoringSignificantLocationChanges and regionMonitoring.
Here is the sample I made which worked well with iOS 13.
Lets take regionMonitoring first. We have certainly no problems when the app is in foreground state and we can use the CLLocationManager's didUpdate delegate to get the location and send it to the server.
Keep latest current location in AppDelegate's property, lets say:
var lastLocation:CLLocation?
//And a location manager
var locationManager = CLLocationManager()
We have two UIApplicationDelegates
func applicationDidEnterBackground(_ application: UIApplication) {
//Create a region
}
func applicationWillTerminate(_ application: UIApplication) {
//Create a region
}
So whenever the user kills the app or makes the app go to background, we can certainly create a region around the latest current location fetched. Here is an example to create a region.
func createRegion(location:CLLocation?) {
if CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) {
let coordinate = CLLocationCoordinate2DMake((location?.coordinate.latitude)!, (location?.coordinate.longitude)!)
let regionRadius = 50.0
let region = CLCircularRegion(center: CLLocationCoordinate2D(
latitude: coordinate.latitude,
longitude: coordinate.longitude),
radius: regionRadius,
identifier: "aabb")
region.notifyOnExit = true
region.notifyOnEntry = true
//Send your fetched location to server
//Stop your location manager for updating location and start regionMonitoring
self.locationManager?.stopUpdatingLocation()
self.locationManager?.startMonitoring(for: region)
}
else {
print("System can't track regions")
}
}
Make use of RegionDelegates
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
print("Entered Region")
}
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
print("Exited Region")
locationManager?.stopMonitoring(for: region)
//Start location manager and fetch current location
locationManager?.startUpdatingLocation()
}
Grab the location from didUpdate method
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if UIApplication.shared.applicationState == .active {
} else {
//App is in BG/ Killed or suspended state
//send location to server
// create a New Region with current fetched location
let location = locations.last
lastLocation = location
//Make region and again the same cycle continues.
self.createRegion(location: lastLocation)
}
}
Here I have made a 50m region radius circle. I have tested this and it is called generally after crossing 100m from your center point.
Now the second approach can me using significantLocationChanges
On making the app go background or terminated, we can just stop location manager for further updating locations and can call the startMonitoringSignificantLocationChanges
self.locationManager?.stopUpdatingLocation()
self.locationManager?.startMonitoringSignificantLocationChanges()
When the app is killed, the location is grabbed from didFinishLaunching method's launchOptions?[UIApplicationLaunchOptionsKey.location]
if launchOptions?[UIApplicationLaunchOptionsKey.location] != nil {
//You have a location when app is in killed/ not running state
}
Make sure to keep BackgroundModes On for Location Updates
Also make sure to ask for locationManager?.requestAlwaysAuthorization() by using the key
<key>NSLocationAlwaysUsageDescription</key>
<string>Allow location</string>
in your Info.plist
There can be a third solution by taking 2 LocationManagers simultaneously.
For region
Significant Location Changes
As using significantLocationChanges
Apps can expect a notification as soon as the device moves 500 meters
or more from its previous notification. It should not expect
notifications more frequently than once every five minutes. If the
device is able to retrieve data from the network, the location manager
is much more likely to deliver notifications in a timely manner.
as per the give Apple Doc
So it totally depends on your requirements as the location fetching depends on many factors like the number of apps opened, battery power, signal strength etc when the app is not running.
Also keep in mind to always setup a region with good accuracy.
I know that this will not solve your problem completely but you will get an idea to move forward as per your requirements.
I'm trying to send the user a local notification when a region is entered (at an immediate distance) while the app is closed. I have it currently working if the app is in the background, but can't get it to work if the app is closed. I've read other posts that say this is possible, but none of the solutions work and they are outdated. I'd appreciate some help in Swift 3.
Here's my code (all in AppDelegate):
In didFinishLaunchingWithOptions:
locationManager.delegate = self
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.allowsBackgroundLocationUpdates = true
locationManager.requestAlwaysAuthorization()
let uuid = UUID(uuidString: "someuuid")!
let beaconRegion = CLBeaconRegion(proximityUUID: uuid, identifier: "SomeBeacon")
beaconRegion.notifyEntryStateOnDisplay = true
beaconRegion.notifyOnEntry = true
beaconRegion.notifyOnExit = true
locationManager.startMonitoring(for: beaconRegion)
locationManager.startRangingBeacons(in: beaconRegion)
I also have didRangeBeacons implemented.
The code looks correct to allow detection when the app is closed. You do not need:
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.allowsBackgroundLocationUpdates = true
But they should not hurt anything.
You only mention trouble with the app closed, so I assume foreground detection works fine. Does it? If not, troubleshoot this first.
It is often difficult to properly test the app closed use case leading to failures due to test setup issues. A few tips may help:
iOS will only send a region entry event if it thinks it is out of region. Often in testing it thinks it is in region so you do not get an event. To ensure you are out of region, turn off your beacon or go out of range with the app in the foreground and wait until you get an exit callback. Only then should you kill the app to test closed detection.
If rebooting your phone, always wait 5 minutes after startup to be sure CoreLocation is fully initialized. And make sure you have followed rule 1.
Make sure you do not have a bunch of other beacon apps installed on the phone taking up all the Bluetooth detection hardware acceleration slots. If you do, detections in the background can be delayed up to 15 min. Uninstall all beacon apps then uninstall and reinstall yours.
If you follow the testing tips above you should see detection callbacks within a couple of seconds of the beacon being in the vicinity.
UPDATE As #davidgyoung pointed out to me on a different question, this method of using the UNLocationNotificationTrigger won't report the major and minor identifiers to you because it is using the monitoring API and not ranging which is what you need to obtain the major and minor numbers. This limits what all you might do with this wrapper API. It sure is convenient, but not as helpful as one might hope.
The new (iOS 10, Swift 3) way from what I can tell is to use the UserNotifications library and implement it with a UNNotificationRequest. You can pass it a beacon region via UNLocationNotificationTrigger. Now I know it seems counter intuitive for what you're wanting (notification when app is closed), however, you want to add the NSLocationWhenInUseUsageDescription key to your Info.plist instead of NSLocationAlwaysUsageDescription. I'm not sure why it is that way, but it's a necessary step. Here's the code I'm using for creating a (iBeacon) location based notification:
// Ensure you use a class variable in order to ensure the location
// manager is retained, otherwise the alert message asking you to
// authorize it will disappear before you can tap "allow" which
// will keep it from working
let locationManager = CLLocationManager()
// ...
// Somewhere in your class (e.g. AppDelegate or a ViewController)
self.locationManager.requestWhenInUseAuthorization()
let region = CLBeaconRegion(proximityUUID: UUID(uuidString: "YOUR-UNIQUE-UUID-STRING")!, identifier: "com.yourcompany.youridentifier")
region.notifyOnEntry = true
region.notifyOnExit = false
let content = UNMutableNotificationContent()
content.title = "Notification Title"
content.body = "Body text goes here"
// Not sure if repeats needs to be set to true here, but that's
// how I've implemented it
let trigger = UNLocationNotificationTrigger(region: region, repeats: true)
// Use the same identifier each time to ensure it gets overwritten
// in the notification system
let identifier = "BeaconLocationIdentifier"
let request = UNNotificationRequest.init(identifier: identifier, content: content, trigger: trigger)
// This is probably unnecessary since we're using the same identifier
// each time this code is called
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
// Make sure you set the notification center's delegate. My
// containing class is the AppDelegate. I implement the delegate
// methods there.
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().add(request, withCompletionHandler: { (error) in
})
// Not sure if this is required either, but after I added this, everything
// started working
self.locationManager.startRangingBeacons(in: region)
Make sure you implement the notification center delegate methods:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
// Called when the notification has been swiped or tapped by the user
// Do something with the response here
// Make sure you call the completion handler
completionHandler()
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
// Called when the app is in the foreground
// Do something with the notification here
// Make sure you call the completion handler
completionHandler([.alert, .sound])
}
Finally, you're going to need to authorize notifications first to get this all to work. You can use this code to do that:
let options: UNAuthorizationOptions = [.alert,.sound]
UNUserNotificationCenter.current().requestAuthorization(options: options) {
(granted, error) in
if !granted {
debugPrint("Something went wrong")
} else {
debugPrint("Notifications granted")
}
}
One last tip: I have an old metal tin that (sort of) acts as a Faraday cage for the beacon I'm using. I just close my beacon up in there for a minute or so and then the app detects that I've gone out of range. This is more convenient than trying to walk far enough away from the beacon or removing the battery (which doesn't seem to work at all for me). Some beacons have stronger signals than others and may still get through the tin, so YMMV.
I've created a simple geofence based application to monitor the movement of people entering and exiting a geofence and I'm trying to send a notification on these events but can't seem to implement it correctly.
I would really appreciate some explained sample code which would go in the view controller and app delegate.
P.s My knowledge of swift is somewhat limited but I understand most aspects necessary for this application.
Thanks for any help!
Edit:
This is my function for creating the notification which I think is correct.
func scheduleNotification() {
let centre = CLLocationCoordinate2DMake(51.364730, -0.189986)
let region = CLCircularRegion(center: centre, radius: 150, identifier: "SGS")
region.notifyOnEntry = true
let trigger = UNLocationNotificationTrigger(region: region, repeats: true)
let enterContent = UNMutableNotificationContent()
enterContent.title = "Enter"
enterContent.body = "Entered premesis"
enterContent.sound = UNNotificationSound.default()
let enterRequest = UNNotificationRequest(identifier: "enterNotification", content: enterContent, trigger: trigger)
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
UNUserNotificationCenter.current().add(enterRequest) {(error) in
if let error = error {
print("Error: \(error)")
}
}
}
I added this to the didFinishLaunchingWithOptions:
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {(accepted, error) in
if !accepted {
print("Notification access denied.")
}
}
I know I need to call the function but I don't know where to do it so that it is called when the geofence is left. Also I'm not sure how to call it from the view controller as it is in the AppDelegate.
Sorry if I'm being stupid but thanks for the help!
You need to implement the location manager's didEnterRegion and didExitRegion delegate methods. Within your implementation of those methods is where you'll post the local notifications.
If you've already done that, make sure that the app has the proper capibility for posting notifications, and that you've registered the proper notification settings for the app in the app delegate.
Ray Wenderlich has a good tutorial on Geofencing and here's Apple's guide on local and remote push notifications.
I have a local geofence notification, set up with the following code:
func startMonitoring(annotation:MKAnnotation) {
let region = CLCircularRegion(center: annotation.coordinate, radius: 10.0, identifier: "randomID")
region.notifyOnExit = false
let notification = UILocalNotification()
notification.region = region
notification.alertBody = "You got it!"
notification.category = self.notificationCategory
notification.soundName = "my_sound.wav"
notification.userInfo = ["ID": "randomID"]
UIApplication.sharedApplication().scheduleLocalNotification(notification)
}
This works great, but the user has to get to the area within a certain time limit for the action of the notification to be available. How can I make the notification NOT fire if the time limit has passed?
I currently only have "WhenInUse" authorization for the user's location and would prefer to keep it that way.
You'd be better off monitoring the CLCircularRegion manually using CLLocationManager, and when your app is notified that the user has entered the region, check whether it's in the time limit and post your UILocalNotification manually.
See the section Using Regions to Monitor Boundary Crossings here: https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocationManager_Class/index.html
You can setup background fetch with setminimumbackgroundfetchinterval , and cancel the scheduled notification if it's going to expire.
It's not 100% reliable because you cannot control exact time, but it's better than nothing.
I am writing an iOS app that requires the device's GPS loction to be updated when a push notification is received.
I use a closure to get the current GPS location. This code runs perfectly when the app is in the foreground (Both "New remote notification" and "Got location" is printed to console), but when the app is in the background, only "New remote notification" (and no location error) is printed to console. As a result I don't have the GPS location of the device at this point, which is very important for my app's functionality.
Any help would be greatly appreciated.
Thanks.
I have in my Info.plist file for 'Required background modes';
App registers for location updates
App downloads content from the network
App downloads content in response to push notifications
My app also has access to location at all times, including the background (successfully tested at other points in the code): NSLocationAlwaysUsageDescription is in the Info.plist file
In my AppDelegate file:
func application(application: UIApplication!, didReceiveRemoteNotification userInfo:NSDictionary!) {
println("New remote notification")
var notification:NSDictionary = userInfo as NSDictionary
var loc: LocationManager?
loc = LocationManager()
loc!.fetchWithCompletion {location, error in
// fetch location or an error
if let myloc = location {
var lat = myloc.coordinate.latitude as Double
var lon = myloc.coordinate.longitude as Double
println("Got location")
} else if let err = error {
println(err.localizedDescription)
}
loc = nil
}
}
If app is not in foreground, make sure to ask for a little time to complete the request with beginBackgroundTaskWithExpirationHandler and then call endBackgroundTask when done.
For more information, see the Executing Finite Length Tasks in the Background Execution chapter of the App Programming Guide for iOS.