New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dependency Injection for Testability #3

grzegorzkrukowski opened this Issue Nov 30, 2018 · 4 comments


None yet
2 participants

grzegorzkrukowski commented Nov 30, 2018

Maybe it's not fully related to purpose of your article, but I still find it important to mention - as you are using Real World Example in title :)

In real world you should not be assuming that calling SKStoreReviewController.requestReview() actually shows the request review UI.

As Apple docs says:

Because this method may or may not present an alert, it's not appropriate to call it in response to a button tap or other user action.

The proper way to handle that is to do something similar to what Apple suggest in one of their examples:

let twoSecondsFromNow = + 2.0
    DispatchQueue.main.asyncAfter(deadline: twoSecondsFromNow) { [navigationController] in
        if navigationController?.topViewController is ProcessCompletedViewController {
            UserDefaults.standard.set(currentVersion, forKey: UserDefaultsKeys.lastVersionPromptedForReviewKey)

Another way is to calculate windows count before and after Apple`s API is called.

NSUInteger windowCount = [UIApplication sharedApplication].windows.count;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
    if (windowCount < [UIApplication sharedApplication].windows.count) {
        [self.ratingService didShowRatingFromSource:source];
        if (completion) completion(YES);
    } else {
        if (completion) completion(NO);

So I think it would be much better in your article to have a requestReview block to take closure with BOOL param as parameter completion, so it can be called asynchronously like in code above informing caller about requestReview results.
And then you should be setting SettingsManager.setLastReviewPromptDate(now) inside this completion block of requestReview.

Otherwise you are risking of delaying rating request for too long, even if it was not really shown to a user.


This comment has been minimized.


vermont42 commented Nov 30, 2018

Thanks for reading the post and leaving this insightful comment.

That is an impressively clever technique to determine whether the review prompt actually appeared. I have updated the post to mention it.

I had thought of the prompt logic as a black box, with no way of determining whether the prompt appeared. My code doesn't cause the prompt to be shown but rather asks iOS, perhaps in vain, to show it. That said, I can see the utility of tracking actual prompt appearances and perhaps firing an appropriate analytic.

I acknowledge that visiting the browse-verbs view controller is an inelegant trigger for calling promptableActionHappened() or requesting a review on the ninth call to that function. A better indicium of engagement would be finishing a verb quiz. Why don't I use that? Due to the large number of verb tenses and my desire for quizzes to be comprehensive, quizzes are quite long: fifty verbs. I have found that many users engage with the app without finishing quizzes. I want to ensure that these less-engaged users get prompted.


This comment has been minimized.

grzegorzkrukowski commented Nov 30, 2018

@vermont42 Thanks for quick answer. Your article is really good, so I'm happy to have my small input there.

For me it was a must - I have multiple trigger points for rating and I needed to know which ones works best - so analytics was the first thing that forced me to find if prompts are really shown and where.

The way I am personally doing review requests:

  1. Try to prompt user for a review with SKStoreReviewController.requestReview()
  2. detect if rating request was shown using cheats above - if it did you are lucky and you can end here
  3. if it was not shown for whatever reason - I'm showing custom popup that asks user if they want to rate the application or do it later
  4. if user clicks they want to rate - redirect user to a itms-apps:// so he can leave review the old way.

This way you will be sure that if you want user to have an option to they get one.
I don't see myself using SKStoreReviewController without knowing it's results and identify if it was actually shown. Based on success of rate popup shown, I'm doing usually lot's of business logic, like keeping last version asked, keeping last date I asked user for review, etc...

Also keep in mind that according to Apple's guidelines, you cannot call SKStoreReviewController.requestReview() on user action:

From docs:
Also remember that the user can disable requests for reviews from ever appearing on their device, so you should avoid referring to your app showing this prompt and never request a review using requestReview() as the result of a user action.

Which is a pity, because easiest way to do rating, would be to have custom popup asking for it, then calling requestReview() with a fallback to action=write-review URL if it didn't appear.
But on the other hand - with SKStoreReviewController it's one less click to leave rating for a user, which is nice.


This comment has been minimized.


vermont42 commented Nov 30, 2018

Are you concerned that if the prompt did not show because the user set the don't-prompt-me setting and you ask the user if s/he wants to review, the user might get annoyed? I would be hesitant to show a custom prompt to users who don't want to see the built-in prompt.

On a meta note, I can see how if Conjugar were an important source of income for me, I would be inclined to maximize the number of review prompts, consistent with Apple rules. Because Conjugar is not a primary or indeed a source of income for me, my heuristic can afford to be relatively lackadaisical.

On an unrelated note, learning that one person liked my post made my day!


This comment has been minimized.

grzegorzkrukowski commented Dec 1, 2018

I bet more people do like it :)

In terms of custom prompt - I am trying to be gentle with those - usually I only ask once per version + not often then X amount of days (which is around 14 days usually).
With those extra conditions even if user meets the required point in the application, I'm not asking again. Also it's easy to add an option for DON'T ASK ME AGAIN next to ASK LATER on custom popup to make sure you give user full control on rating within your application.

Well at least until Apple starts to rejecting application doing that via custom UI and redirecting to AppStore... I'm afraid one day they will only let us using SKStoreReviewController, but for now it's still fine to use redirect with current guidelines:

In addition, you can continue to include a persistent link in the settings or configuration screens of your app that deep-links to your App Store product page. To automatically open a page on which users can write a review in the App Store, append the query parameter action=write-review to your product URL.

But you never know when they are gonna get picky about that ;)

One more advantage of custom review popup is that you can track actual response from user to optimise the trigger points for your rating popup:
If user clicks RATE NOW - it's success, if he clicks MAYBE LATER - you may consider user could leave review if you are asking in better moment (maybe when user is happy with his score in your application, or did something successfully instead of asking just at the end of the game etc...). If user says NEVER ASK AGAIN there is probably not too much you can do there and user will not review - or you are asking in really bad moment in the application flow that may interrupt him and he gets angry ;)
Tracking % of options that users picks will let you know if you are asking for rating in right moment - best way to test it would be to A/B test different trigger points and compare how user answer your rating request.
It's unfortunately not possible with native SKStoreReviewController API - where there is no way to find out how user reacted to it - if he rated or cancelled it :( so all you can do is guess or do user testing externally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment