Документ описывает текущую реализацию запроса оценки приложения (requestReview) в SwiftUI-WorkoutApp, чтобы функционал было удобно сопровождать и безопасно расширять.
Цели реализации:
- централизованный контроль показа review;
- соблюдение ограничения Apple: не более одного запроса за сессию приложения;
- предсказуемое поведение при достижении порогов фильтра (2, 5, 15);
- поддержка догоняющего сценария между сессиями (если milestone достигнут в сессии, где запрос уже был показан).
SwiftUI-WorkoutApp/Services/ReviewService.swiftSwiftUI-WorkoutApp/Extensions/View+reviewRequest.swiftSwiftUI-WorkoutApp/SwiftUI_WorkoutAppApp.swiftSwiftUI-WorkoutApp/Screens/Root/RootScreen.swift
ReviewService (@MainActor, ObservableObject) отвечает за:
- флаг pending-запроса:
pendingRequest; - ограничение «1 запрос за сессию»:
didRequestThisSession; - учёт применения фильтра:
reviewFilterApplyCountвUserDefaults; - учёт завершённых milestone фильтра:
reviewFilterAttemptedMilestonesвUserDefaults; - фиксацию milestone только после фактического consume (
markConsumed()).
Ключевая идея: milestone фильтра считается завершённым только когда система действительно обработала pending-запрос (через markConsumed()), а не в момент достижения счётчика.
ReviewRequestModifier подключён на корневом экране и:
- слушает
pendingRequestчерез.task(id: reviewService.pendingRequest); - делает небольшую задержку (
0.8s) перед вызовомrequestReview(); - повторно проверяет pending после задержки;
- вызывает
reviewService.markConsumed()после вызоваrequestReview().
- За одну сессию приложения допускается не более одного запроса review.
reviewService.requestReviewIfAppropriate() вызывается после успешных операций:
ParkFormScreen.saveParkTask(послеdismiss())EventFormScreen.saveEventTask(послеdismiss())JournalsListScreen.update(journal:)JournalsListScreen.saveNewJournal()(послеaskForJournals(refresh: true))JournalEntriesScreen.updateEntriesTask(послеaskForEntries(refresh: true))DialogScreen.sendMessageTask(послеaskForMessages(refresh: true))
ParkFilterScreen.applyButton вызывает reviewService.didApplyFilter() после dismiss().
Milestone для фильтра:
- 2
- 5
- 15
Если milestone достигнут в сессии, где запрос уже был показан по другому событию, milestone не теряется и будет запрошен в следующей сессии при следующем применении фильтра.
- Бизнес-точка вызывает
requestReviewIfAppropriate()илиdidApplyFilter(). ReviewServiceвыставляетpendingRequest = true, если сессионный лимит не исчерпан.ReviewRequestModifierполучает pending и вызываетrequestReview().- После вызова
requestReview()модификатор вызываетmarkConsumed(). markConsumed()сбрасывает pending и фиксирует milestone фильтра (если запрос пришёл из фильтра).
- В
SwiftUI_WorkoutAppApp.swiftсоздан и прокинут@StateObject reviewServiceчерез.environmentObject(...). - В
RootScreen.swiftподключён.reviewRequestHandling().
Это гарантирует единый обработчик review на уровне корневого дерева приложения.
Файл: WorkoutAppTests/Services/ReviewServiceTests.swift.
Покрыто:
- первичный и повторный вызов
requestReviewIfAppropriate()в одной сессии; - поведение
markConsumed(); - пороги фильтра 2/5/15 и непороговые значения;
- персистентность счётчика и milestone через
UserDefaults(suiteName:); - межсессионный догоняющий сценарий для пропущенного milestone.
- Не фиксировать milestone фильтра до
markConsumed(). - Не вызывать
didApplyFilter()доdismiss()вParkFilterScreen. - Не обходить
ReviewServiceпрямыми вызовами StoreKit из экранов.
- В нужном экране получить
@EnvironmentObject private var reviewService: ReviewService. - Вставить вызов
requestReviewIfAppropriate()строго после успешного завершения бизнес-операции. - Обновить превью/DI при необходимости.
- Добавить/обновить тест в
ReviewServiceTestsили тест экрана, если изменяется порядок вызовов.
make formatmake buildmake test- ручная проверка сценариев:
- контентный триггер;
- фильтр на 2/5/15;
- не более одного запроса в сессии;
- догоняющий milestone в следующей сессии.