How can I make iOS background app notify based on location info - ios

I am trying to implement a location-baed navigation app on iOS. I will have a device in my pocket and want it notifies when criteria are met based on location information, such as Google Maps navigation notifies when you make a turn on street.
I assume I can make it with CoreLocation and local notification.
Here are what I have done, expect and actually got.
XCode Version 7.1 (7B91b) on OS X El Capitan 10.11.1
In Xcode, File -> New -> Project -> iOS Application -> Single View Application
Add CoreLocation framework to project
Enable "Location updates" background mode
Add NSLocationAlwaysUsageDescription key with value of "hello" to info.plist
Leave ViewController.swift untouched
Edit AppDelegate.swift as follows;
import UIKit
import CoreLocation
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, CLLocationManagerDelegate {
var window: UIWindow?
lazy var locationManager: CLLocationManager! = {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.pausesLocationUpdatesAutomatically = false
manager.delegate = self
manager.requestAlwaysAuthorization()
return manager
}()
var lastNotifictionDate = NSDate()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let settings = UIUserNotificationSettings(forTypes: [.Alert], categories: nil)
UIApplication.sharedApplication().registerUserNotificationSettings(settings)
locationManager.startUpdatingLocation()
return true
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// Do something interesting.
// I make this function return immediately until 10sec passed after previous notification.
let now = NSDate()
if lastNotifictionDate.dateByAddingTimeInterval(10).compare(now) == .OrderedDescending {
//NSLog("Not yet")
return
}
//NSLog("It's time")
lastNotifictionDate = now
// Fire local notification, without sound configured for simplicity.
let notification = UILocalNotification()
notification.alertBody = "Hello"
UIApplication.sharedApplication().presentLocalNotificationNow(notification)
}
// ... other methods are untouched ...
}
The app works on simulator as I expect, namely;
Run the app in iPhone 5 Simulator and allow location and notification services
Press home button (Shift-Command-H).
In the simulator menu, Debug -> Location -> Free drive
The app displays a local notification alert every 10sec
The app acts different on my device, iPhone 5 with iOS 9.1;
Run the app via Xcode and allow location and notification services
Pressed home button
The device does not display notification alert either on home screen or notification center.
Then, I tap the app icon on the device to bring the app foreground, opened the notification center and found two notifications. I expect 5 notifications newly appear if I wait for 50 seconds, but no notification newly appear.
I understand Simulator behaves differently than the actual devices. For example, Simulator shows NSLog() and print() outputs on console during background, while the actual device does not.
I guess I did not configure something for devices or I am trying to do in unfeasible way, but I cannot find what it is. Could you please tell me what is wrong and how I can fix it?

Setting CLLocationManager's allowsBackgroundLocationUpdates to true solved my problem.
manager.allowBackgroundLocationupdates = true
If it is false (default), updates stops when suspended. Updates occur when foreground or background.

Related

Terminated App only being restarted one time

I have an application that by design should be activated by the OS after a location event is delivered from the system even if the app has been terminated. Terminated can mean terminated by the system or by the user in the multitasking switcher. My app delegate code is below and the location delegate is an extension of the app delegate class. The first event that is generated by the system after the app is terminated is delivered to my app and processed properly, but I am not receiving any subsequent events. Is there some code I need to run after the event is received to reregister my application for future updates?
var window: UIWindow?
var locationManager: CLLocationManager!
var notificationCenter: UNUserNotificationCenter?
var today:String? //Today's date format: YYYY-MM-DD
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if launchOptions?[UIApplicationLaunchOptionsKey.location] != nil {
self.locationManager = CLLocationManager()
self.locationManager.delegate = self
self.locationManager.requestAlwaysAuthorization()
self.locationManager.startMonitoringVisits()
self.locationManager.allowsBackgroundLocationUpdates = true
sendTestNotification()
} else {
print("normal launch")
}
You say your app has been "terminated". This suggests that you kill your app intentionally as a way of testing. But if an app has been forcibly terminated, the system deliberately stops background location monitoring, so naturally you won't get any more visit monitoring events. It is as if you had called stopMonitoringVisits yourself.
app terminate is not allow by apple as i know.
exit(0)
you can use that code line to app terminate

Is it possible to open my iOS app remotely?

