diff --git a/Appirater.h b/Appirater.h index 2263a0b9..2ca0fab9 100644 --- a/Appirater.h +++ b/Appirater.h @@ -35,8 +35,8 @@ */ #import -#import "AppiraterDelegate.h" #import +#import "AppiraterDelegate.h" extern NSString *const kAppiraterFirstUseDate; extern NSString *const kAppiraterUseCount; @@ -86,11 +86,16 @@ extern NSString *const kAppiraterReminderRequestDate; #define APPIRATER_RATE_LATER NSLocalizedStringFromTableInBundle(@"Remind me later", @"AppiraterLocalizable", [Appirater bundle], nil) @interface Appirater : NSObject { - +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" UIAlertView *ratingAlert; +#pragma clang diagnostic pop } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" @property(nonatomic, strong) UIAlertView *ratingAlert; +#pragma clang diagnostic pop @property(nonatomic) BOOL openInAppStore; #if __has_feature(objc_arc_weak) @property(nonatomic, weak) NSObject *delegate; diff --git a/Appirater.m b/Appirater.m index e9686908..70d5fdeb 100644 --- a/Appirater.m +++ b/Appirater.m @@ -34,8 +34,9 @@ * Copyright 2012 Arash Payan. All rights reserved. */ +#import +#import #import "Appirater.h" -#import #include #if ! __has_feature(objc_arc) @@ -60,11 +61,6 @@ static NSInteger _significantEventsUntilPrompt = -1; static double _timeBeforeReminding = 1; static BOOL _debug = NO; -#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_5_0 - static id _delegate; -#else - __weak static id _delegate; -#endif static BOOL _usesAnimation = TRUE; static UIStatusBarStyle _statusBarStyle; static BOOL _modalOpen = false; @@ -76,6 +72,7 @@ @interface Appirater () @property (nonatomic, copy) NSString *alertCancelTitle; @property (nonatomic, copy) NSString *alertRateTitle; @property (nonatomic, copy) NSString *alertRateLaterTitle; +@property (nonatomic, strong) NSOperationQueue *eventQueue; - (BOOL)connectedToNetwork; + (Appirater*)sharedInstance; - (void)showPromptWithChecks:(BOOL)withChecks @@ -141,7 +138,7 @@ + (void) setDebug:(BOOL)debug { _debug = debug; } + (void)setDelegate:(id)delegate{ - _delegate = delegate; + Appirater.sharedInstance.delegate = delegate; } + (void)setUsesAnimation:(BOOL)animation { _usesAnimation = animation; @@ -246,10 +243,17 @@ - (BOOL)connectedToNetwork { BOOL nonWiFi = flags & kSCNetworkReachabilityFlagsTransientConnection; NSURL *testURL = [NSURL URLWithString:@"http://www.apple.com/"]; - NSURLRequest *testRequest = [NSURLRequest requestWithURL:testURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:20.0]; - NSURLConnection *testConnection = [[NSURLConnection alloc] initWithRequest:testRequest delegate:self]; - return ((isReachable && !needsConnection) || nonWiFi) ? (testConnection ? YES : NO) : NO; + NSURLSessionConfiguration* sessionConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + sessionConfiguration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; + sessionConfiguration.timeoutIntervalForRequest = 20.0; + + NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + + NSURLSessionTask *task = [session dataTaskWithURL:testURL]; + [task resume]; + + return ((isReachable && !needsConnection) || nonWiFi) ? ( (task.state != NSURLSessionTaskStateSuspended) ? YES : NO ) : NO; } + (Appirater*)sharedInstance { @@ -259,7 +263,8 @@ + (Appirater*)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ appirater = [[Appirater alloc] init]; - appirater.delegate = _delegate; + appirater.eventQueue = [[NSOperationQueue alloc] init]; + appirater.eventQueue.maxConcurrentOperationCount = 1; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillResignActive) name: UIApplicationWillResignActiveNotification object:nil]; }); @@ -269,33 +274,39 @@ + (Appirater*)sharedInstance { } - (void)showRatingAlert:(BOOL)displayRateLaterButton { - UIAlertView *alertView = nil; id delegate = _delegate; if(delegate && [delegate respondsToSelector:@selector(appiraterShouldDisplayAlert:)] && ![delegate appiraterShouldDisplayAlert:self]) { return; } - if (displayRateLaterButton) { - alertView = [[UIAlertView alloc] initWithTitle:self.alertTitle - message:self.alertMessage - delegate:self - cancelButtonTitle:self.alertCancelTitle - otherButtonTitles:self.alertRateTitle, self.alertRateLaterTitle, nil]; + if (NSStringFromClass([SKStoreReviewController class]) != nil) { + // If SKStoreReviewController is used, skip the custom dialog and directly go the the rating + [Appirater rateApp]; } else { - alertView = [[UIAlertView alloc] initWithTitle:self.alertTitle - message:self.alertMessage - delegate:self - cancelButtonTitle:self.alertCancelTitle - otherButtonTitles:self.alertRateTitle, nil]; - } - - self.ratingAlert = alertView; + // Otherwise show a custom Alert + UIAlertView *alertView = nil; + if (displayRateLaterButton) { + alertView = [[UIAlertView alloc] initWithTitle:self.alertTitle + message:self.alertMessage + delegate:self + cancelButtonTitle:self.alertCancelTitle + otherButtonTitles:self.alertRateTitle, self.alertRateLaterTitle, nil]; + } else { + alertView = [[UIAlertView alloc] initWithTitle:self.alertTitle + message:self.alertMessage + delegate:self + cancelButtonTitle:self.alertCancelTitle + otherButtonTitles:self.alertRateTitle, nil]; + } + + self.ratingAlert = alertView; [alertView show]; + } - if (delegate && [delegate respondsToSelector:@selector(appiraterDidDisplayAlert:)]) { - [delegate appiraterDidDisplayAlert:self]; - } + if (delegate && [delegate respondsToSelector:@selector(appiraterDidDisplayAlert:)]) { + [delegate appiraterDidDisplayAlert:self]; + } } - (void)showRatingAlert @@ -482,17 +493,17 @@ + (void)appWillResignActive { } + (void)appEnteredForeground:(BOOL)canPromptForRating { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), - ^{ - [[Appirater sharedInstance] incrementAndRate:canPromptForRating]; - }); + Appirater *a = [Appirater sharedInstance]; + [a.eventQueue addOperationWithBlock:^{ + [[Appirater sharedInstance] incrementAndRate:canPromptForRating]; + }]; } + (void)userDidSignificantEvent:(BOOL)canPromptForRating { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), - ^{ - [[Appirater sharedInstance] incrementSignificantEventAndRate:canPromptForRating]; - }); + Appirater *a = [Appirater sharedInstance]; + [a.eventQueue addOperationWithBlock:^{ + [[Appirater sharedInstance] incrementSignificantEventAndRate:canPromptForRating]; + }]; } #pragma GCC diagnostic push @@ -564,10 +575,20 @@ + (UIViewController *) topMostViewController: (UIViewController *) controller { + (void)rateApp { + //Use the built SKStoreReviewController if available (available from iOS 10.3 upwards) + if (NSStringFromClass([SKStoreReviewController class]) != nil) { + // Also note, that SKStoreReviewController takes care of impression limitation by itself so it's ok to not save the impression manually + // This also works in the simulator + [SKStoreReviewController requestReview]; + return; + } + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; [userDefaults setBool:YES forKey:kAppiraterRatedCurrentVersion]; [userDefaults synchronize]; + + //Use the in-app StoreKit view if available (iOS 6) and imported. This works in the simulator. if (![Appirater sharedInstance].openInAppStore && NSStringFromClass([SKStoreProductViewController class]) != nil) { @@ -582,11 +603,6 @@ + (void)rateApp { } [[self getRootViewController] presentViewController:storeViewController animated:_usesAnimation completion:^{ [self setModalOpen:YES]; - //Temporarily use a black status bar to match the StoreKit view. - [self setStatusBarStyle:[UIApplication sharedApplication].statusBarStyle]; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 - [[UIApplication sharedApplication]setStatusBarStyle:UIStatusBarStyleLightContent animated:_usesAnimation]; -#endif }]; //Use the standard openUrl method if StoreKit is unavailable. @@ -595,17 +611,17 @@ + (void)rateApp { #if TARGET_IPHONE_SIMULATOR NSLog(@"APPIRATER NOTE: iTunes App Store is not supported on the iOS simulator. Unable to open App Store page."); #else - NSString *reviewURL = [templateReviewURL stringByReplacingOccurrencesOfString:@"APP_ID" withString:[NSString stringWithFormat:@"%@", _appId]]; + NSString *reviewURL = [templateReviewURL stringByReplacingOccurrencesOfString:@"APP_ID" withString:_appId]; // iOS 7 needs a different templateReviewURL @see https://github.com/arashpayan/appirater/issues/131 // Fixes condition @see https://github.com/arashpayan/appirater/issues/205 if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0 && [[[UIDevice currentDevice] systemVersion] floatValue] < 8.0) { - reviewURL = [templateReviewURLiOS7 stringByReplacingOccurrencesOfString:@"APP_ID" withString:[NSString stringWithFormat:@"%@", _appId]]; + reviewURL = [templateReviewURLiOS7 stringByReplacingOccurrencesOfString:@"APP_ID" withString:_appId]; } // iOS 8 needs a different templateReviewURL also @see https://github.com/arashpayan/appirater/issues/182 else if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) { - reviewURL = [templateReviewURLiOS8 stringByReplacingOccurrencesOfString:@"APP_ID" withString:[NSString stringWithFormat:@"%@", _appId]]; + reviewURL = [templateReviewURLiOS8 stringByReplacingOccurrencesOfString:@"APP_ID" withString:_appId]; } [[UIApplication sharedApplication] openURL:[NSURL URLWithString:reviewURL]]; @@ -659,7 +675,6 @@ - (void)productViewControllerDidFinish:(SKStoreProductViewController *)viewContr //Close the in-app rating (StoreKit) view and restore the previous status bar style. + (void)closeModal { if (_modalOpen) { - [[UIApplication sharedApplication]setStatusBarStyle:_statusBarStyle animated:_usesAnimation]; BOOL usedAnimation = _usesAnimation; [self setModalOpen:NO]; diff --git a/Appirater.podspec.json b/Appirater.podspec.json index fb17070b..82f3b4ba 100644 --- a/Appirater.podspec.json +++ b/Appirater.podspec.json @@ -1,17 +1,17 @@ { "name": "Appirater", - "version": "2.0.5.1", + "version": "2.2.0.1", "platforms": { "ios": "5.0" }, "summary": "A utility that reminds your iPhone app's users to review the app.", - "homepage": "http://arashpayan.com/blog/2009/09/07/presenting-appirater/", + "homepage": "https://arashpayan.com/blog/2009/09/07/presenting-appirater/", "authors": { "Arash Payan": "arash@ara.sh" }, "source": { "git": "https://github.com/timehop/appirater.git", - "tag": "2.0.5.1" + "tag": "2.2.0.1" }, "source_files": "*.{h,m}", "resource_bundles": { @@ -27,6 +27,6 @@ "weak_frameworks": "StoreKit", "license": { "type": "MIT", - "text": "Copyright 2015. Arash Payan. This library is distributed under the terms of the MIT/X11." + "text": "Copyright 2017. Arash Payan. This library is distributed under the terms of the MIT/X11." } } diff --git a/CHANGELOG.md b/CHANGELOG.md index dd87b632..14952d39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ Change Log ========== +Version 2.2.0 *(2017-09-23)* +---------------------------- +* Use SKStoreReviewController if available + * Available on iOS > 10.3 + * You'll need to link the StoreKit Framework +* Armenian localization +* Fix delegate not being set after Appirater initialization (Issue #215) + +Version 2.1.0 *(2016-11-04)* +---------------------------- +* Fix and suppress various Xcode warnings +* Switch to NSURLSession +* Serialize incrementing events + Version 2.0.4 *(2014-09-18)* ---------------------------- * Change: Better URL for iOS 7.1 and 8 support diff --git a/README.md b/README.md index 0278daa0..030512bf 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,13 @@ Introduction ------------- -Appirater is a class that you can drop into any iPhone app (iOS 4.0 or later) 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. Read on below for how to get started. If you need any help using, -the library check out the [Appirater group] [appiratergroup]. +--------------- +Appirater is a class that you can drop into any iPhone app (iOS 4.0 or later) 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. Read on below for how to get started. If you need any help using, the library, post your questions on [Stack Overflow] [stackoverflow] under the `appirater` tag. Getting Started --------------- -###Cocoapods -If you're new to Cocoapods [watch this](http://nsscreencast.com/episodes/5-cocoapods). To add Appirater to your app, add `pod "Appirater"` to your Podfile. - -Cocoapods support is still experimental, and might not work in all use cases. If you experience problems, open an issue and install via Git submodule - -###Manually -1. Add the Appirater code into your project. -2. If your project doesn't use ARC, add the `-fobjc-arc` compiler flag to `Appirater.m` in your target's Build Phases » Compile Sources section. -3. Add the `CFNetwork`, `SystemConfiguration`, and `StoreKit` frameworks to your project. Be sure to **change Required to Optional** for StoreKit in your target's Build Phases » Link Binary with Libraries section. +### CocoaPods +To add Appirater to your app, add `pod "Appirater"` to your Podfile. Configuration ------------- @@ -37,10 +27,10 @@ Configuration 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. -###Development +### Development Setting `[Appirater setDebug:YES]` will ensure that the rating request is shown each time the app is launched. -###Production +### Production Make sure you set `[Appirater setDebug:NO]` to ensure the request is not shown every time the app is launched. Also make sure that each of these components are set in the `application:didFinishLaunchingWithOptions:` method. This example states that the rating request is only shown when the app has been launched 5 times **and** after 7 days. @@ -67,13 +57,17 @@ If you wanted to show the request after 5 days only you can set the following: [Appirater appLaunched:YES]; ``` -Help and Support Group +SKStoreReviewController ---------------------- -Requests for help, questions about usage, suggestions and other relevant topics should be posted at the [Appirater group] [appiratergroup]. As much as I'd like to help everyone who emails me, I can't respond to private emails, but I'll respond to posts on the group where others can benefit from the Q&As. +In iOS 10.3, [SKStoreReviewController](https://developer.apple.com/library/content/releasenotes/General/WhatsNewIniOS/Articles/iOS10_3.html) was introduced which allows rating directly within the app without any additional setup. + +Appirater automatically uses `SKStoreReviewController` if available. You'll need to manually link `StoreKit` in your App however. + +If `SKStoreReviewController` is used, Appirater is used only to decide when to show the rating dialog to the user. Keep in mind, that `SKStoreReviewController` automatically limits the number of impressions, so the dialog might be displayed less frequently than your configured conditions might suggest. License ------- -Copyright 2014. [Arash Payan] [arash]. +Copyright 2017. [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 @@ -83,16 +77,10 @@ Ports for other SDKs -------------- A few people have ported Appirater to other SDKs. The ports are listed here in hopes that they may assist developers of those SDKs. I don't know how closesly (if at all) they track the Objective-C version of Appirater. If you need support for any of the libraries, please contact the maintainer of the port. -+ MonoTouch Port (using C#). [Github] [monotouchport] + MonoTouch Binding (using native Appirater). [Github] [monotouchbinding] -+ Corona SDK. [Github] [coronasdkport] -+ Titanium SDK. [Github] [titaniumport] -[appiratergroup]: http://groups.google.com/group/appirater -[homepage]: http://arashpayan.com/blog/index.php/2009/09/07/presenting-appirater/ -[arash]: http://arashpayan.com +[stackoverflow]: http://stackoverflow.com/ +[homepage]: https://arashpayan.com/blog/2009/09/07/presenting-appirater/ +[arash]: https://arashpayan.com [Appirater.h]: https://github.com/arashpayan/appirater/blob/master/Appirater.h -[monotouchport]: https://github.com/chebum/Appirater-for-MonoTouch [monotouchbinding]: https://github.com/theonlylawislove/MonoTouch.Appirater -[coronasdkport]: https://github.com/aliasgar84/Appirater -[titaniumport]: https://github.com/mpociot/TiAppirater diff --git a/fa.lproj/AppiraterLocalizable.strings b/fa.lproj/AppiraterLocalizable.strings new file mode 100644 index 00000000..c30a6810 --- /dev/null +++ b/fa.lproj/AppiraterLocalizable.strings @@ -0,0 +1,5 @@ +"If you enjoy using %@, would you mind taking a moment to rate it? It won't take more than a minute. Thanks for your support!" = "اگر از استفاده برنامه %@ لذت می‌برید، زمان دارید بهش امتیاز دهید؟ بیشتر از یک دقیقه زمان نخواهد گرفت. با تشکر از اینکه ما را همایت می‌کنید."; +"Rate %@" = "ارزیابی کردن %@"; +"No, Thanks" = "نه، مرسی"; +"Remind me later" = "بعدا"; + diff --git a/hy.lproj/AppiraterLocalizable.strings b/hy.lproj/AppiraterLocalizable.strings new file mode 100644 index 00000000..8a398194 --- /dev/null +++ b/hy.lproj/AppiraterLocalizable.strings @@ -0,0 +1,4 @@ +"If you enjoy using %@, would you mind taking a moment to rate it? It won't take more than a minute. Thanks for your support!" = "Եթե Դուք հաճույքով եք օգտագործում %@-ը, դեմ չե՞ք լինի տրամադրել մեկ րոպե այն գնահատելու համար: Այն չի պահանջի ձեզանից ավելի քան մեկ րոպե: Շնորհակալություն աջակցության համար:"; +"Rate %@" = "Գնահատել %@-ը"; +"No, Thanks" = "Ոչ, շնորհակալություն"; +"Remind me later" = "Հիշեցնել ավելի ուշ";