Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

*Multitasking aware and can track when the app is brought into the fo…

…reground.

*Added ability to dynamically control when rating pop ups shows.
*Launches are now replaced by 'uses'.
*Added support for tracking 'significant events'
*More information in README.md and Appirater.h comments
*Removed non-markdown README
  • Loading branch information...
commit 3d1fc366492d7395d217b75ecdd66fbf185cfefb 1 parent cdfba2f
Arash Payan authored
Showing with 320 additions and 90 deletions.
  1. +84 −6 Appirater.h
  2. +210 −63 Appirater.m
  3. +0 −21 README
  4. +26 −0 README.md
View
90 Appirater.h
@@ -36,8 +36,9 @@
#import <Foundation/Foundation.h>
-extern NSString *const kAppiraterLaunchDate;
-extern NSString *const kAppiraterLaunchCount;
+extern NSString *const kAppiraterFirstUseDate;
+extern NSString *const kAppiraterUseCount;
+extern NSString *const kAppiraterSignificantEventCount;
extern NSString *const kAppiraterCurrentVersion;
extern NSString *const kAppiraterRatedCurrentVersion;
extern NSString *const kAppiraterDeclinedToRate;
@@ -82,13 +83,39 @@ extern NSString *const kAppiraterDeclinedToRate;
Users will need to have the same version of your app installed for this many
days before they will be prompted to rate it.
*/
-#define DAYS_UNTIL_PROMPT 30 // double
+#define APPIRATER_DAYS_UNTIL_PROMPT 30 // double
/*
- Users will need to launch the same version of the app this many times before
- they will be prompted to rate it.
+ An example of a 'use' would be if the user launched the app. Bringing the app
+ into the foreground (on devices that support it) would also be considered
+ a 'use'. You tell Appirater about these events using the two methods:
+ [Appirater appLaunched:]
+ [Appirater appEnteredForeground:]
+
+ Users need to 'use' the same version of the app this many times before
+ before they will be prompted to rate it.
+ */
+#define APPIRATER_USES_UNTIL_PROMPT 20 // integer
+
+/*
+ A significant event can be anything you want to be in your app. In a
+ telephone app, a significant event might be placing or receiving a call.
+ In a game, it might be beating a level or a boss. This is just another
+ layer of filtering that can be used to make sure that only the most
+ loyal of your users are being prompted to rate you on the app store.
+ If you leave this at a value of -1, then this won't be a criteria
+ used for rating. To tell Appirater that the user has performed
+ a significant event, call the method:
+ [Appirater userDidSignificantEvent:];
*/
-#define LAUNCHES_UNTIL_PROMPT 15 // integer
+#define APPIRATER_SIG_EVENTS_UNTIL_PROMPT -1 // integer
+
+/*
+ Once the rating alert is presented to the user, they might select
+ 'Remind me later'. This value specifies how long (in days) Appirater
+ will wait before reminding them.
+ */
+#define APPIRATER_TIME_BEFORE_REMINDING 1 // double
/*
'YES' will show the Appirater alert everytime. Useful for testing how your message
@@ -100,6 +127,57 @@ extern NSString *const kAppiraterDeclinedToRate;
}
+/*
+ DEPRECATED: While still functional, it's better to use
+ appLaunched:(BOOL)canPromptForRating instead.
+
+ Calls [Appirater appLaunched:YES]. See appLaunched: for details of functionality.
+ */
+ (void)appLaunched;
+/*
+ Tells Appirater that the app has launched, and on devices that do NOT
+ support multitasking, the 'uses' count will be incremented. You should
+ call this method at the end of your application delegate's
+ application:didFinishLaunchingWithOptions: method.
+
+ If the app has been used enough to be rated (and enough significant events),
+ you can suppress the rating alert
+ by passing NO for canPromptForRating. The rating alert will simply be postponed
+ until it is called again with YES for canPromptForRating. The rating alert
+ can also be triggered by appEnteredForeground: and userDidSignificantEvent:
+ (as long as you pass YES for canPromptForRating in those methods).
+ */
++ (void)appLaunched:(BOOL)canPromptForRating;
+
+/*
+ Tells Appirater that the app was brought to the foreground on multitasking
+ devices. You should call this method from the application delegate's
+ applicationWillEnterForeground: method.
+
+ If the app has been used enough to be rated (and enough significant events),
+ you can suppress the rating alert
+ by passing NO for canPromptForRating. The rating alert will simply be postponed
+ until it is called again with YES for canPromptForRating. The rating alert
+ can also be triggered by appLaunched: and userDidSignificantEvent:
+ (as long as you pass YES for canPromptForRating in those methods).
+ */
++ (void)appEnteredForeground:(BOOL)canPromptForRating;
+
+/*
+ Tells Appirater that the user performed a significant event. A significant
+ event is whatever you want it to be. If you're app is used to make VoIP
+ calls, then you might want to call this method whenever the user places
+ a call. If it's a game, you might want to call this whenever the user
+ beats a level boss.
+
+ If the user has performed enough significant events and used the app enough,
+ you can suppress the rating alert by passing NO for canPromptForRating. The
+ rating alert will simply be postponed until it is called again with YES for
+ canPromptForRating. The rating alert can also be triggered by appLaunched:
+ and appEnteredForeground: (as long as you pass YES for canPromptForRating
+ in those methods).
+ */
++ (void)userDidSignificantEvent:(BOOL)canPromptForRating;
+
@end
View
273 Appirater.m
@@ -38,16 +38,24 @@
#import <SystemConfiguration/SCNetworkReachability.h>
#include <netinet/in.h>
-NSString *const kAppiraterLaunchDate = @"kAppiraterLaunchDate";
-NSString *const kAppiraterLaunchCount = @"kAppiraterLaunchCount";
+NSString *const kAppiraterFirstUseDate = @"kAppiraterFirstUseDate";
+NSString *const kAppiraterUseCount = @"kAppiraterUseCount";
+NSString *const kAppiraterSignificantEventCount = @"kAppiraterSignificantEventCount";
NSString *const kAppiraterCurrentVersion = @"kAppiraterCurrentVersion";
NSString *const kAppiraterRatedCurrentVersion = @"kAppiraterRatedCurrentVersion";
NSString *const kAppiraterDeclinedToRate = @"kAppiraterDeclinedToRate";
+NSString *const kAppiraterReminderRequestDate = @"kAppiraterReminderRequestDate";
NSString *templateReviewURL = @"itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?id=APP_ID&onlyLatestVersion=true&pageNumber=0&sortOrdering=1&type=Purple+Software";
+NSString *templateReviewURLIpad = @"itms-apps://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=APP_ID";
+
@interface Appirater (hidden)
- (BOOL)connectedToNetwork;
++ (Appirater*)sharedInstance;
+- (void)showRatingAlert;
+- (BOOL)ratingConditionsHaveBeenMet;
+- (void)incrementUseCount;
@end
@implementation Appirater (hidden)
@@ -83,28 +91,117 @@ - (BOOL)connectedToNetwork {
return ((isReachable && !needsConnection) || nonWiFi) ? (testConnection ? YES : NO) : NO;
}
-@end
-
++ (Appirater*)sharedInstance {
+ static Appirater *appirater = nil;
+ if (appirater == nil)
+ {
+ @synchronized(self) {
+ if (appirater == nil)
+ appirater = [[Appirater alloc] init];
+ }
+ }
+
+ return appirater;
+}
-@implementation Appirater
+- (void)showRatingAlert {
+ UIAlertView *alertView = [[[UIAlertView alloc] initWithTitle:APPIRATER_MESSAGE_TITLE
+ message:APPIRATER_MESSAGE
+ delegate:self
+ cancelButtonTitle:APPIRATER_CANCEL_BUTTON
+ otherButtonTitles:APPIRATER_RATE_BUTTON, APPIRATER_RATE_LATER, nil] autorelease];
+ [alertView show];
+}
-+ (void)appLaunched {
- Appirater *appirater = [[Appirater alloc] init];
- [NSThread detachNewThreadSelector:@selector(_appLaunched) toTarget:appirater withObject:nil];
+- (BOOL)ratingConditionsHaveBeenMet {
+ if (APPIRATER_DEBUG)
+ return YES;
+
+ NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
+
+ NSDate *dateOfFirstLaunch = [NSDate dateWithTimeIntervalSince1970:[userDefaults doubleForKey:kAppiraterFirstUseDate]];
+ NSTimeInterval timeSinceFirstLaunch = [[NSDate date] timeIntervalSinceDate:dateOfFirstLaunch];
+ NSTimeInterval timeUntilRate = 60 * 60 * 24 * APPIRATER_DAYS_UNTIL_PROMPT;
+ if (timeSinceFirstLaunch < timeUntilRate)
+ return NO;
+
+ // check if the app has been used enough
+ int useCount = [userDefaults integerForKey:kAppiraterUseCount];
+ if (useCount <= APPIRATER_USES_UNTIL_PROMPT)
+ return NO;
+
+ // check if the user has done enough significant events
+ int sigEventCount = [userDefaults integerForKey:kAppiraterSignificantEventCount];
+ if (sigEventCount <= APPIRATER_SIG_EVENTS_UNTIL_PROMPT)
+ return NO;
+
+ // has the user previously declined to rate this version of the app?
+ if ([userDefaults boolForKey:kAppiraterDeclinedToRate])
+ return NO;
+
+ // has the user already rated the app?
+ if ([userDefaults boolForKey:kAppiraterRatedCurrentVersion])
+ return NO;
+
+ // if the user wanted to be reminded later, has enough time passed?
+ NSDate *reminderRequestDate = [NSDate dateWithTimeIntervalSince1970:[userDefaults doubleForKey:kAppiraterReminderRequestDate]];
+ NSTimeInterval timeSinceReminderRequest = [[NSDate date] timeIntervalSinceDate:reminderRequestDate];
+ NSTimeInterval timeUntilReminder = 60 * 60 * 24 * APPIRATER_TIME_BEFORE_REMINDING;
+ if (timeSinceReminderRequest < timeUntilReminder)
+ return NO;
+
+ return YES;
}
-- (void)_appLaunched {
- NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+- (void)incrementUseCount {
+ // get the app's version
+ NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleVersionKey];
+
+ // get the version number that we've been tracking
+ NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
+ NSString *trackingVersion = [userDefaults stringForKey:kAppiraterCurrentVersion];
+ if (trackingVersion == nil)
+ {
+ trackingVersion = version;
+ [userDefaults setObject:version forKey:kAppiraterCurrentVersion];
+ }
if (APPIRATER_DEBUG)
+ NSLog(@"APPIRATER Tracking version: %@", trackingVersion);
+
+ if ([trackingVersion isEqualToString:version])
{
- [self performSelectorOnMainThread:@selector(showPrompt) withObject:nil waitUntilDone:NO];
+ // check if the first use date has been set. if not, set it.
+ NSTimeInterval timeInterval = [userDefaults doubleForKey:kAppiraterFirstUseDate];
+ if (timeInterval == 0)
+ {
+ timeInterval = [[NSDate date] timeIntervalSince1970];
+ [userDefaults setDouble:timeInterval forKey:kAppiraterFirstUseDate];
+ }
- return;
+ // increment the use count
+ int useCount = [userDefaults integerForKey:kAppiraterUseCount];
+ useCount++;
+ [userDefaults setInteger:useCount forKey:kAppiraterUseCount];
+ if (APPIRATER_DEBUG)
+ NSLog(@"APPIRATER Use count: %d", useCount);
+ }
+ else
+ {
+ // it's a new version of the app, so restart tracking
+ [userDefaults setObject:version forKey:kAppiraterCurrentVersion];
+ [userDefaults setDouble:[[NSDate date] timeIntervalSince1970] forKey:kAppiraterFirstUseDate];
+ [userDefaults setInteger:1 forKey:kAppiraterUseCount];
+ [userDefaults setInteger:0 forKey:kAppiraterSignificantEventCount];
+ [userDefaults setBool:NO forKey:kAppiraterRatedCurrentVersion];
+ [userDefaults setBool:NO forKey:kAppiraterDeclinedToRate];
+ [userDefaults setDouble:0 forKey:kAppiraterReminderRequestDate];
}
- BOOL willShowPrompt = NO;
-
+ [userDefaults synchronize];
+}
+
+- (void)incrementSignificantEventCount {
// get the app's version
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleVersionKey];
@@ -122,67 +219,108 @@ - (void)_appLaunched {
if ([trackingVersion isEqualToString:version])
{
- // get the launch date
- NSTimeInterval timeInterval = [userDefaults doubleForKey:kAppiraterLaunchDate];
+ // check if the first use date has been set. if not, set it.
+ NSTimeInterval timeInterval = [userDefaults doubleForKey:kAppiraterFirstUseDate];
if (timeInterval == 0)
{
timeInterval = [[NSDate date] timeIntervalSince1970];
- [userDefaults setDouble:timeInterval forKey:kAppiraterLaunchDate];
+ [userDefaults setDouble:timeInterval forKey:kAppiraterFirstUseDate];
}
- NSTimeInterval secondsSinceLaunch = [[NSDate date] timeIntervalSinceDate:[NSDate dateWithTimeIntervalSince1970:timeInterval]];
- double secondsUntilPrompt = 60 * 60 * 24 * DAYS_UNTIL_PROMPT;
-
- // get the launch count
- int launchCount = [userDefaults integerForKey:kAppiraterLaunchCount];
- launchCount++;
- [userDefaults setInteger:launchCount forKey:kAppiraterLaunchCount];
+ // increment the significant event count
+ int sigEventCount = [userDefaults integerForKey:kAppiraterSignificantEventCount];
+ sigEventCount++;
+ [userDefaults setInteger:sigEventCount forKey:kAppiraterSignificantEventCount];
if (APPIRATER_DEBUG)
- NSLog(@"APPIRATER Launch count: %d", launchCount);
-
- // have they previously declined to rate this version of the app?
- BOOL declinedToRate = [userDefaults boolForKey:kAppiraterDeclinedToRate];
-
- // have they already rated the app?
- BOOL ratedApp = [userDefaults boolForKey:kAppiraterRatedCurrentVersion];
-
- if (secondsSinceLaunch > secondsUntilPrompt &&
- launchCount > LAUNCHES_UNTIL_PROMPT &&
- !declinedToRate &&
- !ratedApp)
- {
- if ([self connectedToNetwork]) // check if they can reach the app store
- {
- willShowPrompt = YES;
- [self performSelectorOnMainThread:@selector(showPrompt) withObject:nil waitUntilDone:NO];
- }
- }
+ NSLog(@"APPIRATER Significant event count: %d", sigEventCount);
}
else
{
// it's a new version of the app, so restart tracking
[userDefaults setObject:version forKey:kAppiraterCurrentVersion];
- [userDefaults setDouble:[[NSDate date] timeIntervalSince1970] forKey:kAppiraterLaunchDate];
- [userDefaults setInteger:1 forKey:kAppiraterLaunchCount];
+ [userDefaults setDouble:0 forKey:kAppiraterFirstUseDate];
+ [userDefaults setInteger:0 forKey:kAppiraterUseCount];
+ [userDefaults setInteger:1 forKey:kAppiraterSignificantEventCount];
[userDefaults setBool:NO forKey:kAppiraterRatedCurrentVersion];
[userDefaults setBool:NO forKey:kAppiraterDeclinedToRate];
+ [userDefaults setDouble:0 forKey:kAppiraterReminderRequestDate];
}
-
[userDefaults synchronize];
- if (!willShowPrompt)
- [self autorelease];
+}
+
+@end
+
+
+@implementation Appirater
+
+- (void)incrementAndRate:(NSNumber*)_canPromptForRating {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ [self incrementUseCount];
+
+ if ([_canPromptForRating boolValue] == YES &&
+ [self ratingConditionsHaveBeenMet] &&
+ [self connectedToNetwork])
+ {
+ [self showRatingAlert];
+ }
[pool release];
}
-- (void)showPrompt {
- UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:APPIRATER_MESSAGE_TITLE
- message:APPIRATER_MESSAGE
- delegate:self
- cancelButtonTitle:APPIRATER_CANCEL_BUTTON
- otherButtonTitles:APPIRATER_RATE_BUTTON, APPIRATER_RATE_LATER, nil];
- [alertView show];
+- (void)incrementSignificantEventAndRate:(NSNumber*)_canPromptForRating {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+
+ [self incrementSignificantEventCount];
+
+ if ([_canPromptForRating boolValue] == YES &&
+ [self ratingConditionsHaveBeenMet] &&
+ [self connectedToNetwork])
+ {
+ [self showRatingAlert];
+ }
+
+ [pool release];
+}
+
++ (void)appLaunched {
+ [Appirater appLaunched:YES];
+}
+
++ (void)appLaunched:(BOOL)canPromptForRating {
+ /* We only count launches on non-multitasking devices, because
+ multitasking devices also get a usage call when they come
+ into the foreground and we don't want to count app launches
+ as two uses on multitasking devices. */
+ UIDevice *device = [UIDevice currentDevice];
+ if ([device respondsToSelector:@selector(multitaskingSupported)] &&
+ device.multitaskingSupported)
+ {
+ return;
+ }
+
+ NSNumber *_canPromptForRating = [[NSNumber alloc] initWithBool:canPromptForRating];
+ [NSThread detachNewThreadSelector:@selector(incrementAndRate:)
+ toTarget:[Appirater sharedInstance]
+ withObject:_canPromptForRating];
+ [_canPromptForRating release];
+}
+
++ (void)appEnteredForeground:(BOOL)canPromptForRating {
+ NSNumber *_canPromptForRating = [[NSNumber alloc] initWithBool:canPromptForRating];
+ [NSThread detachNewThreadSelector:@selector(incrementAndRate:)
+ toTarget:[Appirater sharedInstance]
+ withObject:_canPromptForRating];
+ [_canPromptForRating release];
+}
+
++ (void)userDidSignificantEvent:(BOOL)canPromptForRating {
+ NSNumber *_canPromptForRating = [[NSNumber alloc] initWithBool:canPromptForRating];
+ [NSThread detachNewThreadSelector:@selector(incrementSignificantEventAndRate:)
+ toTarget:[Appirater sharedInstance]
+ withObject:_canPromptForRating];
+ [_canPromptForRating release];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
@@ -193,28 +331,37 @@ - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)butto
{
// they don't want to rate it
[userDefaults setBool:YES forKey:kAppiraterDeclinedToRate];
+ [userDefaults synchronize];
break;
}
case 1:
{
// they want to rate it
- NSString *reviewURL = [templateReviewURL stringByReplacingOccurrencesOfString:@"APP_ID" withString:[NSString stringWithFormat:@"%d", APPIRATER_APP_ID]];
- [[UIApplication sharedApplication] openURL:[NSURL URLWithString:reviewURL]];
-
+ NSString *reviewURL = nil;
+ // figure out which URL to use. iPad only apps have to use a different app store URL
+ NSDictionary *bundleDictionary = [[NSBundle mainBundle] infoDictionary];
+ if ([bundleDictionary objectForKey:@"UISupportedInterfaceOrientations"] != nil &&
+ [bundleDictionary objectForKey:@"UISupportedInterfaceOrientations~ipad"] == nil)
+ {
+ // it's an iPad only app, so use the iPad url
+ reviewURL = [templateReviewURLIpad stringByReplacingOccurrencesOfString:@"APP_ID" withString:[NSString stringWithFormat:@"%d", APPIRATER_APP_ID]];
+ }
+ else // iPhone or Universal app, so we can use the direct url
+ reviewURL = [templateReviewURL stringByReplacingOccurrencesOfString:@"APP_ID" withString:[NSString stringWithFormat:@"%d", APPIRATER_APP_ID]];
[userDefaults setBool:YES forKey:kAppiraterRatedCurrentVersion];
+ [userDefaults synchronize];
+
+ [[UIApplication sharedApplication] openURL:[NSURL URLWithString:reviewURL]];
break;
}
case 2:
// remind them later
+ [userDefaults setDouble:[[NSDate date] timeIntervalSince1970] forKey:kAppiraterReminderRequestDate];
+ [userDefaults synchronize];
break;
default:
break;
}
-
- [userDefaults synchronize];
-
- [alertView release];
- [self release];
}
@end
View
21 README
@@ -1,21 +0,0 @@
-Introduction
-------------
-Appirater is a class that you can drop into any iPhone app that will help remind your users
-to review your app on the App Store. The code is released under the MIT/X11, so feel free to
-modify and share your changes with the world. To find out more, check out the project
-homepage: http://arashpayan.com/blog/index.php/2009/09/07/presenting-appirater/
-
-Getting Started
----------------
-1) Add the Appirater code into your project
-2) Add the CFNetwork and SystemConfiguration frameworks to your project
-3) Call [Appirater appLaunched] at the end of your app delegate's application:didFinishLaunchingWithOptions: method.
-4) Finally, set the APPIRATER_APP_ID in Appirater.h to your Apple provided software id.
-
-License
--------
-Copyright 2008. Arash Payan (http://arashpayan.com)
-This library is distributed under the terms of the MIT/X11.
-
-While not required, I greatly encourage and appreciate any improvements that you make
-to this library be contributed back for the benefit of all who use Appirater.
View
26 README.md
@@ -0,0 +1,26 @@
+Introduction
+------------
+Appirater is a class that you can drop into any iPhone app that will help remind your users
+to review your app on the App Store. The code is released under the MIT/X11, so feel free to
+modify and share your changes with the world. To find out more, check out the [project
+homepage] [homepage].
+
+Getting Started
+---------------
+1. Add the Appirater code into your project
+2. Add the `CFNetwork` and `SystemConfiguration` frameworks to your project
+3. Call `[Appirater appLaunched:YES]` at the end of your app delegate's `application:didFinishLaunchingWithOptions:` method.
+4. Call `[Appirater appEnteredForeground:YES]` in your app delegate's `applicationWillEnterForeground:` method.
+5. (OPTIONAL) Call `[Appirater userDidSignificantEvent:YES]` when the user does something 'significant' in the app.
+6. Finally, set the `APPIRATER_APP_ID` in `Appirater.h` to your Apple provided software id.
+
+License
+-------
+Copyright 2010. [Arash Payan] [arash].
+This library is distributed under the terms of the MIT/X11.
+
+While not required, I greatly encourage and appreciate any improvements that you make
+to this library be contributed back for the benefit of all who use Appirater.
+
+[homepage]: http://arashpayan.com/blog/index.php/2009/09/07/presenting-appirater/
+[arash]: http://arashpayan.com
Please sign in to comment.
Something went wrong with that request. Please try again.