Wednesday, 19 July 2017

How and when to ask users to leave a review for your iOS app

I finally added a means of requesting users to leave a review for the My Day To-Do family of apps (Pro or Lite). The interesting thing is, the underlying code to do so is different for those using iOS 10.3 and above and those with iOS versions below it. In this post, I am going to share the code (Swift) to achieve that and my logic behind when to prompt users to leave a review. What logic? I mean as a user the last thing I want is for an app to ask me to leave a review, the first time I open it. That's just annoying and in that case I am more likely to leave a negative review. Anyway without further a due let's get to it.

Background

Prompting users to leave a review for My Day To-Do was a lower priority item on the list of things to-do for My Day To-Do. However the recent spike in downloads for My Day To-Do just made it a higher priority item and something to be done ASAP (If you want to know more about the download spike, you can read this post). In my quest to add the logic to prompt users for a review, I discovered that programatically there is more than one way to do this, depending on the iOS version on the users device. In summary, for anyone using iOS10.3 and above you will write a lot less code than you will for those using versions below iOS 10.3.

Problem

Provide a means for users of my iOS app to leave a review for the app i.e. a prompt or something asking them to review the app.

Solution 

There are many things to consider for this solution. Things such as,

When to ask users to leave a review?

The last thing I wanted for My Day To-Do is for the users to open the app and be prompted for a review. I only wanted to prompt the users who have used the app a certain number of times to leave a review. I mean if someone has used your app more than once, chances are that they like it and are more likely to leave a positive review or constructive criticism in their negative review that you can work on. At the same time, you also cannot wait forever (a  few weeks) to prompt users and ask them to leave a review they are more likely to stop using your app. I mean the priority is USERS FIRST and if there is something in your app that they do not like (and for good reason), it has to be addressed ASAP. Therefore while coming up with my logic, I had to make a choice that strikes a balance between the aforementioned things. 

The logic behind user review prompt

How I solved this problem is by maintaining a counter in UserDefaults of how many times does the user access the 'Day Tasks' tab of My Day To-Do. Once the counter crosses a certain threshold X, I prompt the users to leave a review.

fyi, My Day To-Do is a tab based task management app with the main tab being the 'Day Tasks' tab, where your day tasks are loaded for a particular date. 

For users using iOS 10.3 and above

This one is really simple, it's literally just a few lines of code

import StoreKit
func askForUserReview(userDefaults: UserDefaults) {
let reviewReqCount = userDefaults.integer(forKey: Constants.REVIEW_REQUEST_COUNT)
if let reReqCount = reviewReqCount { if reReqCount > X {
SKStoreReviewController.requestReview()
if #available(iOS 10.3, *) { } } }
}
SKStoreReviewController is a part of the StoreKit framework, so make sure that you have that import statement in your class. 

The code above is pretty explanatory but let me clarify a few things to avoid confusion (if any?) 

We have a function called askForUserReview that expects a userDefaults as a parameter, I mean we could declare a userDefaults as a class variable but I prefer avoiding that in favour of function params. Why? Just in case I start writing recursive code, this habit would help, personally I find it much easier to debug code this way and much more. Listing all the reasons is seriously beyond the scope of this post.

let reviewReqCount = userDefaults.integer(forKey: Constants.REVIEW_REQUEST_COUNT)
Next, we get the review request count value. I mentioned earlier that we only ask users to leave a review for the app if they have used the app a certain number of times which I track through a counter stored in userDefaults. Regarding Constants.REVIEW_REQUEST_COUNT, I generally avoid hard coding strings in code, especially those that are constants. Hence I have a Constants class with a static variable REVIEW_REQUEST_COUNT which is the key used to store and retrieve the reviewRequestCounter.

I will avoid talking about the if condition for unwrapping optionals, I mean that should be immediately obvious to anyone who has worked with Swift. If you have not? then read here to know more about it.

if reReqCount > X {
if #available(iOS 10.3, *) {
SKStoreReviewController.requestReview()
}
}
The next thing we do is to check  if our reviewRequestCounter is above X, remember X is just an integer, I mean it's just another constant e.g. X = 10 or X = 25  etc. Then we check if the user is on a device using iOS10.3 or more. If so then we request the user to leave a review for the app, via SKStoreReviewController.requestReview() method that's it, it really is that simple to show a review request prompt to the user. Read this article to know more about availability checking(#available) in Swift.  

What if the user is on an iOS device below 10.3?

I am so glad you asked, let us find out shall we,

func askForUserReview(userDefaults: UserDefaults) {
    let reviewReqCount = userDefaults.integer(forKey: Constants.REVIEW_REQUEST_COUNT)
    if let reReqCount = reviewReqCount {
        if reReqCount > X {
            if #available(iOS 10.3, *) {
                SKStoreReviewController.requestReview()
            } else {
                let title = NSLocalizedString("reviewReqTitle", comment: "")
                let message = NSLocalizedString("reviewReqMsg", comment: "")
                let okTxt = NSLocalizedString("ok", comment: "")
                let cancelTxt = NSLocalizedString("cancel", comment: "")
                if hostViewController.presentingViewController == nil {
                    let alertWithConfirm = UIAlertController(title: title,  message: message, preferredStyle: .alert)
                    let ok = UIAlertAction(title: okTxt, style: .cancel) {
                        Void in
                        self.rateApp(appId: "id1066820078") { success in
                            print("RateApp \(success)")
                        }
                        self.incrementReviewRequestCount(reviewReqCount: reReqCount,userDefaults: userDefaults)
                    }
                    let cancel = UIAlertAction(title: cancelTxt, style: .destructive, handler: nil)
                    alertWithConfirm.addAction(ok)
                    alertWithConfirm.addAction(cancel)
                    hostViewController.present(alertWithConfirm, animated: true, completion: nil)
                }

            }
        }
    }
}

func rateApp(appId: String, completion: @escaping ((_ success: Bool)->())) {
    guard let url = URL(string : "itms-apps://itunes.apple.com/app/" + appId) else {
        completion(false)
        return
    }
    completion(UIApplication.shared.openURL(url))
}


In summary, what's happening in the code above is, we prompt the user with an alert (UIAlertController) and ask them, "Are you enjoying the app? Would you like to leave a review?" and they have the option of tapping the Yes or No buttons. If they say yes, we redirect them to the app store page of the app via it's app id which in this case is the app id of My Day To-Do

Now let's examine individual sections of the code above to gain a better understanding, 

let title = NSLocalizedString("reviewReqTitle", comment: "")
let message = NSLocalizedString("reviewReqMsg", comment: "")
let cancelTxt = NSLocalizedString("cancel", comment: "")
let okTxt = NSLocalizedString("ok", comment: "")
Here the text for the dialog prompt shown to the user asking them whether or not they want to leave a review for the app is being initialised. You can read more about NSLocalizedString here but it's generally a good idea to have all static text in your app come from a strings file, so in case you want to localise it, you are ready for it. If you want a better understanding of how to localise your iOS app, have a read of this tutorial.

p.s. In case of users with iOS versions 10.3 and above, we do not have to show any Alert dialogs or anything, the SKStoreReviewController.requestReview takes care of everything for us.

if hostViewController.presentingViewController == nil {
     let alertWithConfirm = UIAlertController(title: title,  message: message, preferredStyle: .alert)
     let ok = UIAlertAction(title: okTxt, style: .cancel) {
      Void in
        self.rateApp(appId: "id1066820078") { success in
            print("RateApp \(success)")
         } 
       
      }
     let cancel = UIAlertAction(title: cancelTxt, style: .destructive, handler: nil)
     alertWithConfirm.addAction(ok)
     alertWithConfirm.addAction(cancel)
     hostViewController.present(alertWithConfirm, animated: true, completion: nil)
 }

Next, is just a simple check to ensure if the ViewController is already presenting another viewController. If not, then present the Alert dialog to ask users for an app review using the text that initialised earlier with the logic to handle (UIAlertAction) the OK button press, tap etc (in case the user agrees to leave an app review). In the code above if the user chooses to leave a review for the app, the control is passed to the rateApp method. 


func rateApp(appId: String, completion: @escaping ((_ success: Bool)->())) {
    guard let url = URL(string : "itms-apps://itunes.apple.com/app/" + appId) else {
        completion(false)
        return
    }
    completion(UIApplication.shared.openURL(url))
}

As I said earlier, I prefer parametrising my methods when possible or it makes sense to do so, in this case it did make sense to me. The rateApp accepts the appId as a parameter i.e. appId of the app to review. This code is reusable, I mean it can be copied and pasted to any other app. You can read more about Swift guard statements here. Now as per getting the app id? well it's pretty simple, have a look at the url for My Day To-Do Lite 

https://itunes.apple.com/app/my-day-to-do-lite-like-pen-paper-day-planner/id1066820078

The id is at the end, I have highlighted that part of the url in bold, so if you want to get the App ID for your app, I assume you should be able to find it in it's app store URL.

Lastly, if you need to know more about completionHandlers, have a read of this article and decide when it's best for you to use them. 

Conclusion

Phew! That took longer than I initially thought, anyway in this post we examined how to prompt users to leave a review for your iOS app. In a way, I almost regret not having done this from the very beginning, but given that I am the only developer working on My Day To-Do, I have to prioritise. There are a ton of things to do, including maintaining this blog (adding new content etc), hence I simply cannot get to everything on time. 

The point of it all is that, if you haven't already added a means to ask users to leave a review for your app, then do that sooner than later. Think of a way in which it makes the most sense for your app to ask users for a review and then add a review prompt. If you do not have enough time to add a fall-back to iOS versions before 10.3 then at least do so for those with iOS10.3 and above. I mean by now, I have shown you just how simple it is to do and besides something is better than nothing right? 

Finally, I am working on My Day To-Do full-time right now so if you find my blog posts useful and want to support me you can 

You could also show your support by giving the My Day To-Do Facebook page a like.


1 comment:

  1. Thanks for sharing this valuable information and we collected some information from this post.

    IOS in-house Corporate training in Nigeria

    ReplyDelete