drewster99/nccrequestreview
Gated App Store review requests for SwiftUI apps. The package decides when asking for a review is appropriate, so the system prompt (SwiftUI's requestReview environment action) only appears after the user has gotten real value from the app, never during onboarding or the firs
Requirements
| Platform | Minimum | |----------|---------| | iOS / iPadOS / Mac Catalyst | 17.0 | | macOS | 14.0 | | visionOS | 1.0 (builds; not declared in platforms) |
Swift 6 language mode. tvOS and watchOS are not supported: StoreKit's RequestReviewAction is unavailable there.
Installation
Swift Package Manager:
dependencies: [
.package(url: "https://github.com/drewster99/NCCRequestReview.git", from: "3.1.0")
],
targets: [
.target(name: "MyApp", dependencies: ["NCCRequestReview"])
]Or in Xcode: File ▸ Add Package Dependencies… and enter https://github.com/drewster99/NCCRequestReview.git.
Usage
Create one manager at the App level, attach the handler to the outermost view of every window scene, and record primary actions where they happen.
import SwiftUI
import NCCRequestReview
@main
struct MyApp: App {
@StateObject private var reviewManager = NCCRequestReviewManager(
configuration: .standard,
onEvent: { event in
// Map to your analytics, e.g.
// Analytics.log("review_\(event)")
print(event)
}
)
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(reviewManager)
.nccRequestReviewHandler(reviewManager)
}
}
}
struct ContentView: View {
@EnvironmentObject private var reviewManager: NCCRequestReviewManager
var body: some View {
Button("Finish recording") {
saveRecording()
// Counts the action, then requests a review 2 seconds later if
// every gate passes at both moments.
reviewManager.recordPrimaryActionAndRequestReview("recording_finished", delay: 2)
}
}
private func saveRecording() { /* ... */ }
}To count an action without requesting, call recordPrimaryAction(_:). To request without counting, call requestReviewIfAppropriate(trigger:delay:). cancelScheduledRequest() cancels a pending request, for example when the user starts a new task during the delay.
Configuration
NCCRequestReviewManager.Configuration(
minimumPrimaryActionCount: 3, // lifetime actions before any request
minimumIntervalBetweenRequests: 3 * 24 * 60 * 60, // cooldown between attempts
minimumLaunchCount: 2, // never in the first launch
minimumIntervalSinceFirstLaunch: 24 * 60 * 60, // and not within 24 hours of it
limitToOneRequestPerAppVersion: false // keyed by CFBundleShortVersionString
).standard uses the values above. .debugQuickTesting (one action, a 60-second cooldown, no first-session gates) is meant for development only. Negative values are clamped to zero.
Events
onEvent is called on the main actor, after the manager's state has been updated, so a handler may call back into the manager.
| Event | Meaning | |-------|---------| | .primaryActionRecorded(name:totalCount:) | A primary action was counted | | .reviewRequestScheduled(trigger:delaySeconds:) | All gates passed; the request fires after the delay | | .reviewRequestAttempted(trigger:) | StoreKit's review action was invoked | | .reviewRequestSkipped(trigger:reason:) | No request was made; reason is a SkipReason |
Attempted is not shown. StoreKit never reports whether the prompt was displayed. The system rate-limits prompts (at most three a year per app), honors the user's "In-App Ratings & Reviews" setting, and suppresses prompts in TestFlight builds. .reviewRequestAttempted means the API was called, nothing more.
Behavior notes
- One manager per process. Launches are counted when the manager is created, once per
UserDefaultsstore per process. Create the manager atApplevel (as above), not lazily in a screen, and pass the sameUserDefaultsinstance every time. - A "launch" is a process launch. On iOS an app can stay resident for days, so
minimumLaunchCountmay take longer to satisfy than expected. - Clock changes. All intervals use absolute
Dates, so time zone changes have no effect. If a stored date is in the future (the clock was set forward, then corrected), the affected gate fails closed until real time catches up. - Window targeting. The request goes to the most recently activated window whose
scenePhaseis.active. On macOS every visible window is.active, so the chosen window may not be the key window. - Attach to the outermost view. A window's registration lasts from the handler view's
onAppearto itsonDisappear. If the handler sits on a view that disappears while its window stays open (a tab's content, a pushed view), requests skip with.noRequestReviewAction. - App extensions. On iOS the manager reads
UIApplication.shared, which is unavailable in app extensions. Use it only in the app target. - Missing app version. If the main bundle has no
CFBundleShortVersionString, the version is recorded as"unknown"and an error is logged. WithlimitToOneRequestPerAppVersionon, every build then counts as the same version. - Logging uses
OSLogwith subsystemcom.nuclearcyborg.NCCRequestReview.
Persisted state lives in UserDefaults under keys prefixed NCCRequestReview.. Version 1.x used different keys and its history is not migrated.
Tester app
NCCRequestReview_TesterApp is a small macOS harness (swift run NCCRequestReview_TesterApp) that uses .debugQuickTesting. Run outside an app bundle, StoreKit does not present a real prompt, but the events and gate state are visible.
License
MIT. See LICENSE.
Package Metadata
Repository: drewster99/nccrequestreview
Default branch: main
README: README.md