George Garside Blog

A place of many ramblings about macOS and development. If you find something useful on here, it's probably an accident.

It’s possible to request a review from the user using SKStoreReviewController with SwiftUI.

The requestReview() function has been deprecated, so instead it’s necessary to call requestReview(in:) and pass a UIWindowScene.

SKStoreReviewController hasn’t been updated for SwiftUI, so the API still interacts with UIKit, which is where UIWindowScene comes from.

Get a UIWindowScene and requestReview(in:)

To get a UIWindowScene to pass to requestReview(in:), the app needs to interact with UIKit.

  1. UIApplication.shared.connectedScenes gives a Set<UIScene>.
  2. Each UIScene has an activationState.
  3. The scene we want is the one currently being interacted with, which is the one that is foregroundActive.

Code for the above is as follows:

if let scene = UIApplication.shared.connectedScenes .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { SKStoreReviewController.requestReview(in: scene) }
Code language: Swift (swift)

Trigger the review request

That code needs calling at some point. Apple’s guidelines for requesting a review say

Try to make the request at a time that will not interrupt what the user is trying to achieve in your app. For example, at the end of a sequence of events that the user has successfully completed.

Requesting App Store Reviews – StoreKit – Apple Developer

For this example, we’ll use the appearance of a view 30 times to trigger the prompt.

  1. Store the number of times the view has appeared, persistently in @AppStorage.
// Counter of events that would lead to a review being asked for. @AppStorage("review.counter") private var reviewCounter = 0
Code language: Swift (swift)
  1. Increment the counter when the view appears with onAppear.
content .onAppear { reviewCounter += 1 }
Code language: Swift (swift)
  1. Trigger the prompt when the view disappears if the counter has been incremented past the desired point.
content .onDisappear { if reviewCounter > 30 { reviewCounter = 0 DispatchQueue.main.async { if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { SKStoreReviewController.requestReview(in: scene) } } } }
Code language: Swift (swift)

Using a ViewModifier

Wrapping this all up in a ViewModifier simplifies the call site considerably. For example, attaching this to any view could be as simple as

Text("Thanks for using this service!") .reviewCounter()
Code language: Swift (swift)

given the following ViewModifier and View extension:

/// `SKStoreReviewController.requestReview` after 30 `onAppear`-triggering appearances of the view. struct ReviewCounter: ViewModifier { /// Counter of events that would lead to a review being asked for. @AppStorage("review.counter") private var reviewCounter = 0 func body(content: Content) -> some View { content .onAppear { reviewCounter += 1 } .onDisappear { if reviewCounter > 30 { reviewCounter = 0 DispatchQueue.main.async { #if os(macOS) SKStoreReviewController.requestReview() #else if let scene = UIApplication.shared.connectedScenes .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { SKStoreReviewController.requestReview(in: scene) } #endif } } } } } extension View { /// `SKStoreReviewController.requestReview` after 30 `onAppear`-triggering appearances of the view. func reviewCounter() -> some View { modifier(ReviewCounter()) } }
Code language: Swift (swift)

Leave a Reply

No comments