I'm pretty sure I already know what the outcome of this attempt is going to be, but before I start going through a lot of effort for nothing, I should probably just ask someone about this. Here's what I want to try:
I'm developing an iOS app in Swift, and I just wrote a PHP script to send a silent push notification to my device. In this silent notification, I'm passing along instructions to have my app delegate open the app using the UIApplication.shared.openURL(url:) method. This PHP script will only be run if a user taps a certain button on an online web page, which can only be accessed when the user is using Safari web browser on his iPhone device with my app already running in the background, so there's no chance that anyone will be able to trigger the PHP script from any other device than an iPhone which already has my app installed and running in the background. If you're wondering why I would use this workaround while I can also just use something as simple as URL schemes or deep linking, well, it's quite simple: first of all, deep linking requires the user to confirm that he wants my app to open before it actually opens. I don't want this, instead, I want my app to open automatically as soon as the button is tapped. Second, after the app is opened through deep linking or universal links, there's this really annoying breadcrumb button in the status bar, which shouldn't be there anymore after my user transitions from the web page to my app. I've tried everything to get rid of the confirmation prompt and breadcrumb button, and this is the last thing I can come up with. Is it possible to trigger the openURL method using a remote notification, even when the app is already open and running in the background?
you can't open / bring to foreground an app without user interaction
You can use silent push notification ( Pushkit ).
It will also work in background and with app terminated mode.
Once you receive silent push notification, you have to schedule local notification with interactive buttons in notification.
As per user tap, your app will be open and with tracking on didFinishLaunchingWithOption you can open specific URL
Note - Without user interaction, you would not be able to open specific URL in safari.
You can refer below code for same.
import UIKit
import PushKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate,PKPushRegistryDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let types: UIRemoteNotificationType = [.Alert, .Badge, .Sound]
application.registerForRemoteNotificationTypes(types)
self.PushKitRegistration()
return true
}
//MARK: - PushKitRegistration
func PushKitRegistration()
{
let mainQueue = dispatch_get_main_queue()
// Create a push registry object
if #available(iOS 8.0, *) {
let voipRegistry: PKPushRegistry = PKPushRegistry(queue: mainQueue)
// Set the registry's delegate to self
voipRegistry.delegate = self
// Set the push type to VoIP
voipRegistry.desiredPushTypes = [PKPushTypeVoIP]
} else {
// Fallback on earlier versions
}
}
#available(iOS 8.0, *)
func pushRegistry(registry: PKPushRegistry!, didUpdatePushCredentials credentials: PKPushCredentials!, forType type: String!) {
// Register VoIP push token (a property of PKPushCredentials) with server
let hexString : String = UnsafeBufferPointer<UInt8>(start: UnsafePointer(credentials.token.bytes),
count: credentials.token.length).map { String(format: "%02x", $0) }.joinWithSeparator("")
print(hexString)
}
#available(iOS 8.0, *)
func pushRegistry(registry: PKPushRegistry!, didReceiveIncomingPushWithPayload payload: PKPushPayload!, forType type: String!) {
// Process the received push
}
}
Refer some more material for push kit integration.

CLCircularRegion and wake up app

In application we have mechanism like native Reminder app in iOS with firing notifications when user enter or exit in some region.
But two devices behave differently (5 and 5s) in same time. All devices have enable notifications, and allow use locations.
Two devices have a some "travel" and in the route created 10 points. First device (5) when came to finish received only 6 notifications, (5s) don't receive any notification.
But my question is how I can know when my app is restart in background or continue working. Because, all log in app I redirect into a file, and after download container and analyze what happened in app in travel time.
I noticed app restart in same times when device is enter to region and my log marks fired in the file but notifications don't receive. This is happended when app try to get some information from web service in didFinishLaunchingWithOptions
And maybe this is problem. How to know distinguish restart app or continue working. Thx.
Are you checking UIApplicationLaunchOptionsLocationKey in didFinishLaunchingWithOptions similar to (sorry, Swift is what I have now):
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if launchOptions?[UIApplicationLaunchOptionsLocationKey] != nil {
// app was launched in response to incoming location event
}
}
Additionally, if you're not already doing this you may need to create notifications differently if app is in background:
// Show an alert if application is active
if UIApplication.sharedApplication().applicationState == .Active {
if let message = notefromRegionIdentifier(region.identifier) {
if let viewController = window?.rootViewController {
showSimpleAlertWithTitle(nil, message: message, viewController: viewController)
}
}
}
else {
// Otherwise present a local notification:
let notification = UILocalNotification()
notification.alertBody = notefromRegionIdentifier(region.identifier)
notification.soundName = "Default";
UIApplication.sharedApplication().presentLocalNotificationNow(notification)
}

