Requesting App Store reviews in SwiftUI

Starting from iOS 16 and macOS 13 we have a unified native SwiftUI API to request App Store reviews in our apps. We can read requestReview property from the environment and call it as a function at the appropriate time.

There are specific guidelines on when to call this function, such as it should never be called in response to a user action and shouldn't be called more than once for the same app version.

In this post we are going to follow the recommendations from Requesting App Store Reviews documentation article to write an example for requesting App Store reviews in SwiftUI apps using the new API.

We are going to request a review if a user has successfully completed some multistep process in our app at least 4 times.

For that we'll save the process completion count in AppStorage and monitor changes to the property with onChange() modifier.

struct ContentView: View {
    @AppStorage("processCompletedCount")
    var processCompletedCount = 0
    
    var body: some View {
        VStack {
            Button("Do something") {
                processCompletedCount += 1
            }
            Text("Process count: \(processCompletedCount)")
        }
        .onChange(
            of: processCompletedCount
        ) { newValue in
            guard newValue >= 4
            else { return }
            
            ...
        }
    }
}

Pressing the button in the example above simulates the completion of a process, that in a real app can be whatever makes sense in your case: creating a folder with notes, sending messages, writing or reading recipes etc. We have to make sure that the user has enough experience with the app to give a review.

Next, we have to check that we haven't already asked the user to review this specific app version. To do that, we'll read the current version from the bundle and compare it with the last version prompted for review that we'll also have to save in AppStorage.

struct ContentView: View {
    @AppStorage("processCompletedCount")
    var processCompletedCount = 0
    
    @AppStorage("lastVersionPromptedForReview")
    var lastVersionPromptedForReview: String?
    
    var currentVersion: String {
        let infoDictionaryKey = kCFBundleVersionKey as String
        guard
            let currentVersion = Bundle.main.object(
                forInfoDictionaryKey: infoDictionaryKey
            ) as? String
        else {
            fatalError(
                "Expected to find a bundle version in the info dictionary"
            )
        }
        return currentVersion
    }
    
    var body: some View {
        VStack {
            Button("Do something") {
                processCompletedCount += 1
            }
            Text("Process count: \(processCompletedCount)")
        }
        .onChange(
            of: processCompletedCount
        ) { newValue in
            guard
                newValue >= 4,
                lastVersionPromptedForReview != currentVersion
            else { return }

            ...
        }
    }
}

And now we'll import StoreKit, read the new requestReview value from the environment and call it, if the situation meets all the requirements. We'll call it asynchronously with a small delay, so that the review prompt doesn't pop up as soon as the user completes the task.

import SwiftUI
import StoreKit

struct ContentView: View {
    @AppStorage("processCompletedCount")
    var processCompletedCount = 0
    
    @AppStorage("lastVersionPromptedForReview")
    var lastVersionPromptedForReview: String?
    
    @Environment(\.requestReview)
    var requestReview
    
    var currentVersion: String {
        let infoDictionaryKey = kCFBundleVersionKey as String
        guard
            let currentVersion = Bundle.main.object(
                forInfoDictionaryKey: infoDictionaryKey
            ) as? String
        else {
            fatalError(
                "Expected to find a bundle version in the info dictionary"
            )
        }
        return currentVersion
    }
    
    var body: some View {
        VStack {
            Button("Do something") {
                processCompletedCount += 1
            }
            Text("Process count: \(processCompletedCount)")
        }
        .onChange(
            of: processCompletedCount
        ) { newValue in
            guard
                newValue >= 4,
                lastVersionPromptedForReview != currentVersion
            else { return }

            Task {
                try await Task.sleep(
                    until: .now + .seconds(2),
                    tolerance: .seconds(0.5),
                    clock: .suspending
                )
                
                requestReview()
                lastVersionPromptedForReview = currentVersion
            }
        }
    }
}

We need to remember to save the current app version in AppStorage after prompting for review, so that we don't prompt again for the same version.

We don't have a way of knowing if the system actually presented the prompt when we requested or if the user dismissed it without giving a review. But, since Apple's example for requesting reviews only attempts to present the prompt once per app version, I chose to apply the same logic in my example too.