6

If the review popup initiated from a view controller shows up, there isn't a way to switch the window focus back to the view controller when the popup is dismissed due to lack of callback function of SKStoreReviewController.requestReview().

I would like to make a call to becomeFirstResponder() when the review popup is dismissed. Any idea?

Is there a way to extend the SKStoreReviewController and add a callback somehow?

zs2020
  • 53,766
  • 29
  • 154
  • 219
  • 1
    not sure if this will help you https://stackoverflow.com/questions/43745157/mechanism-to-detect-display-of-ios-10-3-app-rating-dialog/47457826#47457826 – a4arpan Jan 14 '19 at 16:12

2 Answers2

2

Warning this will probably break at some point.

Step 1: add this code to your didFinishLaunchingWithOptions

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    let windowClass: AnyClass = UIWindow.self

    let originalSelector: Selector = #selector(setter: UIWindow.windowLevel)
    let swizzledSelector: Selector = #selector(UIWindow.setWindowLevel_startMonitor(_:))

    let originalMethod = class_getInstanceMethod(windowClass, originalSelector)
    let swizzledMethod = class_getInstanceMethod(windowClass, swizzledSelector)

    let didAddMethod = class_addMethod(windowClass, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))

    if didAddMethod {
        class_replaceMethod(windowClass, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
    } else {
        method_exchangeImplementations(originalMethod!, swizzledMethod!)
    }

    return true
}

Step 2: add this class

class MonitorObject: NSObject {
    weak var owner: UIWindow?

    init(_ owner: UIWindow?) {
        super.init()
        self.owner = owner
        NotificationCenter.default.post(name: UIWindow.didBecomeVisibleNotification, object: self)
    }

    deinit {
         NotificationCenter.default.post(name: UIWindow.didBecomeHiddenNotification, object: self)
    }
}

Step 3: Add this UIWindow extension

private var monitorObjectKey = "monitorKey"
private var partialDescForStoreReviewWindow = "SKStore"

extension UIWindow {
    // MARK: - Method Swizzling
    @objc func setWindowLevel_startMonitor(_ level: Int) {
        setWindowLevel_startMonitor(level)

        if description.contains(partialDescForStoreReviewWindow) {
             let monObj = MonitorObject(self)
             objc_setAssociatedObject(self, &monitorObjectKey, monObj, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}

Step 4: add this to ViewDidLoad of your controller where you want this

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeHiddenNotification(_:)), name: UIWindow.didBecomeHiddenNotification, object: nil)
}

Step 5: add the callback for the notification and check that the associated object is a match

@objc func windowDidBecomeHiddenNotification(_ notification: Notification?) {
    if notification?.object is MonitorObject {
        print("hello")
    }
}

Now when a review dialog is closed the notification is triggered and 'print("hello") will be called.

Dylan
  • 1,071
  • 1
  • 12
  • 24
  • Probably because it's considered pretty hacky code, and that it could be cleaned a bit more, or that fact I didn't explain it very well. – Dylan Jan 16 '19 at 00:02
  • That's fine. I don't think there exists unhacky code coz Apple documentation doesn't say anything about the call back. Thx for the effort! – zs2020 Jan 16 '19 at 00:08
  • Yeah hope they can fix it in the future – Dylan Jan 16 '19 at 00:11
  • Don't work on iOS 13 but in iOS 12 working, any idea for IOS 13? – ARASHz4 Jan 25 '20 at 07:31
  • I will look into iOS 13 tonight or tomorrow. First thing that comes to mind is checking if the window description has changed (partialDescForStoreReviewWindow in step 3). Just set a break point and check when you open the review window that the description contains that string. If not you know what to do. – Dylan Jan 27 '20 at 00:18
  • When dismiss SKStoreReviewController step 3 not called – ARASHz4 Feb 04 '20 at 10:11
0

Sometimes iOS app is losing the responder chain, like in the above example of showing StoreKit prompt. What we can do is to detect such events in UIApplication.sendAction and reactivate the first responder chain via becomeFirstResponder. UIKit will reestablish the first responder chain and we can resend the same event.

class MyApplication: UIApplication {

    func reactivateResponderChainWhenFirstResponderEventWasNotHandled() {
        becomeFirstResponder()
    }

    override func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool {
        let wasHandled = super.sendAction(action, to: target, from: sender, for: event)
        if wasHandled == false, target == nil {
            reactivateResponderChainWhenFirstResponderEventWasNotHandled()
            return super.sendAction(action, to: target, from: sender, for: event)
        }
        return wasHandled
    }
}

This works for me on iOS 13 and does not require any private API access.

Paul Zabelin
  • 225
  • 2
  • 6