startMonitoringSignificantLocationChanges not working in swift

I'd added CLLocationManager in my app using Swift in the AppDelegate file.
In the Appdelegate.swift file,
import CoreLocation
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, CLLocationManagerDelegate {
var locationManager: CLLocationManager!
In the didbecomeActive method:
func applicationDidBecomeActive(application: UIApplication) {
if((locationManager) != nil)
{
locationManager.stopMonitoringSignificantLocationChanges()
locationManager.delegate = nil
locationManager = nil
}
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.activityType = CLActivityType.OtherNavigation
locationManager.requestAlwaysAuthorization()
locationManager.startMonitoringSignificantLocationChanges()
}
If I use startUpdatingLocation, didUpdateLocations method gets called, but not for startMonitoringSignificantLocationChanges.
Why it's not getting called for startMonitoringSignificantLocationChanges.
I'm testing this in ios simulator. I don't know how to check in device.
It's working, but it's really hard to trigger significant location changes - it usually happens when the device is changing cell towers - I don't think it's possible to do with the simulator.
You'd probably have to get on a bike/car and travel AT LEAST a few kilometres.
There's a trick you can use though, that will trigger significant location change:
Switch Airplaine mode in your iPhone on and off with a few second intervals repeatedly, it should trick the device into thinking that it changed cell towers and trigger the significant location change.
In your simulator, goto Debug->Location->Custom and change location, then test it.
In your simulator, select Features -> Location -> Freeway Drive
Wait a bit for startMonitoringSignificantLocationChanges to trigger didUpdateLocations.
Note:
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. startMonitoringSignificantLocationChanges()

Local notifications sent to background works in simulator but not on device

In the simulator I'm able to get the exact result I want: When I trigger a notification and my simulator phone is locked, then the notification gets pushed to the watch.
This used to work on device (my iPhone 6 on iOS 9.1 and Watch on watchOS 2.0). For some reason it stopped working and I don't know how to debug the problem.
So on device, I make sure that the app is in background by going to home screen and locking the phone and making sure my watch is asleep. When the notification is triggered, nothing happens. When I open up the app, that's when the notifications finally gets triggered. I.E. if I trigger 3 notifications, none of them register until I open the app back into foreground. This is not how it works in simulator (simulator has correct behavior described above).
The notification is triggered by me changing a text object that is stored in my Firebase db. The change calls the sendNotification function. Again, this works perfectly fine in the simulator and used to work on device but for some reason doesn't work anymore.
In app delegate I have:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
UIApplication.sharedApplication().registerUserNotificationSettings(UIUserNotificationSettings(forTypes: .Alert, categories: nil))
// Setting up Local Notification
let assistAction = UIMutableUserNotificationAction()
assistAction.activationMode = UIUserNotificationActivationMode.Background
assistAction.identifier = categoryID
assistAction.title = "Assist"
assistAction.authenticationRequired = false
assistAction.destructive = false
// Category
let assistCategory = UIMutableUserNotificationCategory()
assistCategory.identifier = categoryID
assistCategory.setActions([assistAction], forContext: UIUserNotificationActionContext.Default)
// Only way to get this to work with parameter
let categories = NSSet(object: assistCategory)
// Settings
let settings = UIUserNotificationSettings(forTypes: [.Alert, .Badge, .Sound], categories: categories as? Set<UIUserNotificationCategory>)
UIApplication.sharedApplication().registerUserNotificationSettings(settings)
// UIApplication.sharedApplication().registerForRemoteNotifications()
return true
}
Then in my view controller I have:
func sendNotification(customerName: String, helpText: String) {
// Local Notification
let notification = UILocalNotification()
notification.alertBody = "\(customerName) \(helpText)"
notification.soundName = UILocalNotificationDefaultSoundName
notification.fireDate = NSDate()
notification.category = categoryID
UIApplication.sharedApplication().presentLocalNotificationNow(notification)
print("Sent notification");
}
Simulator seems to work differently than device. It's weird that simulator local notifications was acting like remote push notifications.
I ended up giving up on trying to make local notifications show up while app is in background on device because I've learned that that is not the expected behavior.
The correct solution is to just set up remote push notification, which I used Parse to do. And now it's working as expected. I have a web server that uses the Parse REST API to send a POST request to Parse and my iOS app is then set up to receive remote push notifications which now show up on my watch when my phone is locked.

Resources