Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use window instead of pushing the lock onto the view controller #65

Open
wants to merge 9 commits into
base: daz/support_carthage_watchos
Choose a base branch
from
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
language: objective-c
osx_image: xcode7
script:
- xctool -workspace VENTouchLock.xcworkspace -scheme VENTouchLockTests -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
- xcodebuild test -workspace VENTouchLock.xcworkspace -scheme VENTouchLockTests -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
Expand Down
5 changes: 0 additions & 5 deletions VENTouchLock/Controllers/VENTouchLockSplashViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,5 @@ typedef NS_ENUM(NSUInteger, VENTouchLockSplashViewControllerUnlockType) {
- (void)dismissWithUnlockSuccess:(BOOL)success
unlockType:(VENTouchLockSplashViewControllerUnlockType)unlockType
animated:(BOOL)animated;
/**
Signals the splash view controller to behave like a snapshot view for app-switch. This method should not be called outside of the VENTouchLock framework.
@param isSnapshotViewController YES if the splash view controller is for the app-switch snapshot. NO othwerise.
*/
- (void)setIsSnapshotViewController:(BOOL)isSnapshotViewController;

@end
49 changes: 14 additions & 35 deletions VENTouchLock/Controllers/VENTouchLockSplashViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@
#import "VENTouchLockEnterPasscodeViewController.h"
#import "VENTouchLock.h"

@interface VENTouchLockSplashViewController ()
@property (nonatomic, assign) BOOL isSnapshotViewController;
@end

@implementation VENTouchLockSplashViewController

#pragma mark - Creation and Lifecycle

- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
if (!self.isSnapshotViewController) {
self.touchLock.backgroundLockVisible = NO;
}
}

- (instancetype)init
Expand Down Expand Up @@ -49,25 +42,11 @@ - (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
if (!self.isSnapshotViewController) {
dispatch_async(dispatch_get_main_queue(), ^{
[self showUnlockAnimated:NO];
});
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
[self showUnlock];
}
}

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
}

- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
}


#pragma mark - Present unlock methods

Expand Down Expand Up @@ -125,13 +104,6 @@ - (VENTouchLockEnterPasscodeViewController *)enterPasscodeVC
return enterPasscodeVC;
}

- (void)appWillEnterForeground
{
if (!self.presentedViewController) {
[self showUnlockAnimated:NO];
}
}

- (void)unlockWithType:(VENTouchLockSplashViewControllerUnlockType)unlockType
{
[self dismissWithUnlockSuccess:YES
Expand All @@ -143,16 +115,23 @@ - (void)dismissWithUnlockSuccess:(BOOL)success
unlockType:(VENTouchLockSplashViewControllerUnlockType)unlockType
animated:(BOOL)animated
{
[self.presentingViewController dismissViewControllerAnimated:animated completion:^{
if (self.didFinishWithSuccess) {
self.didFinishWithSuccess(success, unlockType);
}
}];
[self.touchLock unlockAnimated:animated];
if (self.didFinishWithSuccess) {
self.didFinishWithSuccess(success, unlockType);
}
}

- (void)initialize
{
_touchLock = [VENTouchLock sharedInstance];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showUnlock) name:UIApplicationWillEnterForegroundNotification object:nil];
}

- (void)showUnlock
{
dispatch_async(dispatch_get_main_queue(), ^{
[self showUnlockAnimated:NO];
});
}

@end
11 changes: 7 additions & 4 deletions VENTouchLock/VENTouchLock.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ typedef NS_ENUM(NSUInteger, VENTouchLockTouchIDResponse) {

@interface VENTouchLock : NSObject

/**
YES if the app is locked after having entered the background, and NO otherwise.
*/
@property (assign, nonatomic) BOOL backgroundLockVisible;
@property (assign, nonatomic, getter=isPasscodePresented) BOOL passcodePresented;

/**
@return A singleton VENTouchLock instance.
Expand Down Expand Up @@ -99,6 +96,12 @@ typedef NS_ENUM(NSUInteger, VENTouchLockTouchIDResponse) {
*/
- (void)lock;

/**
If the passcode screen is presented, calling this method will unlock it.
@param animated Whether or not the unlocking should animate.
*/
- (void)unlockAnimated:(BOOL)animated;

/**
@return The proxy for the receiver's user interface. Custom appearance preferences may optionally be set by editing the returned instance's properties.
*/
Expand Down
90 changes: 46 additions & 44 deletions VENTouchLock/VENTouchLock.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ @interface VENTouchLock ()
@property (copy, nonatomic) NSString *touchIDReason;
@property (assign, nonatomic) NSUInteger passcodeAttemptLimit;
@property (assign, nonatomic) Class splashViewControllerClass;
@property (strong, nonatomic) UIView *snapshotView;
@property (strong, nonatomic) VENTouchLockAppearance *appearance;

@property (strong, nonatomic) UIViewController *displayController;

@property (weak, nonatomic) UIWindow *mainWindow;
@property (strong, nonatomic) UIWindow *lockWindow;

@end

@implementation VENTouchLock
Expand All @@ -41,9 +45,11 @@ - (instancetype)init
self = [super init];
if (self) {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
[notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
[notificationCenter addObserver:self selector:@selector(applicationDidFinishLaunching:) name:UIApplicationDidFinishLaunchingNotification object:nil];

[notificationCenter addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
[notificationCenter addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];

self.mainWindow = [[UIApplication sharedApplication].windows firstObject];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we be sure this is the application's main window? It might be better to directly ask UIApplication's delegate for its window?

}
return self;
}
Expand All @@ -62,6 +68,11 @@ - (void)setKeychainService:(NSString *)service
self.splashViewControllerClass = splashViewControllerClass;
}

- (BOOL)isPasscodePresented
{
return self.lockWindow.isKeyWindow;
}


#pragma mark - Keychain Methods

Expand Down Expand Up @@ -91,6 +102,8 @@ - (void)setPasscode:(NSString *)passcode

- (void)deletePasscode
{
[self unlockAnimated:NO];

[[NSUserDefaults standardUserDefaults] removeObjectForKey:VENTouchLockUserDefaultsKeyTouchIDActivated];
[VENTouchLockEnterPasscodeViewController resetPasscodeAttemptHistory];
[[NSUserDefaults standardUserDefaults] synchronize];
Expand Down Expand Up @@ -174,71 +187,60 @@ - (void)requestTouchIDWithCompletion:(void (^)(VENTouchLockTouchIDResponse))comp

- (void)lock
{
if (![self isPasscodeSet]) {
if (![self isPasscodeSet] || self.isPasscodePresented) {
return;
}

if (self.splashViewControllerClass != NULL) {
VENTouchLockSplashViewController *splashViewController = [[self.splashViewControllerClass alloc] init];
if ([splashViewController isKindOfClass:[VENTouchLockSplashViewController class]]) {
UIWindow *mainWindow = [[UIApplication sharedApplication].windows firstObject];
UIViewController *rootViewController = [UIViewController ventouchlock_topMostController];
UIViewController *displayController;
self.lockWindow = [[UIWindow alloc] initWithFrame:self.mainWindow.frame];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding:

self.lockWindow.windowLevel = UIWindowLevelAlert; // or UIWindowLevelAlert + 1

?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will give us the benefit of overlaying other windows which reside at lower, more normal levels.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! Done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up the UIWindowLevel doesn't help in the situation i mentioned, by the same token that UIApplication is a shared instance. Basically, we want to make sure that we call [self.lockWindow makeKeyAndVisible]; last with respect to whatever the host app might be doing. Hope this makes sense :)

By introducing a delay helps, but not ideal

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    [self.lockWindow makeKeyAndVisible];

});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation for -makeKeyAndVisible says that the method makes sure the window is displayed above all other windows with the same window level or lower.

This is a convenience method to make the receiver the main window and displays it in front of other windows at the same window level or lower.

To address your concerns, @zinthemoney, I propose doing a few things to guard against becoming obscured and to alert developers to the possibility that their app may be doing something that collides with how VENTouchLock operates.

  1. On the invocation of -lock, check if -[UIApplication windows] has more than 1 window and -[UIApplication keyWindow] is not equal to -[id<UIApplicationDelegate> window]. If this is the case, we should set lockWindow's windowLevel to be equal to the windowLevel of the currently key window.
  2. Make our lock window a UIWindow subclass that overrides -setRootViewController: and raises an assertion if the argument is any type of view controller that we don't expect. This won't crash in release builds but will crash in debug builds.
  3. Observe the UIWindowDidBecomeKeyNotification and UIWindowDidBecomeVisibleNotification notifications. If we're currently locked and we receive these messages, raise an assertion that informs the developer that they've attempted to display a window on top of VENTouchLock and that this is undefined behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hyperspacemark I agree with your points, my only concern on having this obscured behavior that (potentially) breaks existing VENTouchLock integrations is adding integration complexity. We should solve the issue within the framework, and not pass on the responsibility to the developers. @ayanonagon, @eliperkins what do you think?

self.lockWindow.windowLevel = UIWindowLevelAlert + 1;

if (self.appearance.splashShouldEmbedInNavigationController) {
displayController = [splashViewController ventouchlock_embeddedInNavigationControllerWithNavigationBarClass:self.appearance.navigationBarClass];
self.displayController = [splashViewController ventouchlock_embeddedInNavigationControllerWithNavigationBarClass:self.appearance.navigationBarClass];
}
else {
displayController = splashViewController;
self.displayController = splashViewController;
}

BOOL fromBackground = [UIApplication sharedApplication].applicationState == UIApplicationStateBackground;
if (fromBackground) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
VENTouchLockSplashViewController *snapshotSplashViewController = [[self.splashViewControllerClass alloc] init];
[snapshotSplashViewController setIsSnapshotViewController:YES];
UIViewController *snapshotDisplayController;
if (self.appearance.splashShouldEmbedInNavigationController) {
snapshotDisplayController = [snapshotSplashViewController ventouchlock_embeddedInNavigationControllerWithNavigationBarClass:self.appearance.navigationBarClass];
}
else {
snapshotDisplayController = snapshotSplashViewController;
}
[snapshotDisplayController loadView];
[snapshotDisplayController viewDidLoad];
snapshotDisplayController.view.frame = mainWindow.bounds;
self.snapshotView = snapshotDisplayController.view;
[mainWindow addSubview:self.snapshotView];
}
self.lockWindow.rootViewController = self.displayController;

dispatch_async(dispatch_get_main_queue(), ^{
self.backgroundLockVisible = YES;
[rootViewController presentViewController:displayController animated:NO completion:nil];
[self.lockWindow makeKeyAndVisible];
});
}
}
}

