I'm trying to determine different scenarios of UIApplication didReceiveLocalNotification:. If a user clicks the notification the while app is inactive, I should bring them to the corresponding UI page. Otherwise, if user just opened the app without tapping a notification, I should let them stay in the UI page which they left off from.
However, there is a little problem as the UILocalNotification was scheduled by myself.
[[UIApplication sharedApplication] scheduleLocalNotification:scheduledAlert];
So every time it fired by iOS, it will call the same delegate method as the callback when I manually clicked the notification on the status bar:
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { ... }
I tried some tricky hack such as counting the fired times while app is inactive mode, counting 1 means iOS firing, and counting 2 means user clicked, and relative counting management code.
But I don't think this could be the best practice. I checked the iOS Messages application, which has the same functionality. If you put the app into inactive mode, such as scroll down the status bar, then it can determine the UILocalNotification is from your touch (and will open the text editing mode) or just you return back to the app (stay in the previous status).
I would appreciate if you guys could let me know what the best solution is here!
I use a custom key-value pair which I set a NSMutableDictionary, which I then assign to UILocalNotification.userInfo, to tell different scenarios.
I check the app state so that I only handle the notification if I'm coming from the background:
UIApplicationState state = [application applicationState];
if (state == UIApplicationStateInactive) {
// Your Code Here
} else {
}
Related
I show my local notification like this periodically.
UILocalNotification *notification = [[UILocalNotification alloc]init];
[notification setAlertBody:#"Test test"];
[notification setUserInfo:#{#"test": #"test"}];
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
I need to detect back that notification and I plan to write here.
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
It always call that function whether user tap on notification or it automatically call in foreground.
So, I separate using this.
if (application.applicationState == UIApplicationStateActive)
When I show notification center, it become InActive. But, it still call didReceiveLocalNotification. I can't differentiate whether user tap on notification from notification center or because of my periodic posting notification.
How can I really know that I tap on notification (Either from InActive State or Background State) in didReceiveLocalNotification?
Assuming that I understood your issue correctly, I stumbled on the same obstacle and couldn't find a super clean solution.
So the situation where the following method
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
is called and applicationState is equal to UIApplicationStateInactive happens in two cases:
the app is in the foreground and the notification has just been fired
the notification has been fired some time ago, notification center is pulled down and user tapped on the notification
One way to distinguish these two cases is to check the notification's fireDate:
notification.fireDate.timeIntervalSinceNow < 0.5
If this expression is true, it's very likely that the first case happened. If the expression is false, it's very likely that the second case happened.
This solution depends on the system delivering the notification without delay and/or the user not being fast enough to click the notification in the notification center under 500ms since the notification's firing. I'm not sure how likely is it for a firing delay to happen. I guess it's possible if the device is under some kind of processing load.
I hope there is a cleaner solution, hopefully someone will share it.
First of all, read this from Apple Documentation:
The user taps a custom action button in an iOS 8 notification. In this
case, iOS calls either
application:handleActionWithIdentifier:forRemoteNotification:completionHandler:
or
application:handleActionWithIdentifier:forLocalNotification:completionHandler:.
In both methods, you get the identifier of the action so that you can
determine which button the user tapped. You also get either the remote
or local notification object, so that you can retrieve any information
you need to handle the action.
The user taps the default button in the alert or taps (or clicks) the
app icon. If the default action button is tapped (on a device running
iOS), the system launches the app and the app calls its delegate’s
application:didFinishLaunchingWithOptions: method, passing in the
notification payload (for remote notifications) or the
local-notification object (for local notifications). Although
application:didFinishLaunchingWithOptions: isn’t the best place to
handle the notification, getting the payload at this point gives you
the opportunity to start the update process before your handler method
is called.
Second, this is how you can differentiate whether didReceiveLocalNotification: is called from active or inactive state:
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
UIApplicationState appState = UIApplicationStateActive;
if ([application respondsToSelector:#selector(applicationState)])
appState = application.applicationState;
if (appState == UIApplicationStateActive)
{
}
else
{
}
}
application:didReceiveLocalNotification:
Sent to the delegate when a running app receives a local notification.
Check this:
iOS UILocalNotification - No delegate methods triggered when app is running in background and the icon is clicked upon notification
Use KVO key-value-observing to know and do something when the button is tapped.
Here is the situation that I want to handle quoted from Apple's documentation.
As a result of the presented notification, the user taps the action button of the alert or taps (or clicks) the application icon.
If the action button is tapped (on a device running iOS), the system launches the application and the application calls its delegate’s application:didFinishLaunchingWithOptions: method (if implemented); it passes in the notification payload (for remote notifications) or the local-notification object (for local notifications).
If the application icon is tapped on a device running iOS, the application calls the same method, but furnishes no information about the notification . If the application icon is clicked on a computer running OS X, the application calls the delegate’s applicationDidFinishLaunching: method in which the delegate can obtain the remote-notification payload.
How do I handle this situation if there is no information about the notification?
If I understand you correctly, it sounds like you have a UILocalNotification that has been fired, but you need to still handle it if the user taps the application icon instead of the notification. Correct?
If this is the case, then to my knowledge you won't be able to handle the notification from the app delegate, because the app is not being launched or brought out of the background by the notification, but instead by the user's interaction.
However, if you are setting a badgeNumber on the application with the notification then you could try something like this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
if (launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]) {
UILocalNotification *notification = launchOptions[UIApplicationLaunchOptionsLocalNotificationKey];
} else if ([UIApplication sharedApplication].applicationIconBadgeNumber > 0) {
// Assume that user launched the app from the icon with a notification present.
}}
You may also have to check the badgeNumber in - (void)applicationDidBecomeActive:(UIApplication *)application as well.
Improve to #Aron Crittendon answer:
Consider also to handle that in applicationDidBecomeActive:
-(void)applicationDidBecomeActive:(UIApplication *)application
{
if ([UIApplication sharedApplication].applicationIconBadgeNumber > 0) {
//application is in background, fired notification and user tapped app icon with badge
}
}
As the documentation states, if you tap the icon on iOS (and not the notification's alert/banner) then the same method is called but you get no notification information. There is no way to handle a local notification simply by tapping the app icon.
I have a button which when pressed creates an object thing. It then sleeps for ten seconds and then calls thing.go which gets the application state like
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
Then following this question's answer and comments I check if the app is in the background, and if it is, it displays a local notification.
So I press the button and immediately press the home key to go to the main screen (thereby putting the app in the background)
However, I can tell that the if statement is returning false and is therefore not executing any code within (by putting an NSLog inside the if statement).
So my next thought process was to somehow display the date in the console using NSLog(state). Obviously I cannot do this.
So how can I display the applicationState to resolve my issue? Or is there another way I can check to see if the app is running in the background within this class's method?
Here is the go function which is called inside the IBAction when the button is pressed
-(void)go {
if (state == UIApplicationStateBackground) {
NSLog(#"App is in background");
}
}
Here is the IBAction which is linked to the button, i.e. runs when button is pressed
- (IBAction)button_help {
myclass* thingy = [[myclass alloc] init];
sleep(10);
thingy.go;
}
You should pretty much NEVER use sleep. Forget it exists. It locks up your app, and nothing changes, including transitions to the background.
What you want to do is to set a timer for 10 seconds that then invokes thing.go.
However, that's going to be complicated by the fact that normally timers don't run in the background. In fact, unless you take special steps, your app doesn't get any processor time in the background. You get told that you are going to the background, and then the next call you get is the return-to-foregraound.
When your app gets a message that it is being sent to the background, it will need to make the system call that asks for background processing time. (I don't remember the call off the top of my head, and don't have Xcode running at the moment.)
Apple changed the rules for background processing in iOS 7, but you say this is iOS 6, so those changes don't apply.
If I understand correctly, you're checking to see if the app is returning from a local notification or if it was already active?
if (state == UIApplicationStateActive) {
// do something
NSLog(#"application was active ");
} else {
NSLog(#"sent from notification");
}
The app receives a UILocalNotification, but if the user is at the UITableViewController at the fire time, the table view (containing the scheduled notifications) does not reload. The user has to get out of that view and load the view again so that the cells are loaded and, as the notification was already fired, it will not be displayed on any cell of that table view.
Problem is: If the user touches the specific tableView cell that contained the notification that just fired, the app crashes, cause the notification is not there anymore.
I've implemented the - (void)reloadData in every place possible, and it still doesn't load in real time.
What would be a better solution for this?
Other detail, how can I push a specific view after the notification is displayed (when the user slides the app icon when the phone is locked)?
Any help will be truly appreciated, since theres are the last details remaining to publish my first App.
The problem you describe is caused by the current local notification: While the notification is handled, it is still in the list of scheduled notifications, so refreshing the table view will have no effect. A solution to this problem is to defer reloading the table view until after the notification is handled, something like
// In your app delegate
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.localNotificationsController reloadDataIfNeeded];
});
}
// In your controller that shows the local notifications
- (void)reloadDataIfNeeded
{
if (![self isViewLoaded]) return;
[self.tableView reloadData];
}
You could also delete the notification from the list if you only use notifications that only fire once (so that you are sure that the notification will disappear anyway):
// In your app delegate
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
[application cancelLocalNotification:notification];
[self.localNotificationsController reloadDataIfNeeded];
}
As to the second part of your question ("how can I push a specific view after the notification is displayed (when the user slides the app icon when the phone is locked?"), there are two scenarios how an application can be activated by a local notification.
Your application was suspended, but still in memory. Then selecting the local push notification will make the app enter the foreground, -application:didReceiveLocalNotification: will be called, and [application applicationState] will be UIApplicationStateInactive
Your application is not running, i.e. not suspended, not in memory. Then you will receive the local notification in the launchOptions in -application:didFinishLaunchingWithOptions::
UILocalNotification *localNotification = [launchOptions valueForKey:UIApplicationLaunchOptionsLocalNotificationKey];
Despite what the documentation says, -application:didReceiveLocalNotification: will not be called in this case.
So to show the notification that woke up the application, you can push your controller in these two cases.
I have an app that needs to do something when it’s sent to background using the Home button and something else when the device is locked using the top hardware button. The standard way of solving these requirements are the notifications and delegate methods sent out by UIApplication. On iOS 4 they look like this:
// Pressing the home button
Will resign active.
Did enter background.
// Tapping app icon on Springboard
Will enter foreground.
Did become active.
// Pressing the lock button
Will resign active.
// Unlocking the device
Did become active.
In other words, it’s quite easy to tell between locking and backgrounding. On iOS 5 the behaviour changed:
// Pressing the home button
Will resign active.
Did enter background.
// Tapping app icon on Springboard
Will enter foreground.
Did become active.
// Pressing the lock button
Will resign active.
Did enter background.
// Unlocking the device
Will enter foreground.
Did become active.
Notice that the didEnterBackground and willEnterForeground notifications are now sent out even when (un)locking the device, making it impossible to tell between locking and backgrounding. Is this change documented somewhere? Is it a regression? Do you know another way to distinguish the two cases?
iOS 6
In my preliminary testing via the simulator, checking the application state with
[[UIApplication sharedApplication] applicationState]
in either
- (void)applicationWillEnterForeground:(UIApplication *)application
- (void)applicationDidEnterBackground:(UIApplication *)application
allows you to differentiate between a call to lock the device and just switching back to the homescreen. A lock screen will return 1 (UIApplicationStateInactive), whereas a home button press will register as a 2 (UIApplicationStateBackground).
It seems consistent and should work on an iOS device just as reliably as it does in the simulator.
iOS 7
The iOS 6 method no longer works in iOS 7. In order to do this now, you have to utilize CFNotificationCenter and listen for a darwin notification (labeled: com.apple.springboard.lockcomplete). You can find the github repo with the sample project here: https://github.com/binarydev/ios-home-vs-lock-button
Credit for the iOS 7 fix goes out to wqq
I have looked into this quite a bit so I would love to be wrong here if someone knows something I don't, but technically, there is no documented way to tell the difference between locking the device, and sending to background.
One thing you can check however, is the UIApplicationState during the transition from foreground to background. Locking a device will give UIApplicationStateInactive and moving the App to the background will give UIApplicationStateBackground. But, since this behaviour is not officially documented it may change in the future.
A basic example:
- (void)applicationDidEnterBackground:(UIApplication *)application {
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
NSLog(#"Device state: %#", state);
switch (state) {
case UIApplicationStateActive:
/* ... */
break;
case UIApplicationStateInactive:
/* Device was/is locked */
break;
case UIApplicationStateBackground:
/* User pressed home button or opened another App (from an alert/email/etc) */
break;
}
}
UIApplicationState - The running states of an application
typedef enum {
UIApplicationStateActive,
UIApplicationStateInactive,
UIApplicationStateBackground
}
UIApplicationState
Constants
UIApplicationStateActive - The application
is running in the foreground and currently receiving events. Available
in iOS 4.0 and later.
UIApplicationStateInactive - The application is running in the
foreground but is not receiving events. This might happen as a result
of an interruption or because the application is transitioning to or
from the background.
UIApplicationStateBackground - The application is
running in the background.
According to the UIApplicationDelegate Protocol Reference:
applicationWillResignActive:
didEnterBackground:
// ...
willEnterForeground:
applicationDidBecomeActive:
are the only methods that ever get called in both situations.
According to the iOS 4.3 to iOS 5.0 API Diff, these are the ONLY changes regarding UIApplication or UIApplicationDelegate, so I couldn't find where they documented any of these notification changes:
UIApplication.h
Added -[UIApplication setNewsstandIconImage:]
Added UIApplication.userInterfaceLayoutDirection
Added UIApplicationDelegate.window
Added UIApplication(UINewsstand)
Added UIApplicationLaunchOptionsNewsstandDownloadsKey
Added UIRemoteNotificationTypeNewsstandContentAvailability
Added UIUserInterfaceLayoutDirection
Added UIUserInterfaceLayoutDirectionLeftToRight
Added UIUserInterfaceLayoutDirectionRightToLeft
This is more of a workaround/hack, but according to my experience it's very reliable.
When the device is screen-locked (not just home button-ed, if that's a word :)) - bound network (UDP) sockets are broken.
I was using GCDAsyncUDPSocket (also AsyncUDPSocket before) and they both fire a network/broken pipe error reliably when the device is turned off.
In my case I need the UDP socket anyway, for other apps it might be a bit smelly, however, just binding/listening on a UDP socket without any action is not too terrible if you really need to differentiate here.
This note will [self destruct]; is 5 minutes (so Apple won't find out).
There’s a thread about this issue on Apple Developer Forums (registered developers only, sorry). The gist is that the new behaviour is by design. There are requests for a new API feature to distinguish between the two use cases, but nothing working yet.
Here is what Apple's iOS Programming Guide says:
Pressing the Sleep/Wake button is another type of interruption that
causes your app to be deactivated temporarily. When the user presses
this button, the system disables touch events, moves the app to the
background but sets the value of the app’s applicationState property
to UIApplicationStateInactive (as opposed to
UIApplicationStateBackground), and finally locks the screen.
http://developer.apple.com/library/ios/#DOCUMENTATION/iPhone/conceptual/iPhoneOSProgrammingGuide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html
So, you should check the UIApplication's applicationState property in applicationDidEnterBackground:. If it is UIApplicationStateBackground the user pressed the home button. But if it is UIApplicationStateInactive the user locked the device.