Mechanism to detect display of iOS 10.3 app rating dialog?

谁都会走 提交于 2019-11-30 15:39:07

Your question got me thinking, and it is easier than I would have thought.

My first thought was to check UIWindow related things - a quick look at the documentation revealed, that there are UIWindow related notifications - great! I made a quick project, subscribed to all of them and presented the review controller. This popped up in the logs :

method : windowDidBecomeVisibleNotification:  
object -> <SKStoreReviewPresentationWindow: 0x7fe14bc03670; baseClass = UIApplicationRotationFollowingWindow; frame = (0 0; 414 736); opaque = NO; gestureRecognizers = <NSArray: 0x61800004de30>; layer = <UIWindowLayer: 0x61800003baa0>>

So in order to detect if the review controller was shown, you'd need to subscribe to a notification and inspect it's object property to find out its class :

- (void)viewDidLoad {
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(windowDidBecomeVisibleNotification:)
                                                 name:UIWindowDidBecomeVisibleNotification
                                               object:nil];
}

- (void)windowDidBecomeVisibleNotification:(NSNotification *)notification {
    if ([notification.object isKindOfClass:NSClassFromString(@"SKStoreReviewPresentationWindow")]) {
        NSLog(@"the review request was shown!");
    }
}

Now bear in mind that SKStoreReviewPresentationWindow is not publicly accessible - so you can't simply write [SKStoreReviewPresentationWindow class], and tricking the system by using NSClassFromString is just that - tricking the system. Unfortunately the other most interesting notification, UIWindowDidResignKey, was not issued - I hoped that the main window would resign, but unfortunately not. Some further debugging also showed that the main window remains key and not hidden. You could of course try comparing the notification.object to [UIApplication sharedApplication].window, but there were also other windows being shown - UITextEffectsWindow and UIRemoteKeyboardWindow, especially when the alert was first shown, and both of them are also not public.

I'd consider this solution a hack - it is prone to changes by Apple that will break it. But most importantly, this could be grounds for rejection during review, so use at your own risk. I tested this on iPhone 7+ Simulator, iOS 10.3, Xcode 8.3.2


Now, since we now know that it is kinda possible to detect if the review controller was shown, a more interesting problem is how to detect that it was NOT shown. You'd need to introduce some timeout after which you'd do something because the alert was not shown. This can feel like your app hanged, so it would be a bad experience for your users. Also, I noticed that the review controller is not shown immediately, so it even makes more sense why Apple doesn't recommend showing it after pressing a button.

Well, I have made a pretty hacked solution to this problem:

WARNING:The solution contains both method Swizzling and object association. The solution is able to go through a Apple review, but it will likely break in the future.

Since SKStoreReviewPresentationWindow is inheriting from UIWindow I have made a category on UIWindow, that post events whenever the window is shown or hidden:

@interface MonitorObject:NSObject

@property (nonatomic, weak) UIWindow* owner;

-(id)init:(UIWindow*)owner;
-(void)dealloc;

@end

@interface UIWindow (DismissNotification)

+ (void)load;

@end

#import "UIWindow+DismissNotification.h"
#import <objc/runtime.h>

@implementation MonitorObject


-(id)init:(UIWindow*)owner
{
    self = [super init];
    self.owner = owner;
    [[NSNotificationCenter defaultCenter] postNotificationName:UIWindowDidBecomeVisibleNotification object:self];
    return self;

}
-(void)dealloc
{
      [[NSNotificationCenter defaultCenter] postNotificationName:UIWindowDidBecomeHiddenNotification object:self];
}

@end



@implementation UIWindow (DismissNotification)

static NSString* monitorObjectKey = @"monitorKey";
static NSString* partialDescForStoreReviewWindow =  @"SKStore";
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(setWindowLevel:);
        SEL swizzledSelector = @selector(setWindowLevel_startMonitor:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}


#pragma mark - Method Swizzling

- (void)setWindowLevel_startMonitor:(int)level{
    [self setWindowLevel_startMonitor:level];

    if([self.description containsString:partialDescForStoreReviewWindow])
    {
        MonitorObject *monObj = [[MonitorObject alloc] init:self];
        objc_setAssociatedObject(self, &monitorObjectKey, monObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    }
}

@end

Use it like:

Subscribe to the events:

 [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(windowDidBecomeVisibleNotification:)
                                                 name:UIWindowDidBecomeVisibleNotification
                                               object:nil];


 [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(windowDidBecomeHiddenNotification:)
                                                 name:UIWindowDidBecomeHiddenNotification
                                               object:nil];

And when the events get fired react on them:

- (void)windowDidBecomeVisibleNotification:(NSNotification *)notification
{
    if([notification.object class] == [MonitorObject class])
    {
        NSLog(@"Review Window shown!");
    }
}

- (void)windowDidBecomeHiddenNotification:(NSNotification *)notification
{
    if([notification.object class] == [MonitorObject class])
    {
        NSLog(@"Review Window hidden!");
    }
}

You can see a video of the solution in action here

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!