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.
UIApplication.shared.connectedScenes
gives aSet<UIScene>
.- Each UIScene has an
activationState
. - 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.
- 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)
- Increment the counter when the view appears with
onAppear
.
content
.onAppear {
reviewCounter += 1
}
Code language: Swift (swift)
- 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)
Outstanding! Was looking for code to prompt review after x # of tries that worked for both iOS & macOS. Works beautifully!