???? Всем привет. Меня зовут Алексей Межевикин. Я iOS-разработчик c 2011 года. Последнии 4 года занимаюсь разработкой, монетизацией и продвижением своих приложений.

В продвижении очень важную роль играет рейтинг приложения в AppStore. Рейтинг формируется исходя из оценок пользователей оставленных либо непосредственно в AppStore либо через специальное окошко, которое вызывается внутри приложения.

В этой статье я предлагаю написать крошечную библиотеку для удобного запроса оценки. По сути, это будет обертка над нативным SKStoreReviewController, которая будет запрашивать отзыв через энное количество запусков, либо энное количество дней после установки.

Немного теории

В iOS есть специальный инструмент для запроса оценки у пользователя. Называется он SKStoreReviewController и доступен начиная с iOS 10.3.

Не смотря на то, что класс называется контроллером это не контроллер в классическом понимании, а по сути обертка над ним. Мы не имеем прямого доступа и не можем его презентовать самостоятельно а только лишь можем запросить его показ через статический метод SKStoreReviewController.requestReview(). При этом вызов метода requestReview не гарантирует показа контроллера. Так как показ возможен только три раза в год, при условии что изменилась версия приложения и пользователь не отключил запрос оценки в настройках iOS.

Поэтому очень важно не потратить заветные три попытки впустую. Для этого давайте напишем небольшую обертку которая возьмет контроль на себя и избавит нас от этой головной боли.

Приступим к коду

Первым делом создадим класс и напишем метод самого запроса. Начиная с iOS 14 метод requestReview запрашивается с передачей текущей сцены. Поэтому используем конструкцию #available для проверки версии iOS. Так как метод может быть вызван до создания сцены, например в applicationDidFinishLaunchingWithOptions() , то вызываем весь код с задержкой в 1 секунду.

class AppReview {

    func request() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            if #available(iOS 14.0, *) {
                if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
                    SKStoreReviewController.requestReview(in: scene)
                }
            } else {
                SKStoreReviewController.requestReview()
            }
        }
    }
    
}

Теперь давайте добавим инициализатор и два свойства.

class AppReview {

  public let minLaunches: Int
  public let minDays: Int

  public init(minLaunches: Int = 0, minDays: Int = 0) {
      self.minLaunches = minLaunches
      self.minDays = minDays
  }
  
}

minLaunches —  минимальное количество запусков после которого можно показать запрос.

minDays —  минимальное количество дней после которого можно показать запрос.

Для хранения информации о количестве запусков, дате первого запуска а так же дате последнего запроса создадим 4 свойства. Значения будем хранить в UserDefaults.

var launches: Int {
  get { ud.integer(forKey: "AppReviewLaunches") }
  set(value) { ud.set(value, forKey: "AppReviewLaunches") }
}
    
var firstLaunchDate: Date? {
  get { ud.object(forKey: "AppReviewFirstLaunchDate") as? Date }
  set(value) { ud.set(value, forKey: "AppReviewFirstLaunchDate") }
}
    
var lastReviewDate: Date? {
  get { ud.object(forKey: "AppReviewLastReviewDate") as? Date }
  set(value) { ud.set(value, forKey: "AppReviewLastReviewDate") }
}
    
var lastReviewVersion: String? {
  get { ud.string(forKey: "AppReviewLastReviewVersion") }
  set(value) { ud.set(value, forKey: "AppReviewLastReviewVersion") }
}

Теперь нам нужно посчитать количество дней после первого запуска приложения и количество дней после последнего запроса оценки. Напишем функцию для этого, а подсчет вынесем в отдельные свойства.

func daysBetween(_ start: Date, _ end: Date) -> Int {
     Calendar.current.dateComponents([.day], from: start, to: end).day!
}

var daysAfterFirstLaunch: Int {
    if let date = firstLaunchDate {
        return daysBetween(date, Date())
    }
    return 0
}
    
var daysAfterLastReview: Int {
    if let date = lastReviewDate {
        return daysBetween(date, Date())
    }
    return 0
}

Давайте создадим свойство isNeeded которое будет решать нужно ли запрашивать оценку сейчас или нет.

var isNeeded: Bool {
   // если количество запусков больше либо равно минимальному
   launches >= minLaunches &&
   // и количество дней после первого запуска больше либо равно минимальному
   daysAfterFirstLaunch >= minDays &&
   // и запроса не разу не было либо прошло больше 125 дней после последнего
   (lastReviewDate == nil || daysAfterLastReview >= 125) &&
   // и версия приложения изменилась после последнего запроса
   lastReviewVersion != version
}

И финальный штрих это метод requestIfNeeded() который мы будем непосредственно использовать. А метод request() который мы написали в первом шаге мы сделаем приватным от греха подальше.

func requestIfNeeded() -> Bool {
  // Устанавливаем дату первого запуска если она пустая
  if firstLaunchDate == nil { firstLaunchDate = Date() }
  // Увеличиваем счетчик запусков
  launches += 1
  // Если не нужно показывать то возвращаем false
  guard isNeeded else { return false }
  // Если нужно то сохраняем дату последнего запроса
  lastReviewDate = Date()
  // И текущую версию приложения
  lastReviewVersion = version
  // Делаем запрос
  request()
  // И возвращаем true говорящей о том что произошел запрос
  return true
}

Добавим немного синтаксического сахарку.

static func requestIf(launches: Int = 0, days: Int = 0) -> Bool {
    AppReview(minLaunches: launches, minDays: days).requestIfNeeded()
}

Чтобы можно было делать так:

// Запрос после третьего запуска приложения
AppReview.requestIf(launches: 3)

// Запрос если прошло 5 дней после первого запуска
AppReview.requestIf(days: 5)

// Запрос если было 3 запуска и прошло 5 дней
AppReview.requestIf(launches: 3, days: 5)

Иногда, возникает необходимость делать запрос оценки после какого-либо действия пользователя, например после того как пользовать купил подписку. Для этого делаем так:

AppReview().requestIfNeeded()

Так как minLaunches и minDays по умолчанию равны нулю то isNeeded будет проверять только то, что после последнего запроса прошло 125 дней и изменилась версия приложения.

Завершение

Теперь собираем весь код приведенный выше в единое целое и публикуем его на GitHub. Получилось всего 100 строк кода.

Ссылка на GitHub

Библиотеку можно использовать как в виде SPM пакета:

https://github.com/mezhevikin/AppReview.git

Так и в виде CocoaPods зависимости:

pod 'AppReview', :git => 'https://github.com/mezhevikin/AppReview.git'

На этом все. Надеюсь библиотека кому-нибудь пригодится. Все предложения и замечания принимаются как виде комментов так и в виде пуллреквестов на GitHub.

Комментарии (6)


  1. Adrenalinevictim
    26.09.2022 19:38
    +1

    прекрасный самопиар, спасибо!


    1. imezhevikin Автор
      26.09.2022 19:39

      пожалуйста!


  1. storoj
    26.09.2022 21:06
    +1

    Так как метод может быть вызван до создания сцены, например в applicationDidFinishLaunchingWithOptions() , то вызываем весь код с задержкой в 1 секунду.

    так может не надо вызывать метод до создания сцены, если она нужна?


    1. imezhevikin Автор
      26.09.2022 21:18

      Я исходил из того что SceneDelegate появился только в iOS 13. Соотвественно если приложение поддерживает более старые версии то либо applicationDidFinishLaunchingWithOptions либо вызывать в контроллере. Поправьте если я не прав


  1. KopievDev
    27.09.2022 10:16
    +1

    Сильно)


    1. imezhevikin Автор
      27.09.2022 10:16

      Спасибо. Исправил и там и тут