Skip to content

Instantly share code, notes, and snippets.

@diamantidis
Last active April 5, 2020 14:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save diamantidis/7c615a886acb04b87428a81f9c1029b8 to your computer and use it in GitHub Desktop.
Save diamantidis/7c615a886acb04b87428a81f9c1029b8 to your computer and use it in GitHub Desktop.
Code snippets for the post 'Help your user rate and review your iOS app!' (https://diamantidis.github.io/2020/04/05/app-reviews-for-iOS-apps)
import Foundation
enum StorageKey: String {
case lastVersionPromptedReview
case dayOfFirstAction
case actionCount
}
protocol Storage {
func integer(forKey key: StorageKey) -> Int
func set(_ value: Int, forKey key: StorageKey)
func object(forKey key: StorageKey) -> Any?
func set(_ value: NSDate, forKey key: StorageKey)
func string(forKey key: StorageKey) -> String?
func set(_ value: String, forKey key: StorageKey)
}
extension UserDefaults: Storage {
func integer(forKey key: StorageKey) -> Int {
return self.integer(forKey: key.rawValue)
}
func set(_ value: Int, forKey key: StorageKey) {
self.set(value, forKey: key.rawValue)
}
func object(forKey key: StorageKey) -> Any? {
return self.object(forKey: key.rawValue)
}
func set(_ value: NSDate, forKey key: StorageKey) {
self.set(value, forKey: key.rawValue)
}
func string(forKey key: StorageKey) -> String? {
return self.string(forKey: key.rawValue)
}
func set(_ value: String, forKey key: StorageKey) {
self.set(value, forKey: key.rawValue)
}
}
import Foundation
protocol BundleType {
func object(forInfoDictionaryKey key: String) -> Any?
}
extension Bundle: BundleType {}
import Foundation
class InAppReviewManager {
// MARK: - Initializers
init(storage: Storage = UserDefaults.standard, bundle: BundleType = Bundle.main) {
self.storage = storage
self.bundle = bundle
}
// MARK: - Public methods
func shouldAskForReview() -> Bool {
// Count the number of actions
var actionCount = storage.integer(forKey: .actionCount)
actionCount += 1
storage.set(actionCount, forKey: .actionCount)
// Store the day of first action
let dateOfFirstActionObj = storage.object(forKey: .dayOfFirstAction)
if dateOfFirstActionObj == nil {
storage.set(NSDate(), forKey: .dayOfFirstAction)
}
// Calculate if there have been three weeks since the first action
let threeWeeksInSeconds: TimeInterval = 3 * 7 * 24 * 60 * 60
let threeWeeksAgo = Date(timeIntervalSinceNow: -threeWeeksInSeconds)
guard let dateOfFirstAction = dateOfFirstActionObj as? Date else {
return false
}
let comparisonResult = dateOfFirstAction.compare(threeWeeksAgo)
// Check if user has reviewed this version
let bundleVersionKey = kCFBundleVersionKey as String
guard let bundleVersion = bundle.object(forInfoDictionaryKey: bundleVersionKey) as? String else {
return false
}
let lastVersion = storage.string(forKey: .lastVersionPromptedReview)
guard case .orderedAscending = comparisonResult, actionCount >= 3, lastVersion == nil || lastVersion != bundleVersion else {
return false
}
storage.set(bundleVersion, forKey: .lastVersionPromptedReview)
return true
}
// MARK: - Private properties
private let storage: Storage
private let bundle: BundleType
}
import UIKit
import StoreKit
...
let manager = InAppReviewManager()
if manager.shouldAskForReview() {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
SKStoreReviewController.requestReview()
}
}
...
import Foundation
@testable import In_App_Review_Demo
class MockStorage: Storage {
var actionCount: Int?
var setActionCallback: ((_ count: Int)->Void)?
var dayOfFirstAction: NSDate?
var setDayCallback: ((_ day: NSDate)->Void)?
var latestVersion: String?
var setVersionCallback: ((_ version: String)->Void)?
func integer(forKey key: StorageKey) -> Int {
return actionCount ?? -1
}
func set(_ value: Int, forKey key: StorageKey) {
setActionCallback?(value)
}
func object(forKey key: StorageKey) -> Any? {
return dayOfFirstAction
}
func set(_ value: NSDate, forKey key: StorageKey) {
setDayCallback?(value)
}
func string(forKey key: StorageKey) -> String? {
return latestVersion
}
func set(_ value: String, forKey key: StorageKey) {
setVersionCallback?(value)
}
}
import Foundation
@testable import In_App_Review_Demo
class MockBundle: BundleType {
var version: String?
func object(forInfoDictionaryKey key: String) -> Any? {
return version
}
}
import XCTest
@testable import In_App_Review_Demo
class InAppReviewManagerTests: XCTestCase {
// MARK: - Overriden methods
override func setUp() {
storage = MockStorage()
bundle = MockBundle()
sut = InAppReviewManager(storage: storage, bundle: bundle)
}
// MARK: - Test methods
func testFirstTimeAction__ShouldNotShowReview() {
// Arrange
storage.actionCount = 0
storage.dayOfFirstAction = nil
storage.latestVersion = nil
bundle.version = "1"
let actionCountExpectation = expectation(description: "func `set(value:Int, forKey: StorageKey)` is called")
storage.setActionCallback = { _ in
actionCountExpectation.fulfill()
}
let dayExpectation = expectation(description: "func `set(value:NSDate, forKey: StorageKey)` is called")
storage.setDayCallback = { _ in
dayExpectation.fulfill()
}
let versionExpectation = expectation(description: "func `set(value:String, forKey: StorageKey)` is called")
versionExpectation.isInverted = true
storage.setVersionCallback = { _ in
versionExpectation.fulfill()
}
// Act
let response = sut.shouldAskForReview()
// Assert
XCTAssertFalse(response)
waitForExpectations(timeout: 1, handler: nil)
}
func testThirdActionAfterThreeWeeksOnNewVersion__ShouldShowReview() {
// Arrange
storage.actionCount = 2
storage.dayOfFirstAction = NSDate(timeIntervalSinceNow: -3 * 7 * 24 * 60 * 60 )
storage.latestVersion = "1"
bundle.version = "2"
let actionCountExpectation = expectation(description: "func `set(value:Int, forKey: StorageKey)` is called")
storage.setActionCallback = { _ in
actionCountExpectation.fulfill()
}
let dayExpectation = expectation(description: "func `set(value:NSDate, forKey: StorageKey)` is called")
dayExpectation.isInverted = true
storage.setDayCallback = { _ in
dayExpectation.fulfill()
}
let versionExpectation = expectation(description: "func `set(value:String, forKey: StorageKey)` is called")
storage.setVersionCallback = { _ in
versionExpectation.fulfill()
}
// Act
let response = sut.shouldAskForReview()
// Assert
XCTAssertTrue(response)
waitForExpectations(timeout: 1, handler: nil)
}
// MARK: - Private properties
private var sut: InAppReviewManager!
private var storage: MockStorage!
private var bundle: MockBundle!
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment