检测 iOS 10.3 应用评级对话框显示的机制?
Mechanism to detect display of iOS 10.3 app rating dialog?
TL;DR:iOS 上是否有某种方法可以检测 iOS 10.3 中添加的 Storekit App Rating 对话框的 presence/display?
我最近使用以下方法为我的应用添加了对新 app rating dialog 的支持:
[SKStoreReviewController requestReview];
但是,我知道有一些使用注意事项(如 documented here),即在调用上述函数时可能会或可能不会显示对话框,除非客户已经给应用评分或客户关闭对话框 3 次。
我还知道 Apple 不希望用户操作直接调用对话框的呈现,因此要报告对话框的存在:
Although you should call this method when it makes sense in the user experience flow of your app, the actual display of a rating/review request view is governed by App Store policy. Because this method may or may not present an alert, it's not appropriate to call it in response to a button tap or other user action.
但这并不能阻止 UX 团队将这些按钮放在图形设计中并询问 "can we know if the dialog was shown"?
所以,我的问题是,是否有其他间接方式可以确定此对话框的呈现方式?
我最近一直在使用 Appium 对 Android 和 iOS 应用程序进行一些自动化测试,并使用 Xpaths 查找本机 UI 元素,所以只是想知道是否相同可以在 iOS 应用程序的上下文中实现。
你的问题让我开始思考,这比我想象的要容易。
我的第一个想法是检查 UIWindow
相关的东西 - 快速查看 the documentation 发现有 UIWindow
相关的通知 - 太棒了!我做了一个快速项目,订阅了所有项目并展示了审查控制器。这在日志中弹出:
method : windowDidBecomeVisibleNotification:
object -> <SKStoreReviewPresentationWindow: 0x7fe14bc03670; baseClass = UIApplicationRotationFollowingWindow; frame = (0 0; 414 736); opaque = NO; gestureRecognizers = <NSArray: 0x61800004de30>; layer = <UIWindowLayer: 0x61800003baa0>>
因此,为了检测是否显示了审查控制器,您需要订阅通知并检查它的 object
属性 以找出它的 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!");
}
}
现在记住 SKStoreReviewPresentationWindow
不是 public 可访问的 - 所以你不能简单地写 [SKStoreReviewPresentationWindow class]
,使用 NSClassFromString
来欺骗系统是就是这样 - 欺骗系统。不幸的是,另一个最有趣的通知 UIWindowDidResignKey
没有发布——我希望主要的 window 会辞职,但不幸的是没有。一些进一步的调试还表明主要 window 仍然是关键并且没有隐藏。您当然可以尝试将 notification.object
与 [UIApplication sharedApplication].window
进行比较,但也显示了其他 windows - UITextEffectsWindow
和 UIRemoteKeyboardWindow
,尤其是当警报首次出现时显示,而且他们两个也不是 public.
我认为这个解决方案是一个 hack - 它很容易被 Apple 更改而破坏它。但最重要的是,这可能是审查期间拒绝的理由,因此使用风险自负。我在 iPhone 7+ 模拟器、iOS 10.3、Xcode 8.3.2
上测试了这个
现在,既然我们现在知道 有点 可以检测是否显示了审查控制器,一个更有趣的问题是如何检测它 未显示。你需要引入一些超时,之后你会做一些事情,因为没有显示警报。这感觉就像您的应用程序被挂起,因此这对您的用户来说是一种糟糕的体验。另外,我注意到审查控制器没有立即显示,所以苹果不建议在按下按钮后显示它更有意义。
好吧,我已经为这个问题做了一个非常巧妙的解决方案:
警告:该解决方案同时包含方法 Swizzling 和对象关联。
该解决方案能够通过 Apple 审查,但将来可能会崩溃。
由于 SKStoreReviewPresentationWindow
继承自 UIWindow
我在 UIWindow 上创建了一个类别,每当 window 显示或隐藏时 post 事件:
@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
像这样使用它:
订阅活动:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(windowDidBecomeVisibleNotification:)
name:UIWindowDidBecomeVisibleNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(windowDidBecomeHiddenNotification:)
name:UIWindowDidBecomeHiddenNotification
object:nil];
并且当事件被触发时对它们做出反应:
- (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!");
}
}
TL;DR:iOS 上是否有某种方法可以检测 iOS 10.3 中添加的 Storekit App Rating 对话框的 presence/display?
我最近使用以下方法为我的应用添加了对新 app rating dialog 的支持:
[SKStoreReviewController requestReview];
但是,我知道有一些使用注意事项(如 documented here),即在调用上述函数时可能会或可能不会显示对话框,除非客户已经给应用评分或客户关闭对话框 3 次。
我还知道 Apple 不希望用户操作直接调用对话框的呈现,因此要报告对话框的存在:
Although you should call this method when it makes sense in the user experience flow of your app, the actual display of a rating/review request view is governed by App Store policy. Because this method may or may not present an alert, it's not appropriate to call it in response to a button tap or other user action.
但这并不能阻止 UX 团队将这些按钮放在图形设计中并询问 "can we know if the dialog was shown"?
所以,我的问题是,是否有其他间接方式可以确定此对话框的呈现方式?
我最近一直在使用 Appium 对 Android 和 iOS 应用程序进行一些自动化测试,并使用 Xpaths 查找本机 UI 元素,所以只是想知道是否相同可以在 iOS 应用程序的上下文中实现。
你的问题让我开始思考,这比我想象的要容易。
我的第一个想法是检查 UIWindow
相关的东西 - 快速查看 the documentation 发现有 UIWindow
相关的通知 - 太棒了!我做了一个快速项目,订阅了所有项目并展示了审查控制器。这在日志中弹出:
method : windowDidBecomeVisibleNotification:
object -> <SKStoreReviewPresentationWindow: 0x7fe14bc03670; baseClass = UIApplicationRotationFollowingWindow; frame = (0 0; 414 736); opaque = NO; gestureRecognizers = <NSArray: 0x61800004de30>; layer = <UIWindowLayer: 0x61800003baa0>>
因此,为了检测是否显示了审查控制器,您需要订阅通知并检查它的 object
属性 以找出它的 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!");
}
}
现在记住 SKStoreReviewPresentationWindow
不是 public 可访问的 - 所以你不能简单地写 [SKStoreReviewPresentationWindow class]
,使用 NSClassFromString
来欺骗系统是就是这样 - 欺骗系统。不幸的是,另一个最有趣的通知 UIWindowDidResignKey
没有发布——我希望主要的 window 会辞职,但不幸的是没有。一些进一步的调试还表明主要 window 仍然是关键并且没有隐藏。您当然可以尝试将 notification.object
与 [UIApplication sharedApplication].window
进行比较,但也显示了其他 windows - UITextEffectsWindow
和 UIRemoteKeyboardWindow
,尤其是当警报首次出现时显示,而且他们两个也不是 public.
我认为这个解决方案是一个 hack - 它很容易被 Apple 更改而破坏它。但最重要的是,这可能是审查期间拒绝的理由,因此使用风险自负。我在 iPhone 7+ 模拟器、iOS 10.3、Xcode 8.3.2
上测试了这个现在,既然我们现在知道 有点 可以检测是否显示了审查控制器,一个更有趣的问题是如何检测它 未显示。你需要引入一些超时,之后你会做一些事情,因为没有显示警报。这感觉就像您的应用程序被挂起,因此这对您的用户来说是一种糟糕的体验。另外,我注意到审查控制器没有立即显示,所以苹果不建议在按下按钮后显示它更有意义。
好吧,我已经为这个问题做了一个非常巧妙的解决方案:
警告:该解决方案同时包含方法 Swizzling 和对象关联。 该解决方案能够通过 Apple 审查,但将来可能会崩溃。
由于 SKStoreReviewPresentationWindow
继承自 UIWindow
我在 UIWindow 上创建了一个类别,每当 window 显示或隐藏时 post 事件:
@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
像这样使用它:
订阅活动:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(windowDidBecomeVisibleNotification:)
name:UIWindowDidBecomeVisibleNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(windowDidBecomeHiddenNotification:)
name:UIWindowDidBecomeHiddenNotification
object:nil];
并且当事件被触发时对它们做出反应:
- (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!");
}
}