Here's a utility function I am developing for my own use case that might help a lot of other people. (Feel free to roast and improve/correct my code :D). I am working on a speech practice app and I want to ask for a rating after the user has done a few recordings. I will add the main function and then other helper functions used below it. The brief logic is, you can request a review 3 times a year, so if 1 year has passed, I reset the ask count to 0. Also, the review request won't be presented for each ask. So I have an upper limit of 30 asks before I don't allow the app to attempt review requests anymore. This won't be taken into consideration if the app version has changed, as you can again ask for a review for the new app version.
/// Requests review from user based on certain conditions.
/// 1. Should have recorderd at least 3 recordings (if you want to force attept a review ask don't pass any parameter)
/// 2. Has not already asked for a review today
/// 3. A probabitly of 50% if will ask today
/// 4. If review has not been asked more than 30 times in the same year for the current version
/// - Parameter numberOfRecordings: If the number of recordings is greater than 3 then a review will be asked.
func askForReview(numberOfRecordings: Int = 5) {
let defaults = UserDefaults.standard
let lastAskedReviewAt = defaults.double(forKey: lastAskedReviewAtKey)
let dateStringForLastReviewAsk = getDateString(from: lastAskedReviewAt)
let dateForLastReviewAsk = getDate(from: dateStringForLastReviewAsk) ?? Date(timeIntervalSince1970: 0)
let askedReviewToday = Calendar.current.isDateInToday(dateForLastReviewAsk)
var appReviewRequestsCount = defaults.integer(forKey: appReviewRequestsCountKey)
if Date().localDate().years(from: dateForLastReviewAsk) >= 1 {
defaults.setValue(0, forKey: appReviewRequestsCountKey)
appReviewRequestsCount = 0
}
var isAskingReviewForSameVersion = false
if let currentlyInstalledVersion = getInstalledVersionNumber(), let lastReviewAskedForVersion = defaults.string(forKey: lastReviewAskedForVersionKey) {
if currentlyInstalledVersion == lastReviewAskedForVersion {
isAskingReviewForSameVersion = true
} else {
appReviewRequestsCount = 0
defaults.setValue(0, forKey: appReviewRequestsCountKey)
}
}
let askingReviewTooManyTimes = appReviewRequestsCount >= 30 && isAskingReviewForSameVersion
let totalRecordingsTillDateCount = defaults.integer(forKey: totalRecordingsTillDateCountKey)
let localNumberOfRecordings = max(numberOfRecordings, totalRecordingsTillDateCount)
if localNumberOfRecordings > 3 && Bool.random() && !askedReviewToday && !askingReviewTooManyTimes {
SKStoreReviewController.requestReview()
defaults.setValue(Date().timeIntervalSince1970, forKey: lastAskedReviewAtKey)
if let versionNumber = getInstalledVersionNumber() {
defaults.setValue(versionNumber, forKey: lastReviewAskedForVersionKey)
}
defaults.setValue(appReviewRequestsCount + 1, forKey: appReviewRequestsCountKey)
}
}
Dictionary Keys:
let lastAskedReviewAtKey = "LastAskedReviewAt"
let appReviewRequestsCountKey = "AppReviewRequestsCount"
let lastReviewAskedForVersionKey = "AskedReviewForVersion"
let appVersionNumberKey = "CFBundleShortVersionString"
Helper Functions:
/// Get a string representation in current local time for a timestamp
/// - Parameter timestamp: Timestamp to be converted to date string
/// - Returns: A date string from passed timestamp in dd MMM yyy format
func getDateString(from timestamp: Double) -> String {
let dateFormatter = getDateFormatter()
let date = Date(timeIntervalSince1970: timestamp)
let dateString = dateFormatter.string(from: date)
return dateString
}
/// Get a date from a string of date format dd MMM yyyy.
/// - Parameter dateString: Date string formated as dd MMM yyyy
/// - Returns: A date object by parsing date in dd MMM yyy format
func getDate(from dateString: String) -> Date? {
// print("Date String: ", dateString)
let dateFormatter = getDateFormatter()
return dateFormatter.date(from: dateString) ?? nil
}
//Ref: https://mcmap.net/q/109038/-getting-the-difference-between-two-dates-months-days-hours-minutes-seconds-in-swift
extension Date {
/// Returns the amount of years from another date
func years(from date: Date) -> Int {
return Calendar.current.dateComponents([.year], from: date, to: self).year ?? 0
}
/// Returns the amount of months from another date
func months(from date: Date) -> Int {
return Calendar.current.dateComponents([.month], from: date, to: self).month ?? 0
}
/// Returns the amount of weeks from another date
func weeks(from date: Date) -> Int {
return Calendar.current.dateComponents([.weekOfMonth], from: date, to: self).weekOfMonth ?? 0
}
/// Returns the amount of days from another date
func days(from date: Date) -> Int {
return Calendar.current.dateComponents([.day], from: date, to: self).day ?? 0
}
/// Returns the amount of hours from another date
func hours(from date: Date) -> Int {
return Calendar.current.dateComponents([.hour], from: date, to: self).hour ?? 0
}
/// Returns the amount of minutes from another date
func minutes(from date: Date) -> Int {
return Calendar.current.dateComponents([.minute], from: date, to: self).minute ?? 0
}
/// Returns the amount of seconds from another date
func seconds(from date: Date) -> Int {
return Calendar.current.dateComponents([.second], from: date, to: self).second ?? 0
}
/// Returns the a custom time interval description from another date
func offset(from date: Date) -> String {
if years(from: date) > 0 { return "\(years(from: date))y" }
if months(from: date) > 0 { return "\(months(from: date))M" }
if weeks(from: date) > 0 { return "\(weeks(from: date))w" }
if days(from: date) > 0 { return "\(days(from: date))d" }
if hours(from: date) > 0 { return "\(hours(from: date))h" }
if minutes(from: date) > 0 { return "\(minutes(from: date))m" }
if seconds(from: date) > 0 { return "\(seconds(from: date))s" }
return ""
}
}
//Ref: https://mcmap.net/q/369458/-swift-get-local-date-and-time
extension Date {
func localDate() -> Date {
let nowUTC = Date()
let timeZoneOffset = Double(TimeZone.current.secondsFromGMT(for: nowUTC))
guard let localDate = Calendar.current.date(byAdding: .second, value: Int(timeZoneOffset), to: nowUTC) else {return Date()}
return localDate
}
}
func getInstalledVersionNumber() -> String? {
guard let infoDictionary = Bundle.main.infoDictionary, let currentVersionNumber = infoDictionary[appVersionNumberKey] as? String else { return nil}
return currentVersionNumber
}