My app (prefix "AAS") is basically a game where users lose points every day they don't play. I use UILocalNotifications to alert the user that they've lost points, and invite them back to play. One of my view controllers displays when the points have changed, and it's pretty simple to send out an NSNotification when a UILocalNotification is fired while the app is open).
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
if notification.userInfo != nil {
if let notificationName = notification.userInfo![AASNotification.ActionKey] as? String {
NSNotificationCenter.defaultCenter().postNotificationName(notificationName, object: nil, userInfo: nil)
}
}
}
When the app is reopened after being inactive, one of the classes calculates how many points are lost. Great. Bulletproof, except when the user disallows my app to use NotificationCenter, the app will not be updated if it's open when the notification is supposed to fire. For this case, I wrote my own implementation of a timed notification queue that would mimic UILocalNotification to a certain extent while my app is open. But I thought, someone must have had this problem before, and maybe there is a cocoapod for it.
So my question to the community is, does someone know of a library that dispatches timed NSNotifications? Or a different approach to this problem? Here's my solution, which is barebones and works for the purpose I need:
https://github.com/JamesPerlman/JPScheduledNotificationCenter
I'd love to use one that was coded by a professional and is well tested and feature rich. (I was made aware that this request is off topic for SO.)
Edits:
I want to be able to queue up any amount of NSNotifications to be fired at arbitrary dates. Obviously the NSNotifications can only be received by my app while it is open, that's fine. I do not know the expense of using one NSTimer for each NSNotification (could be hundreds of NSTimers all on the run loop), so my solution only uses one NSTimer at a time. I want the ability to schedule and cancel NSNotifications just like you can do with UINotifications.
You could try NSTimer (NSTimer class reference). In your AppDelegate you can create a method similar to your didReceiveLocalNotification method to execute when the timer is triggered. Also, create an NSUserDefault to store the next time you need to trigger the timer. Finally, at the point where you want to begin the countdown, get the time interval from the current time until the time you want to trigger the event, and set the timer.
So in your AppDelegate, register the default and implement the notifyPlayer:
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
{
let userDefaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.registerDefaults(["alertTime": NSDate()]) //initial value
return true
}
func notifyPlayer() {
// Calculate points and notify relevant viewcontroller to alert player.
let defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
let lastNotificationTime = defaults.objectForKey("alertTime") as! NSDate
let nextNotificationTime = lastNotificationTime.dateByAddingTimeInterval(86400)
defaults.setObject(nextNotificationTime, forKey: "alertTime")
}
}
Now set the timer wherever it makes sense, probably in your app's initial view controller.
class InitialVewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
let savedTime = defaults.objectForKey("alertTime") as! NSDate
let countDownTime = savedTime.timeIntervalSinceNow
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
NSTimer.scheduledTimerWithTimeInterval(countDownTime,
target: appDelegate,
selector: #selector(AppDelegate.notifyPlayer()),
userInfo: nil,
repeats: false)
}
}
It's not perfect, as I haven't tested it, but I think the concept will work for you.
Edit: Just to clarify, this would solve your problem of alerting the user while he is using the app, but won't do anything when the app is not in use. I don't know of any way to send users notification center notifications when permission hasn't been granted.
Related
I've read about this new feature available in iOS 10.3 and thought it will be more flexible and out of the box. But after I read the docs I found out that you need to decide the time to show it and the viewController who calls it. Is there any way I can make it trigger after a random period of time in any viewController is showing at that moment?
In your AppDelegate:
Swift:
import StoreKit
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let shortestTime: UInt32 = 50
let longestTime: UInt32 = 500
guard let timeInterval = TimeInterval(exactly: arc4random_uniform(longestTime - shortestTime) + shortestTime) else { return true }
Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(AppDelegate.requestReview), userInfo: nil, repeats: false)
}
#objc func requestReview() {
SKStoreReviewController.requestReview()
}
Objective-C:
#import <StoreKit/StoreKit.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
int shortestTime = 50;
int longestTime = 500;
int timeInterval = arc4random_uniform(longestTime - shortestTime) + shortestTime;
[NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:#selector(requestReview) userInfo:nil repeats:NO];
}
- (void)requestReview {
[SKStoreReviewController requestReview];
}
The code above will ask Apple to prompt the user to rate the app at a random time between 50 and 500 seconds after the app finishes launching.
Remember that according to Apple's docs, there is no guarantee that the rating prompt will be presented when the requestReview is called.
For Objective - C:
Add StoreKit.framework
Then in your viewController.h
#import <StoreKit/StoreKit.h>
Then in your function call :
[SKStoreReviewController requestReview];
For Swift
Add StoreKit.framework
In your ViewController.swift
import StoreKit
Then in your function call :
if #available(iOS 10.3, *) {
SKStoreReviewController.requestReview()
} else {
// Open App Store with OpenURL method
}
That's it ! Apple will take care of when it would show the rating (randomly).
When in development it will get called every time you call it.
Edited : No need to check OS version, StoreKit won't popup if the OS is less than 10.3, thank Zakaria.
Popping up at a random time is not a good way to use that routine, and is not only in contravention of Apple's advice, but will give you less-than-great results.
Annoying the user with a pop up at a random time will never be as successful as prompting them at an appropriate time- such as when they have just completed a level or created a document, and have that warm fuzzy feeling of achievement.
Taking Peter Johnson's advice, I created a simple class where you can just stick the method in at the desired spot in your code and it'll pop up at a spot where the user's just had a success.
struct DefaultKeys {
static let uses = "uses"
}
class ReviewUtility {
// Default Keys stored in Structs.swift
static let sharedInstance = ReviewUtility()
private init() {}
func recordLaunch() {
let defaults = UserDefaults.standard
// if there's no value set when the app launches, create one
guard defaults.value(forKey: DefaultKeys.uses) != nil else { defaults.set(1, forKey: DefaultKeys.uses); return }
// read the value
var totalLaunches: Int = defaults.value(forKey: DefaultKeys.uses) as! Int
// increment it
totalLaunches += 1
// write the new value
UserDefaults.standard.set(totalLaunches, forKey: DefaultKeys.uses)
// pick whatever interval you want
if totalLaunches % 20 == 0 {
// not sure if necessary, but being neurotic
if #available(iOS 10.3, *) {
// do storekit review here
SKStoreReviewController.requestReview()
}
}
}
}
To use it, stick this where you want it to be called and hopefully you won't tick off users with randomness.
ReviewUtility.sharedInstance.recordLaunch()
Showing the dialog at random time is not probably a good idea. Please see the Apple guideline which mentions: Don’t interrupt the user, especially when they’re performing a time-sensitive or stressful task.
This is what Apple suggests:
Ask for a rating only after the user has demonstrated engagement with your app. For example, prompt the user upon the completion of a game level or productivity task. Never ask for a rating on first launch or during onboarding. Allow ample time to form an opinion.
Don’t be a pest. Repeated rating prompts can be irritating, and may even negatively influence the user’s opinion of your app. Allow at least a week or two between rating requests and only prompt again after the user has demonstrated additional engagement with your app.
This post is also quite interesting...
I cant add comments yet but if you are using Appirater you might want to check the version to see if its lower than 10.3 so the other Appirater review message box pops up.
I have a simple game app, which is programmed with SpriteKit. The Problem is, when a push notifications (SMS,iMessage etc) appears, the game stutters because the update:forScene: method is not called.
To avoid this i want to implement a simple pause menu, which will be shown as soon as a push message comes in.
How can i detect if a push message interrupts the app? In AppDelegate application:willResignActive is also not called.
It would be the best if the game continues when the message comes in, if there is another solution to force the update method to be called.
Had anybody the same Problem?
You should not try to resume your game when an interruption is happening, you should pause it, otherwise its not a good user experience.
For iMessages, phone calls etc you usually use the method you said doesn't work.
I use NSNotificationCenter to pause my games, you can google about it, there is plenty tutorials.
Essentially in your game scene add a NSNotificationCenter observer.
Also create a property for that observers name to avoid typos later on.
let pauseGameKey = "PauseGameKey" // above class so you can access it anywhere in project
class GameScene: SKScene {
// add this in didMoveToView
// in #selector add the method you want to get called
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(yourPauseGameMethod), name: pauseGameKey, object: nil)
}
Than create the willMoveFromView method so you can remove the observer when you transition to another scene (good practice).
override func willMoveFromView(view: SKView) {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
Than in app delegate post the notification when the application will resign.
func applicationWillResignActive(application: UIApplication) {
NSNotificationCenter.defaultCenter().postNotificationName(pauseGameKey, object: nil)
}
For local and remote UINotifications you can additional use these 2 methods in app delegate.
/// Local notification
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
}
/// Remote notification
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
}
Hope this helps
I am trying to understand doing Quick Actions (3D Touch) for iOS 9.
I wanted the user to select 1 of 4 filter to be applied to image, so if I select item 1, I will set the NSUserDefaults.standardUserDefaults() to the filter, then show the correct picture with the applied filter.
In AppDelete.swift:
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
var filterType:Int
switch (shortcutItem.type) {
...set filterType
}
NSUserDefaults.standardUserDefaults().setInteger(filterType, forKey:"filterType")
NSUserDefaults.standardUserDefaults().synchronize()
}
In ViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector:"setDefaultFilter", name: UIApplicationWillEnterForegroundNotification, object:nil) // Handle enter from background
setDefaultFilter()
}
func setDefaultFilter() {
filterType = defaults.integerForKey("filterType")
...
imageView.image = filterImage(defaultImage!, filter:filters[filterType])
}
However, when enter the app from the menu, it will always show the last selection (not the current selection). If I select item 1, nothing happened. I select item 3, item 1 will appeared.
I have also try passing parameters via appDelegate and the result is the same. I believe there are some issues with life cycle.
Any ideas?
NSUserDefaults write data on flash, which may not be so fast.
You can wait a little longer, like observe UIApplicationDidBecomeActiveNotification other than UIApplicationWillEnterForegroundNotification.
Or you can use other ways to pass params, e.g., as an instance variable in AppDelegate.
didFinishLaunchingWithOptions method is always called before calling performActionForShortcutItem method to response to the quick action.
So, I think that you need to check what kind of quick action is selected in didFinishLaunchingWithOptions method. If the app is not launched from quick action, you just continue to your normal app launching process.(default filter)
And if you decide to handle quick action in didFinishLaunchingWithOptions, you have to return NO in didFinishLaunchingWithOptions.
You could get more idea from my demo project:
https://github.com/dakeshi/3D_Touch_HomeQuickAction
I'm trying to use local notification but something is not working.
I have a class notification that handle all the code related to the notifications.
It's apparently working. What is not working is the way I try to trigger my notification.
When the user clicks on the home button, I call my notification class that starts a NSTimer. It repeats every second, and each 10 seconds I call a webservice.
Everything works great on my simulator, but it doesn't work on my real iPhone.
Here the code:
//as a class variable
let notif = Notification()
func applicationDidEnterBackground(application: UIApplication) {
notif.triggerTimer()
}
The notification class
class Notification: NSObject, WsOrderStatusProtocol, WsPinRequestProtocol {
var timer = NSTimer()
var time = 0
var sendNotification:Bool = true
var wsos = WsOrderStatus()
var wsoc = PinRequest()
override init() {
super.init()
self.wsos.delegate = self
self.wsoc.delegate = self
}
func triggerTimer() {
print("log INFO : class Notification, methode : triggerTimer")
NSNotificationCenter.defaultCenter().addObserver(self, selector:"orderCoupon:", name: "actionOrderCouponPressed", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector:"cancelTimer:", name: "actionCancelTimerPressed", object: nil)
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("launchNotification"), userInfo: nil, repeats: true)
}
func launchNotification() {
print("log INFO : class Notification, methode : launchNotification")
time += 1
print("time \(time)")
if time % 10 == 0 {
print("modulo 10")
wsos.getOrderStatus()
}
}
}
In the simulator, I see the logs et the logs that counts to 10 etc, but with my real iphone, I only see the first log "print("log INFO : class Notification, methode : triggerTimer")" then nothing...
Do you know why ?
As Paul says in his comment, your app only spends a very brief time in the background before being suspended. Suspended means that your code doesn't run at all any more, so timers stop.
The simulator doesn't always follow the same rules. When its behavior is different than that of a device then ignore it. It lies.
If you want to have more time to do work in the background, you can ask for it using the method beginBackgroundTaskWithExpirationHandler. Make that call in your applicationDidEnterBackground method.
From testing I've found that that gives you 3 minutes of extra time. After that your expiration handler block gets executed and then you get suspended.
Apple does not want your app running indefinitely from the background. It drains the battery.
I've found that it is possible to lie and tell the system that you are an app that plays sounds from the background, and write your expiration handler to play a short "sound of silence" and then ask for another background task using beginBackgroundTaskWithExpirationHandler. However, doing that will get you rejected from the app store.
I create a UILocationNotification with two action button one call sleep and wake up now. So once the user sees the notification if they pressed wake up now the app will launch and execute some code for some reason the app launches then refuse to execute the codes.
FYI : The code for the UILocalNotification were implement and they are working, the only problem is when I pressed the wake up now button.
func application(application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, completionHandler: () -> Void) {
if notification.category == "options" {
if identifier == "Sleep"{
println("sleep more lazy bumm")
}
else if identifier == "wakeup"{
var object = ViewController()
object.wakeupnow()
}
}
Second Approach I took but it still not working
func application(application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, completionHandler: () -> Void) {
if notification.category == "options" {
if identifier == "Sleep"{
println("sleep more lazy bumm")
}
else if identifier == "wakeup"{
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("wake"), name: UIApplicationWillEnterForegroundNotification, object: nil)
}
}
fun wake(){
var alertview = UIAlertView()
alert.message = "Good job you are up now, so lets get to work"
alert.addButtonWithTitle("ok")
alert.cancelButtonIndex = 0
alert.show()
}
Remember that application:handleActionWithIdentifer:forLocalNotification:completionHandler: gets called in a background thread. If you're doing anything in the UI you'll need to do it on the main queue.
Also, you have to call the completionHandler block as soon as you can, or the system will kill your app.
Please see the Apple documentation about this: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplicationDelegate_Protocol/#//apple_ref/occ/intfm/UIApplicationDelegate/application:handleActionWithIdentifier:forLocalNotification:completionHandler:
Also, in your first example you are instantiating a view controller and calling a function on it but you aren't actually presenting the view controller - what do you want to happen with that view controller? You'll need to do something to make it appear, which will depend on how your app is structured. You may want to present it modally over your root navigation controller, for example. Or maybe you mean to be calling that function on your already-existing ViewController, in which case you need to keep a reference to it somewhere instead of instantiating a new one when the notification action is triggered.
In your second example, you are adding yourself as an observer to the NSNotificationCenter instead of actually posting a notification, so of course your function will never get called. If you want to take that approach, you need to call addObserver sometime earlier - in applicationDidFinishLaunching:, for example:
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("wake"), name: "wakeup", object: nil)
Then, in your handleActionWithIdentifier: function, call:
NSNotificationCenter.defaultCenter().postNotificationName("wakeup", object: nil)
That should result in your wake function being called. You still have the problem of trying to show an alert view from a background thread, though, so you would need to wrap your call in a dispatch_async:
func wake(){
var alertview = UIAlertView()
alert.message = "Good job you are up now, so lets get to work"
alert.addButtonWithTitle("ok")
alert.cancelButtonIndex = 0
dispatch_async(dispatch_get_main_queue(),{
alert.show()
})
}
Incidentally, UIAlertView is deprecated and should be replaced by UIAlertController for iOS 8 and up.