- (void)unlockAnimated:(BOOL)animated
{
dispatch_async(dispatch_get_main_queue(), ^{
if (animated) {
[UIView animateWithDuration:0.2 animations:^{
self.lockWindow.alpha = 0;
} completion:^(BOOL finished) {
self.lockWindow.hidden = YES;
[self.mainWindow makeKeyAndVisible];
}];
} else {
self.lockWindow.hidden = YES;
[self.mainWindow makeKeyAndVisible];
}
});
}


#pragma mark - NSNotifications

- (void)applicationDidFinishLaunching:(NSNotification *)notification
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
[self lock];
}

- (void)applicationDidEnterBackground:(NSNotification *)notification
{
if (!self.backgroundLockVisible) {
[self lock];
}
}

- (void)applicationWillEnterForeground:(NSNotification *)notification
- (void)applicationWillResignActive:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.snapshotView removeFromSuperview];
self.snapshotView = nil;
});

[self lock];
}

@end
10 changes: 5 additions & 5 deletions VENTouchLockSample/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
PODS:
- KIF (3.2.2):
- KIF/XCTest (= 3.2.2)
- KIF/XCTest (3.2.2)
- KIF (3.3.0):
- KIF/Core (= 3.3.0)
- KIF/Core (3.3.0)
- SSKeychain (1.2.2)

DEPENDENCIES:
- KIF (~> 3.0)
- SSKeychain (~> 1.2.2)

SPEC CHECKSUMS:
KIF: b0bd762b0c7890b04920cf618021d6d4fd5127bd
KIF: 0a82046d06f3648799cac522d2d0f7934214caac
SSKeychain: 88767e903ee8d274ed380e364d96b7a101235286

COCOAPODS: 0.37.2
COCOAPODS: 0.38.2
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,7 @@ - (void)performPasscodeAttemptLimitTestWithSplashInNavVC:(BOOL)splashEmbeddedInN

- (void)dismissAndResetLock
{
UIViewController *viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (viewController.presentedViewController) {
viewController = viewController.presentedViewController;
if ([viewController isKindOfClass:[VENTouchLockSplashViewController class]]) {
VENTouchLockSplashViewController *splashViewController = (VENTouchLockSplashViewController *)viewController;
[splashViewController dismissWithUnlockSuccess:YES unlockType:VENTouchLockSplashViewControllerUnlockTypePasscode animated:NO];
}
}
[VENTouchLock sharedInstance].backgroundLockVisible = NO;
[[VENTouchLock sharedInstance] unlockAnimated:NO];
[[VENTouchLock sharedInstance] deletePasscode];
}

Expand All @@ -162,6 +154,7 @@ - (void)simulateAppBackgroundThenForeground
[[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillEnterForegroundNotification object:nil];
[tester waitForTimeInterval:0.5];
[[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidBecomeActiveNotification object:nil];
[tester waitForTimeInterval:0.5];
}

@end