diff --git a/External/LoveLyndir b/External/LoveLyndir index 97eafd9b..0b858644 160000 --- a/External/LoveLyndir +++ b/External/LoveLyndir @@ -1 +1 @@ -Subproject commit 97eafd9b59f84bd9e3fc3cb4313df8b3c034e766 +Subproject commit 0b858644cb58c357e055b41c2747ad9d45fa9ce3 diff --git a/External/Pearl b/External/Pearl index 575b409c..67321728 160000 --- a/External/Pearl +++ b/External/Pearl @@ -1 +1 @@ -Subproject commit 575b409cca36eabfaacf0a963ed259454cb8ec66 +Subproject commit 673217287de2920d21ef84f1d6811d8250f187b4 diff --git a/External/Reveal.framework/Versions/A/Headers/IBARevealLogger.h b/External/Reveal.framework/Versions/A/Headers/IBARevealLogger.h index 92193c71..a2dd329c 100644 --- a/External/Reveal.framework/Versions/A/Headers/IBARevealLogger.h +++ b/External/Reveal.framework/Versions/A/Headers/IBARevealLogger.h @@ -7,7 +7,7 @@ CF_EXTERN_C_BEGIN /*! \brief The Reveal Log level bit flags. - \discussion These flags are addative. Ie, you should bitwise OR them together. + \discussion These flags are additive. i.e. you should bitwise OR them together. \seealso IBARevealLoggerSetLevelMask \seealso IBARevealLoggerGetLevelMask diff --git a/External/Reveal.framework/Versions/A/Reveal b/External/Reveal.framework/Versions/A/Reveal index 0ae41f5e..8ef14875 100644 Binary files a/External/Reveal.framework/Versions/A/Reveal and b/External/Reveal.framework/Versions/A/Reveal differ diff --git a/MasterPassword.xcworkspace/xcshareddata/MasterPassword.xccheckout b/MasterPassword.xcworkspace/xcshareddata/MasterPassword.xccheckout index 69d287bd..663349ee 100644 --- a/MasterPassword.xcworkspace/xcshareddata/MasterPassword.xccheckout +++ b/MasterPassword.xcworkspace/xcshareddata/MasterPassword.xccheckout @@ -5,13 +5,11 @@ IDESourceControlProjectFavoriteDictionaryKey IDESourceControlProjectIdentifier - D4AB9F0C-D746-4319-AABF-B24705099AED + 3D68B2F1-988A-48C3-8450-B37D43BDFE92 IDESourceControlProjectName MasterPassword IDESourceControlProjectOriginsDictionary - 2B6DA448-3730-4F84-B2C3-51272E0D42F3 - ssh://github.com/lhunath/DCIntrospect.git 5263993D-5FE8-464F-B66E-B0F7C2DFF410 ssh://github.com/lhunath/UbiquityStoreManager.git 6A449EC2-A2A3-4635-9C5F-A811E011EAC3 @@ -24,8 +22,10 @@ git://github.com/lhunath/uicolor-utilities.git B0F634DD-AEE1-4F0D-AE35-4FAF51AD1B5A git://github.com/lhunath/RHStatusItemView.git - CBA93B91-B799-4CC6-85B6-749792B76DD4 - ssh://github.com/lhunath/InAppSettingsKit.git + CDDE92CF-0136-4DE0-8318-80EDB5C8CAF9 + git://github.com/lhunath/InAppSettingsKit.git + D5CE8AB8-2F69-4A08-A2CE-93C70E0F0567 + https://github.com/lhunath/DCIntrospect.git E4C8E206-229C-4DA8-A130-0C544DEC7E07 git://github.com/jonmarimba/jrswizzle.git FF42A9E0-F41C-42FC-88CD-F2CCDE15DBB6 @@ -35,8 +35,6 @@ MasterPassword.xcworkspace IDESourceControlProjectRelativeInstallPathDictionary - 2B6DA448-3730-4F84-B2C3-51272E0D42F3 - ../External/DCIntrospect 5263993D-5FE8-464F-B66E-B0F7C2DFF410 ../External/UbiquityStoreManager 6A449EC2-A2A3-4635-9C5F-A811E011EAC3 @@ -49,8 +47,10 @@ ../External/Pearl/External/uicolor-utilities B0F634DD-AEE1-4F0D-AE35-4FAF51AD1B5A ../External/RHStatusItemView - CBA93B91-B799-4CC6-85B6-749792B76DD4 + CDDE92CF-0136-4DE0-8318-80EDB5C8CAF9 ../External/InAppSettingsKit + D5CE8AB8-2F69-4A08-A2CE-93C70E0F0567 + ../External/DCIntrospect E4C8E206-229C-4DA8-A130-0C544DEC7E07 ../External/Pearl/External/jrswizzle FF42A9E0-F41C-42FC-88CD-F2CCDE15DBB6 @@ -68,7 +68,7 @@ IDESourceControlRepositoryExtensionIdentifierKey public.vcs.git IDESourceControlWCCIdentifierKey - 2B6DA448-3730-4F84-B2C3-51272E0D42F3 + D5CE8AB8-2F69-4A08-A2CE-93C70E0F0567 IDESourceControlWCCName DCIntrospect @@ -84,7 +84,7 @@ IDESourceControlRepositoryExtensionIdentifierKey public.vcs.git IDESourceControlWCCIdentifierKey - CBA93B91-B799-4CC6-85B6-749792B76DD4 + CDDE92CF-0136-4DE0-8318-80EDB5C8CAF9 IDESourceControlWCCName InAppSettingsKit diff --git a/MasterPassword/ObjC/iOS/MPAvatarCell.h b/MasterPassword/ObjC/iOS/MPAvatarCell.h index a3ff62cb..f1d0ac9e 100644 --- a/MasterPassword/ObjC/iOS/MPAvatarCell.h +++ b/MasterPassword/ObjC/iOS/MPAvatarCell.h @@ -26,15 +26,21 @@ extern const long MPAvatarAdd; typedef NS_ENUM(NSUInteger, MPAvatarMode) { MPAvatarModeLowered, MPAvatarModeRaisedButInactive, - MPAvatarModeRaisedAndActive + MPAvatarModeRaisedAndActive, + MPAvatarModeRaisedAndHidden, + MPAvatarModeRaisedAndMinimized, }; @interface MPAvatarCell : UICollectionViewCell @property (copy, nonatomic) NSString *name; @property (assign, nonatomic) long avatar; @property (assign, nonatomic) MPAvatarMode mode; -@property (assign, nonatomic) float visibility; +@property (assign, nonatomic) CGFloat visibility; +@property (assign, nonatomic) BOOL spinnerActive; + (NSString *)reuseIdentifier; +- (void)setVisibility:(CGFloat)visibility animated:(BOOL)animated; +- (void)setMode:(MPAvatarMode)mode animated:(BOOL)animated; + @end diff --git a/MasterPassword/ObjC/iOS/MPAvatarCell.m b/MasterPassword/ObjC/iOS/MPAvatarCell.m index 523862b6..d1e44f94 100644 --- a/MasterPassword/ObjC/iOS/MPAvatarCell.m +++ b/MasterPassword/ObjC/iOS/MPAvatarCell.m @@ -25,7 +25,10 @@ const long MPAvatarAdd = 10000; @property(strong, nonatomic) IBOutlet UIImageView *avatarImageView; @property(strong, nonatomic) IBOutlet UILabel *nameLabel; @property(strong, nonatomic) IBOutlet UIView *nameContainer; +@property(strong, nonatomic) IBOutlet UIImageView *spinner; @property(strong, nonatomic) IBOutlet NSLayoutConstraint *nameCenterConstraint; +@property(strong, nonatomic) IBOutlet NSLayoutConstraint *avatarSizeConstraint; +@property(strong, nonatomic) IBOutlet NSLayoutConstraint *avatarTopConstraint; @end @@ -37,6 +40,8 @@ const long MPAvatarAdd = 10000; return @"MPAvatarCell"; } +#pragma mark - Life cycle + - (void)awakeFromNib { [super awakeFromNib]; @@ -45,28 +50,46 @@ const long MPAvatarAdd = 10000; self.avatarImageView.hidden = NO; self.avatarImageView.layer.cornerRadius = self.avatarImageView.bounds.size.height / 2; - self.avatarImageView.layer.shadowColor = [UIColor blackColor].CGColor; - self.avatarImageView.layer.shadowOpacity = 1; - self.avatarImageView.layer.shadowRadius = 15; self.avatarImageView.layer.masksToBounds = NO; self.avatarImageView.backgroundColor = [UIColor clearColor]; [self observeKeyPath:@"selected" withBlock:^(id from, id to, NSKeyValueChange cause, id _self) { - [_self onSelectedOrHighlighted]; + [_self updateAnimated:YES]; }]; [self observeKeyPath:@"highlighted" withBlock:^(id from, id to, NSKeyValueChange cause, id _self) { - [_self onSelectedOrHighlighted]; + [_self updateAnimated:YES]; }]; - self.visibility = 0; - self.mode = MPAvatarModeLowered; + CABasicAnimation *toShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"]; + toShadowOpacityAnimation.toValue = @0.2f; + toShadowOpacityAnimation.duration = 0.5f; + + CABasicAnimation *pulseShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"]; + pulseShadowOpacityAnimation.fromValue = @0.2f; + pulseShadowOpacityAnimation.toValue = @0.6f; + pulseShadowOpacityAnimation.beginTime = 0.5f; + pulseShadowOpacityAnimation.duration = 2.0f; + pulseShadowOpacityAnimation.autoreverses = YES; + pulseShadowOpacityAnimation.repeatCount = MAXFLOAT; + + CAAnimationGroup *group = [CAAnimationGroup new]; + group.animations = @[ toShadowOpacityAnimation, pulseShadowOpacityAnimation ]; + group.duration = MAXFLOAT; + [self.avatarImageView.layer addAnimation:group forKey:@"targetedShadow"]; + self.avatarImageView.layer.shadowColor = [UIColor whiteColor].CGColor; + self.avatarImageView.layer.shadowOffset = CGSizeZero; + + [self setVisibility:0 animated:NO]; + [self setMode:MPAvatarModeLowered animated:NO]; } -- (void)onSelectedOrHighlighted { +- (void)dealloc { - self.avatarImageView.backgroundColor = self.selected || self.highlighted? self.avatarImageView.tintColor: [UIColor clearColor]; + [self removeKeyPathObservers]; } +#pragma mark - Properties + - (void)setAvatar:(long)avatar { _avatar = avatar; @@ -87,11 +110,16 @@ const long MPAvatarAdd = 10000; self.nameLabel.text = name; } -- (void)setVisibility:(float)visibility { +- (void)setVisibility:(CGFloat)visibility { + + [self setVisibility:visibility animated:YES]; +} + +- (void)setVisibility:(CGFloat)visibility animated:(BOOL)animated { _visibility = visibility; - self.nameContainer.alpha = visibility; + [self updateAnimated:animated]; } - (void)setHighlighted:(BOOL)highlighted { @@ -105,35 +133,117 @@ const long MPAvatarAdd = 10000; - (void)setMode:(MPAvatarMode)mode { + [self setMode:mode animated:YES]; +} + +- (void)setMode:(MPAvatarMode)mode animated:(BOOL)animated { + _mode = mode; - [UIView animateWithDuration:0.2f animations:^{ + [self updateAnimated:animated]; +} + +- (void)setSpinnerActive:(BOOL)spinnerActive { + + [self setSpinnerActive:spinnerActive animated:YES]; +} + +- (void)setSpinnerActive:(BOOL)spinnerActive animated:(BOOL)animated { + + _spinnerActive = spinnerActive; + + CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation"]; + rotate.toValue = [NSNumber numberWithDouble:2 * M_PI]; + rotate.duration = 5.0; + + if (spinnerActive) { + rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + rotate.fromValue = @0.0; + rotate.repeatCount = MAXFLOAT; + } + else { + rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; + rotate.repeatCount = 1; + } + + [self.spinner.layer removeAnimationForKey:@"rotation"]; + [self.spinner.layer addAnimation:rotate forKey:@"rotation"]; + + [self updateAnimated:animated]; +} + +#pragma mark - Private + +- (void)updateAnimated:(BOOL)animated { + + [UIView animateWithDuration:animated? 0.2f: 0 animations:^{ self.avatarImageView.transform = CGAffineTransformIdentity; }]; - [UIView animateWithDuration:0.3f animations:^{ - switch (mode) { + [UIView animateWithDuration:animated? 0.3f: 0 animations:^{ + switch (self.mode) { case MPAvatarModeLowered: { + self.avatarSizeConstraint.constant = self.avatarImageView.image.size.height; + self.avatarTopConstraint.priority = UILayoutPriorityDefaultLow; self.nameCenterConstraint.priority = UILayoutPriorityDefaultLow; + self.nameContainer.alpha = self.visibility; self.nameContainer.backgroundColor = [UIColor clearColor]; - self.avatarImageView.alpha = 1; + self.avatarImageView.alpha = self.visibility / 0.7f + 0.3f; + self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility; break; } case MPAvatarModeRaisedButInactive: { + self.avatarSizeConstraint.constant = self.avatarImageView.image.size.height; + self.avatarTopConstraint.priority = UILayoutPriorityDefaultLow; self.nameCenterConstraint.priority = UILayoutPriorityDefaultLow; + self.nameContainer.alpha = self.visibility; self.nameContainer.backgroundColor = [UIColor clearColor]; - self.avatarImageView.alpha = 0.3f; + self.avatarImageView.alpha = 0; + self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility; break; } case MPAvatarModeRaisedAndActive: { + self.avatarSizeConstraint.constant = self.avatarImageView.image.size.height; + self.avatarTopConstraint.priority = UILayoutPriorityDefaultLow; self.nameCenterConstraint.priority = UILayoutPriorityDefaultHigh; + self.nameContainer.alpha = self.visibility; self.nameContainer.backgroundColor = [UIColor blackColor]; self.avatarImageView.alpha = 1; + self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility; + break; + } + case MPAvatarModeRaisedAndHidden: { + self.avatarSizeConstraint.constant = self.avatarImageView.image.size.height; + self.avatarTopConstraint.priority = UILayoutPriorityDefaultLow; + self.nameCenterConstraint.priority = UILayoutPriorityDefaultHigh; + self.nameContainer.alpha = 0; + self.nameContainer.backgroundColor = [UIColor blackColor]; + self.avatarImageView.alpha = 0; + self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility; + break; + } + case MPAvatarModeRaisedAndMinimized: { + self.avatarSizeConstraint.constant = 36; + self.avatarTopConstraint.priority = UILayoutPriorityDefaultHigh; + self.nameCenterConstraint.priority = UILayoutPriorityDefaultHigh; + self.nameContainer.alpha = 0; + self.nameContainer.backgroundColor = [UIColor blackColor]; + self.avatarImageView.alpha = 1; + self.avatarImageView.layer.shadowOpacity = 0; break; } } - + [self.avatarSizeConstraint apply]; + [self.avatarTopConstraint apply]; [self.nameCenterConstraint apply]; + + // Avatar selection and spinner. + if (self.mode != MPAvatarModeRaisedAndMinimized && (self.selected || self.highlighted) && !self.spinnerActive) + self.avatarImageView.backgroundColor = self.avatarImageView.tintColor; + else + self.avatarImageView.backgroundColor = [UIColor clearColor]; + self.avatarImageView.layer.cornerRadius = self.avatarImageView.bounds.size.height / 2; + self.spinner.alpha = self.spinnerActive? 1: 0; }]; } diff --git a/MasterPassword/ObjC/iOS/MPCombinedViewController.h b/MasterPassword/ObjC/iOS/MPCombinedViewController.h index 00363762..cec775b7 100644 --- a/MasterPassword/ObjC/iOS/MPCombinedViewController.h +++ b/MasterPassword/ObjC/iOS/MPCombinedViewController.h @@ -16,34 +16,18 @@ // Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. // -#import "LLGitTip.h" - typedef NS_ENUM(NSUInteger, MPCombinedMode) { MPCombinedModeUserSelection, MPCombinedModePasswordSelection, }; -@interface MPCombinedViewController : UIViewController +@interface MPCombinedViewController : UIViewController + +@property (strong, nonatomic) IBOutlet UIView *usersView; +@property (strong, nonatomic) IBOutlet UIView *passwordsView; @property(assign, nonatomic) MPCombinedMode mode; -#pragma mark - UserSelection - -@property(strong, nonatomic) IBOutlet UIView *userSelectionContainer; -@property(weak, nonatomic) IBOutlet UILabel *hintLabel; -@property(weak, nonatomic) IBOutlet UIView *gitTipTip; -@property(weak, nonatomic) IBOutlet LLGitTip *gitTipButton; -@property(weak, nonatomic) IBOutlet UITextField *entryField; -@property(weak, nonatomic) IBOutlet UILabel *entryLabel; -@property(weak, nonatomic) IBOutlet UIView *entryContainer; -@property(weak, nonatomic) IBOutlet UICollectionView *avatarCollectionView; -@property (strong, nonatomic) IBOutlet NSLayoutConstraint *avatarCollectionCenterConstraint; - -#pragma mark - PasswordSelection - -@property(strong, nonatomic) IBOutlet UIView *passwordSelectionContainer; -@property(strong, nonatomic) IBOutlet UICollectionView *passwordCollectionView; - - (IBAction)doSignOut:(UIBarButtonItem *)sender; @end diff --git a/MasterPassword/ObjC/iOS/MPCombinedViewController.m b/MasterPassword/ObjC/iOS/MPCombinedViewController.m index 3b77a746..b365e918 100644 --- a/MasterPassword/ObjC/iOS/MPCombinedViewController.m +++ b/MasterPassword/ObjC/iOS/MPCombinedViewController.m @@ -17,45 +17,35 @@ // #import "MPCombinedViewController.h" -#import "MPEntities.h" -#import "MPAvatarCell.h" #import "MPiOSAppDelegate.h" #import "MPAppDelegate_Store.h" #import "MPAppDelegate_Key.h" - -typedef NS_ENUM(NSUInteger, MPActiveUserState) { - MPActiveUserStateNone, - MPActiveUserStateLogin, - MPActiveUserStateUserName, - MPActiveUserStateMasterPasswordChoice, - MPActiveUserStateMasterPasswordConfirmation, -}; +#import "MPUsersViewController.h" +#import "MPPasswordsViewController.h" @interface MPCombinedViewController() -@property(nonatomic) MPActiveUserState activeUserState; -@property(nonatomic, strong) NSArray *userIDs; +@property(strong, nonatomic) IBOutlet NSLayoutConstraint *passwordsTopConstraint; +@property(nonatomic, strong) MPUsersViewController *usersVC; +@property(nonatomic, strong) MPPasswordsViewController *passwordsVC; @end @implementation MPCombinedViewController { - __weak id _storeObserver; - __weak id _mocObserver; NSArray *_notificationObservers; - NSString *_masterPasswordChoice; } - (void)viewDidLoad { [super viewDidLoad]; - self.avatarCollectionView.allowsMultipleSelection = YES; + [self setMode:MPCombinedModeUserSelection animated:NO]; +} - [self observeKeyPath:@"avatarCollectionView.contentOffset" withBlock: - ^(id from, id to, NSKeyValueChange cause, MPCombinedViewController *_self) { - [_self updateAvatars]; - }]; +- (void)viewWillAppear:(BOOL)animated { - self.mode = MPCombinedModeUserSelection; + [super viewWillAppear:animated]; + + [[self navigationController] setNavigationBarHidden:YES animated:animated]; } - (void)viewDidAppear:(BOOL)animated { @@ -63,7 +53,6 @@ typedef NS_ENUM(NSUInteger, MPActiveUserState) { [super viewDidAppear:animated]; [self registerObservers]; - [self updateMode]; } - (void)viewWillDisappear:(BOOL)animated { @@ -71,353 +60,60 @@ typedef NS_ENUM(NSUInteger, MPActiveUserState) { [super viewWillDisappear:animated]; [self removeObservers]; - [self needStoreObserved:NO]; } -#pragma mark - UITextFieldDelegate +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { -- (void)textFieldDidEndEditing:(UITextField *)textField { + if ([segue.identifier isEqualToString:@"users"]) + self.usersVC = segue.destinationViewController; + if ([segue.identifier isEqualToString:@"passwords"]) + self.passwordsVC = segue.destinationViewController; } -- (BOOL)textFieldShouldReturn:(UITextField *)textField { +- (UIStatusBarStyle)preferredStatusBarStyle { - if (textField == self.entryField) { - switch (self.activeUserState) { - case MPActiveUserStateNone: { - [textField resignFirstResponder]; + return UIStatusBarStyleLightContent; +} + + +#pragma mark - Properties + +- (void)setMode:(MPCombinedMode)mode { + + [self setMode:mode animated:YES]; +} + +- (void)setMode:(MPCombinedMode)mode animated:(BOOL)animated { + + _mode = mode; + + [self becomeFirstResponder]; + + [UIView animateWithDuration:animated? 0.3f: 0 animations:^{ + switch (self.mode) { + case MPCombinedModeUserSelection: { + [self.usersVC setActive:YES animated:NO]; + [self.passwordsVC setActive:NO animated:NO]; +// MPUsersViewController *usersVC = [self.storyboard instantiateViewControllerWithIdentifier:@"MPUsersViewController"]; +// [self setViewControllers:@[ usersVC ] direction:UIPageViewControllerNavigationDirectionReverse +// animated:animated completion:nil]; break; } - case MPActiveUserStateLogin: { - [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { - BOOL signedIn = NO, isNew = NO; - MPUserEntity *user = [self selectedUserInContext:context isNew:&isNew]; - if (!isNew && user) - signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context - usingMasterPassword:self.entryField.text]; - - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - if (!signedIn) { - // Sign in failed. - // TODO: warn user - return; - } - }]; - }]; - break; - } - case MPActiveUserStateUserName: { - NSString *userName = self.entryField.text; - if (![userName length]) { - // No name entered. - // TODO: warn user - return NO; - } - - [self selectedAvatar].name = userName; - self.activeUserState = MPActiveUserStateMasterPasswordChoice; - break; - } - case MPActiveUserStateMasterPasswordChoice: { - NSString *masterPassword = self.entryField.text; - if (![masterPassword length]) { - // No password entered. - // TODO: warn user - return NO; - } - - self.activeUserState = MPActiveUserStateMasterPasswordConfirmation; - break; - } - case MPActiveUserStateMasterPasswordConfirmation: { - NSString *masterPassword = self.entryField.text; - if (![masterPassword length]) { - // No password entered. - // TODO: warn user - return NO; - } - - if (![masterPassword isEqualToString:_masterPasswordChoice]) { - // Master password confirmation failed. - // TODO: warn user - self.activeUserState = MPActiveUserStateMasterPasswordChoice; - return NO; - } - - [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { - BOOL isNew = NO; - MPUserEntity *user = [self selectedUserInContext:context isNew:&isNew]; - if (isNew) { - user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass( [MPUserEntity class] ) - inManagedObjectContext:context]; - MPAvatarCell *avatarCell = [self selectedAvatar]; - user.avatar = avatarCell.avatar; - user.name = avatarCell.name; - } - - BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context usingMasterPassword:masterPassword]; - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - if (!signedIn) { - // Sign in failed, shouldn't happen for a new user. - // TODO: warn user - self.activeUserState = MPActiveUserStateNone; - return; - } - }]; - }]; - + case MPCombinedModePasswordSelection: { + [self.usersVC setActive:NO animated:NO]; + [self.passwordsVC setActive:YES animated:NO]; +// MPPasswordsViewController *passwordsVC = [self.storyboard instantiateViewControllerWithIdentifier:@"MPPasswordsViewController"]; +// [self setViewControllers:@[ passwordsVC ] direction:UIPageViewControllerNavigationDirectionForward +// animated:animated completion:nil]; break; } } - } - return NO; + [self.passwordsTopConstraint apply]; + }]; } -// This isn't really in UITextFieldDelegate. We fake it from UITextFieldTextDidChangeNotification. -- (void)textFieldDidChange:(UITextField *)textField { - - if (textField == self.entryField) { - switch (self.activeUserState) { - case MPActiveUserStateNone: - break; - case MPActiveUserStateLogin: - break; - case MPActiveUserStateUserName: { - NSString *userName = self.entryField.text; - [self selectedAvatar].name = [userName length]? userName: strl( @"New User" ); - break; - } - case MPActiveUserStateMasterPasswordChoice: - break; - case MPActiveUserStateMasterPasswordConfirmation: - break; - } - } -} - -#pragma mark - UICollectionViewDataSource - -- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - - if (collectionView == self.avatarCollectionView) - return [self.userIDs count] + 1; - - else if (collectionView == self.passwordCollectionView) - return 0; - - Throw(@"unexpected collection view: %@", collectionView); -} - -- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - - if (collectionView == self.avatarCollectionView) { - MPAvatarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MPAvatarCell reuseIdentifier] forIndexPath:indexPath]; - [cell addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(didLongPress:)]]; - [self updateAvatar:cell atIndexPath:indexPath]; - - BOOL isNew = NO; - MPUserEntity *user = [self userForIndexPath:indexPath inContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady] - isNew:&isNew]; - if (isNew) { - // New User - cell.avatar = MPAvatarAdd; - cell.name = strl( @"New User" ); - } - else { - // Existing User - cell.avatar = user.avatar; - cell.name = user.name; - } - - NSArray *selectedIndexPaths = [self.avatarCollectionView indexPathsForSelectedItems]; - if (![selectedIndexPaths count]) - cell.mode = MPAvatarModeLowered; - else if ([selectedIndexPaths containsObject:indexPath]) - cell.mode = MPAvatarModeRaisedAndActive; - else - cell.mode = MPAvatarModeRaisedButInactive; - - return cell; - } - - else if (collectionView == self.passwordCollectionView) - return nil; - - Throw(@"unexpected collection view: %@", collectionView); -} - -- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { - - if (collectionView == self.avatarCollectionView) { - [self.avatarCollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally - animated:YES]; - - [UIView animateWithDuration:0.3f animations:^{ - for (NSUInteger otherItem = 0; otherItem < [collectionView numberOfItemsInSection:indexPath.section]; ++otherItem) - if (otherItem != indexPath.item) { - NSIndexPath *otherIndexPath = [NSIndexPath indexPathForItem:otherItem inSection:indexPath.section]; - [collectionView deselectItemAtIndexPath:otherIndexPath animated:YES]; - - MPAvatarCell *otherCell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:otherIndexPath]; - otherCell.mode = MPAvatarModeRaisedButInactive; - } - - MPAvatarCell *cell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath]; - cell.mode = MPAvatarModeRaisedAndActive; - }]; - - BOOL isNew = NO; - MPUserEntity *user = [self userForIndexPath:indexPath inContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady] - isNew:&isNew]; - if (isNew) - self.activeUserState = MPActiveUserStateUserName; - else if (!user.keyID) - self.activeUserState = MPActiveUserStateMasterPasswordChoice; - else - self.activeUserState = MPActiveUserStateLogin; - } -} - -- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath { - - if (collectionView == self.avatarCollectionView) { - self.activeUserState = MPActiveUserStateNone; - } -} - -#pragma mark - UILongPressGestureRecognizer - -- (void)didLongPress:(UILongPressGestureRecognizer *)recognizer { - - if ([recognizer.view isKindOfClass:[MPAvatarCell class]]) { - if (recognizer.state != UIGestureRecognizerStateBegan) - // Don't show the action menu unless the state is Began. - return; - - MPAvatarCell *avatarCell = (MPAvatarCell *)recognizer.view; - NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady]; - - BOOL isNew = NO; - MPUserEntity *user = [self userForAvatar:avatarCell inContext:mainContext isNew:&isNew]; - NSManagedObjectID *userID = user.objectID; - if (isNew || !user) - return; - - [PearlSheet showSheetWithTitle:user.name - viewStyle:UIActionSheetStyleBlackTranslucent - initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) { - if (buttonIndex == [sheet cancelButtonIndex]) - return; - - if (buttonIndex == [sheet destructiveButtonIndex]) { - // Delete User - [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { - NSManagedObject *user_ = [context existingObjectWithID:userID error:NULL]; - if (user_) { - [context deleteObject:user_]; - [context saveToStore]; - } - }]; - return; - } - - if (buttonIndex == [sheet firstOtherButtonIndex]) - // Reset Password - [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { - MPUserEntity *user_ = (MPUserEntity *)[context existingObjectWithID:userID error:NULL]; - if (user_) - [[MPiOSAppDelegate get] changeMasterPasswordFor:user_ saveInContext:context didResetBlock:^{ - dbg(@"changing mp for user: %@, keyID: %@", user_.name, user_.keyID); - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - NSIndexPath *avatarIndexPath = [self.avatarCollectionView indexPathForCell:avatarCell]; - [self.avatarCollectionView selectItemAtIndexPath:avatarIndexPath animated:NO - scrollPosition:UICollectionViewScrollPositionNone]; - [self collectionView:self.avatarCollectionView didSelectItemAtIndexPath:avatarIndexPath]; - }]; - }]; - }]; - } cancelTitle:[PearlStrings get].commonButtonCancel - destructiveTitle:@"Delete User" otherTitles:@"Reset Password", nil]; - } -} - -#pragma mark - UIScrollViewDelegate - -- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity - targetContentOffset:(inout CGPoint *)targetContentOffset { - - if (scrollView == self.avatarCollectionView) { - CGPoint offsetToCenter = CGPointMake( - self.avatarCollectionView.bounds.size.width / 2, - self.avatarCollectionView.bounds.size.height / 2 ); - NSIndexPath *avatarIndexPath = [self.avatarCollectionView indexPathForItemAtPoint: - CGPointPlusCGPoint( *targetContentOffset, offsetToCenter )]; - CGPoint targetCenter = [self.avatarCollectionView layoutAttributesForItemAtIndexPath:avatarIndexPath].center; - *targetContentOffset = CGPointMinusCGPoint( targetCenter, offsetToCenter ); - NSAssert([self.avatarCollectionView indexPathForItemAtPoint:targetCenter].item == avatarIndexPath.item, @"should be same item"); - } -} - -- (MPAvatarCell *)selectedAvatar { - - NSArray *selectedIndexPaths = self.avatarCollectionView.indexPathsForSelectedItems; - if (![selectedIndexPaths count]) { - // No selected user. - return nil; - } - - return (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:selectedIndexPaths.firstObject]; -} - -- (MPUserEntity *)selectedUserInContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew { - - MPAvatarCell *selectedAvatar = [self selectedAvatar]; - if (!selectedAvatar) { - // No selected user. - *isNew = NO; - return nil; - } - - return [self userForAvatar:selectedAvatar inContext:context isNew:isNew]; -} - -- (MPUserEntity *)userForAvatar:(MPAvatarCell *)cell inContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew { - - return [self userForIndexPath:[self.avatarCollectionView indexPathForCell:cell] inContext:context isNew:isNew]; -} - -- (MPUserEntity *)userForIndexPath:(NSIndexPath *)indexPath inContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew { - - if ((*isNew = indexPath.item >= [self.userIDs count])) - return nil; - - NSError *error = nil; - MPUserEntity *user = (MPUserEntity *)[context existingObjectWithID:self.userIDs[indexPath.item] error:&error]; - if (error) - wrn(@"Failed to load user into context: %@", error); - - return user; -} - -- (void)updateAvatars { - - for (NSIndexPath *indexPath in self.avatarCollectionView.indexPathsForVisibleItems) - [self updateAvatarAtIndexPath:indexPath]; -} - -- (void)updateAvatarAtIndexPath:(NSIndexPath *)indexPath { - - MPAvatarCell *cell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath]; - [self updateAvatar:cell atIndexPath:indexPath]; -} - -- (void)updateAvatar:(MPAvatarCell *)cell atIndexPath:(NSIndexPath *)indexPath { - - CGFloat current = [self.avatarCollectionView layoutAttributesForItemAtIndexPath:indexPath].center.x - - self.avatarCollectionView.contentOffset.x; - CGFloat max = self.avatarCollectionView.bounds.size.width; - cell.visibility = MAX(0, MIN( 1, 1 - ABS( current / (max / 2) - 1 ) )); -} +#pragma mark - Private - (void)registerObservers { @@ -426,46 +122,19 @@ typedef NS_ENUM(NSUInteger, MPActiveUserState) { Weakify(self); _notificationObservers = @[ - [[NSNotificationCenter defaultCenter] - addObserverForName:UIApplicationWillResignActiveNotification object:nil - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - Strongify(self); - -// [self emergencyCloseAnimated:NO]; - self.userSelectionContainer.alpha = 0; - self.passwordSelectionContainer.alpha = 0; - }], - [[NSNotificationCenter defaultCenter] - addObserverForName:UIApplicationDidBecomeActiveNotification object:nil - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - Strongify(self); - - [self updateMode]; - [UIView animateWithDuration:1 animations:^{ - self.userSelectionContainer.alpha = 1; - self.passwordSelectionContainer.alpha = 1; - }]; - }], [[NSNotificationCenter defaultCenter] addObserverForName:MPSignedInNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { Strongify(self); - self.mode = MPCombinedModePasswordSelection; + [self setMode:MPCombinedModePasswordSelection]; }], [[NSNotificationCenter defaultCenter] addObserverForName:MPSignedOutNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { Strongify(self); - self.mode = MPCombinedModeUserSelection; - }], - [[NSNotificationCenter defaultCenter] - addObserverForName:UITextFieldTextDidChangeNotification object:self.entryField - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - Strongify(self); - - [self textFieldDidChange:note.object]; + [self setMode:MPCombinedModeUserSelection animated:[note.userInfo[@"animated"] boolValue]]; }], ]; } @@ -477,181 +146,8 @@ typedef NS_ENUM(NSUInteger, MPActiveUserState) { _notificationObservers = nil; } -- (void)needStoreObserved:(BOOL)observeStore { - if (observeStore) { - Weakify(self); - - NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady]; - if (!_mocObserver && mainContext) - _mocObserver = [[NSNotificationCenter defaultCenter] - addObserverForName:NSManagedObjectContextObjectsDidChangeNotification object:mainContext - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - Strongify(self); - [self updateMode]; - }]; - if (!_storeObserver) - _storeObserver = [[NSNotificationCenter defaultCenter] - addObserverForName:USMStoreDidChangeNotification object:nil - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - Strongify(self); - [self updateMode]; - }]; - } - if (!observeStore) { - if (_mocObserver) - [[NSNotificationCenter defaultCenter] removeObserver:_mocObserver]; - if (_storeObserver) - [[NSNotificationCenter defaultCenter] removeObserver:_storeObserver]; - } -} - -- (void)setUserIDs:(NSArray *)userIDs { - - _userIDs = userIDs; - - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - [self.avatarCollectionView reloadData]; - }]; -} - -- (void)setMode:(MPCombinedMode)mode { - - _mode = mode; - - [self updateMode]; -} - -- (void)updateMode { - - // Ensure we're on the main thread. - if (![NSThread isMainThread]) { - [[NSOperationQueue mainQueue] addOperationWithBlock:^{ - [self updateMode]; - }]; - return; - } - - self.userSelectionContainer.hidden = YES; - self.passwordSelectionContainer.hidden = YES; - - [self becomeFirstResponder]; - - switch (self.mode) { - case MPCombinedModeUserSelection: { - [[self navigationController] setNavigationBarHidden:YES animated:YES]; - self.userSelectionContainer.hidden = NO; - [self needStoreObserved:YES]; - - [self setActiveUserState:MPActiveUserStateNone animated:NO]; - [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { - NSError *error = nil; - NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )]; - fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO] ]; - NSArray *users = [context executeFetchRequest:fetchRequest error:&error]; - if (!users) { - err(@"Failed to load users: %@", error); - self.userIDs = nil; - } - - NSMutableArray *userIDs = [NSMutableArray arrayWithCapacity:[users count]]; - for (MPUserEntity *user in users) - [userIDs addObject:user.objectID]; - self.userIDs = userIDs; - }]; - - break; - } - case MPCombinedModePasswordSelection: { - [[self navigationController] setNavigationBarHidden:NO animated:YES]; - self.passwordSelectionContainer.hidden = NO; - [self needStoreObserved:NO]; - break; - } - } -} - -- (void)setActiveUserState:(MPActiveUserState)activeUserState { - - [self setActiveUserState:activeUserState animated:YES]; -} - -- (void)setActiveUserState:(MPActiveUserState)activeUserState animated:(BOOL)animated { - - _activeUserState = activeUserState; - _masterPasswordChoice = nil; - - [UIView animateWithDuration:animated? 0.3f: 0 animations:^{ - // Set the entry container's contents. - switch (activeUserState) { - case MPActiveUserStateNone: { - for (NSUInteger item = 0; item < [self.avatarCollectionView numberOfItemsInSection:0]; ++item) { - NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:0]; - [self.avatarCollectionView deselectItemAtIndexPath:indexPath animated:YES]; - - MPAvatarCell *avatarCell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath]; - avatarCell.mode = MPAvatarModeLowered; - } - break; - } - case MPActiveUserStateLogin: { - self.entryField.text = strl( @"Enter your master password:" ); - self.entryField.text = nil; - self.entryField.secureTextEntry = YES; - break; - } - case MPActiveUserStateUserName: { - self.entryLabel.text = strl( @"Enter your full name:" ); - self.entryField.text = nil; - self.entryField.secureTextEntry = NO; - break; - } - case MPActiveUserStateMasterPasswordChoice: { - self.entryLabel.text = strl( @"Choose your master password:" ); - self.entryField.text = nil; - self.entryField.secureTextEntry = YES; - break; - } - case MPActiveUserStateMasterPasswordConfirmation: { - _masterPasswordChoice = self.entryField.text; - self.entryLabel.text = strl( @"Confirm your master password:" ); - self.entryField.text = nil; - self.entryField.secureTextEntry = YES; - break; - } - } - - // Manage the random avatar for the new user if selected. - MPAvatarCell *selectedAvatar = [self selectedAvatar]; - if (selectedAvatar.avatar == MPAvatarAdd) { - selectedAvatar.avatar = arc4random() % MPAvatarCount; - } - else { - NSIndexPath *newUserIndexPath = [NSIndexPath indexPathForItem:[_userIDs count] inSection:0]; - MPAvatarCell *newUserAvatar = (MPAvatarCell *)[[self avatarCollectionView] cellForItemAtIndexPath:newUserIndexPath]; - newUserAvatar.avatar = MPAvatarAdd; - newUserAvatar.name = strl( @"New User" ); - } - - // Manage the entry container depending on whether a user is activate or not. - if (activeUserState == MPActiveUserStateNone) { - self.avatarCollectionCenterConstraint.priority = UILayoutPriorityDefaultHigh; - self.entryContainer.alpha = 0; - } - else { - self.avatarCollectionCenterConstraint.priority = UILayoutPriorityDefaultLow; - self.entryContainer.alpha = 1; - } - [self.avatarCollectionCenterConstraint apply]; - - // Toggle the keyboard. - if (activeUserState == MPActiveUserStateNone) - [self.entryField resignFirstResponder]; - } completion:^(BOOL finished) { - if (activeUserState != MPActiveUserStateNone) - [self.entryField becomeFirstResponder]; - }]; -} +#pragma mark - Actions - (IBAction)doSignOut:(UIBarButtonItem *)sender { diff --git a/MasterPassword/ObjC/iOS/MPPasswordsViewController.h b/MasterPassword/ObjC/iOS/MPPasswordsViewController.h new file mode 100644 index 00000000..05fd41b0 --- /dev/null +++ b/MasterPassword/ObjC/iOS/MPPasswordsViewController.h @@ -0,0 +1,33 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// MPCombinedViewController.h +// MPCombinedViewController +// +// Created by lhunath on 2014-03-08. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import "LLGitTip.h" + +@interface MPPasswordsViewController : UIViewController + +@property(strong, nonatomic) IBOutlet UIView *passwordSelectionContainer; +@property(strong, nonatomic) IBOutlet UICollectionView *passwordCollectionView; +@property (strong, nonatomic) IBOutlet NSLayoutConstraint *passwordsToBottomConstraint; +@property (strong, nonatomic) IBOutlet NSLayoutConstraint *navigationBarToTopConstraint; +@property (strong, nonatomic) IBOutlet NSLayoutConstraint *navigationBarToPasswordsConstraint; + +@property(assign, nonatomic) BOOL active; + +- (void)setActive:(BOOL)active animated:(BOOL)animated; + +@end diff --git a/MasterPassword/ObjC/iOS/MPPasswordsViewController.m b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m new file mode 100644 index 00000000..ffe16a47 --- /dev/null +++ b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m @@ -0,0 +1,218 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// MPPasswordsViewController.h +// MPPasswordsViewController +// +// Created by lhunath on 2014-03-08. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import "MPPasswordsViewController.h" +#import "MPiOSAppDelegate.h" +#import "MPAppDelegate_Store.h" + +@interface MPPasswordsViewController() +@property (strong, nonatomic) IBOutlet UINavigationBar *navigationBar; + +@end + +@implementation MPPasswordsViewController { + __weak id _storeObserver; + __weak id _mocObserver; + NSArray *_notificationObservers; +} + +- (void)viewDidLoad { + + [super viewDidLoad]; + + self.view.backgroundColor = [UIColor clearColor]; +} + +- (void)viewWillAppear:(BOOL)animated { + + [super viewWillAppear:animated]; + + [self registerObservers]; + [self observeStore]; +} + +- (void)viewWillDisappear:(BOOL)animated { + + [super viewWillDisappear:animated]; + + [self removeObservers]; + [self stopObservingStore]; +} + +#pragma mark - UITableViewDataSource + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + + if (tableView == self.searchDisplayController.searchResultsTableView) + return 0; + + NSAssert(NO, @"Unexpected table view: %@", tableView); + return 0; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + + if (tableView == self.searchDisplayController.searchResultsTableView) { + + } + + NSAssert(NO, @"Unexpected table view: %@", tableView); + return nil; +} + + +#pragma mark - UITextFieldDelegate + +- (void)textFieldDidEndEditing:(UITextField *)textField { +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + + return NO; +} + +// This isn't really in UITextFieldDelegate. We fake it from UITextFieldTextDidChangeNotification. +- (void)textFieldDidChange:(UITextField *)textField { + +} + +#pragma mark - UICollectionViewDataSource + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + + if (collectionView == self.passwordCollectionView) + return 0; + + Throw(@"unexpected collection view: %@", collectionView); +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + + if (collectionView == self.passwordCollectionView) + return nil; + + Throw(@"unexpected collection view: %@", collectionView); +} + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + +} + +- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath { + +} + +#pragma mark - UILongPressGestureRecognizer + +- (void)didLongPress:(UILongPressGestureRecognizer *)recognizer { + +} + +#pragma mark - UIScrollViewDelegate + + +#pragma mark - Private + +- (void)registerObservers { + + if ([_notificationObservers count]) + return; + + Weakify(self); + _notificationObservers = @[ + [[NSNotificationCenter defaultCenter] + addObserverForName:UIApplicationWillResignActiveNotification object:nil + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + Strongify(self); + + self.passwordSelectionContainer.alpha = 0; + }], + [[NSNotificationCenter defaultCenter] + addObserverForName:UIApplicationDidBecomeActiveNotification object:nil + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + Strongify(self); + +// [self updateMode]; TODO: reload passwords list + [UIView animateWithDuration:1 animations:^{ + self.passwordSelectionContainer.alpha = 1; + }]; + }], + ]; +} + +- (void)removeObservers { + + for (id observer in _notificationObservers) + [[NSNotificationCenter defaultCenter] removeObserver:observer]; + _notificationObservers = nil; +} + +- (void)observeStore { + +// Weakify(self); + + NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady]; + if (!_mocObserver && mainContext) + _mocObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:NSManagedObjectContextObjectsDidChangeNotification object:mainContext + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { +// Strongify(self); +// [self updateMode]; TODO: reload passwords list + }]; + if (!_storeObserver) + _storeObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:USMStoreDidChangeNotification object:nil + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { +// Strongify(self); +// [self updateMode]; TODO: reload passwords list + }]; +} + +- (void)stopObservingStore { + + if (_mocObserver) + [[NSNotificationCenter defaultCenter] removeObserver:_mocObserver]; + if (_storeObserver) + [[NSNotificationCenter defaultCenter] removeObserver:_storeObserver]; +} + +#pragma mark - Properties + +- (void)setActive:(BOOL)active { + + [self setActive:active animated:YES]; +} + +- (void)setActive:(BOOL)active animated:(BOOL)animated { + + _active = active; + + [UIView animateWithDuration:animated? 0.3f: 0 animations:^{ + self.navigationBarToPasswordsConstraint.priority = active? UILayoutPriorityDefaultHigh: 1; + self.navigationBarToTopConstraint.priority = active? 1: UILayoutPriorityDefaultHigh; + self.passwordsToBottomConstraint.priority = active? 1: UILayoutPriorityDefaultHigh; + + [self.navigationBarToPasswordsConstraint apply]; + [self.navigationBarToTopConstraint apply]; + [self.passwordsToBottomConstraint apply]; + }]; +} + +#pragma mark - Actions + +@end diff --git a/MasterPassword/ObjC/iOS/MPPreferencesViewController.m b/MasterPassword/ObjC/iOS/MPPreferencesViewController.m index 39308bca..2b52922c 100644 --- a/MasterPassword/ObjC/iOS/MPPreferencesViewController.m +++ b/MasterPassword/ObjC/iOS/MPPreferencesViewController.m @@ -25,12 +25,12 @@ for (NSUInteger a = 0; a < MPAvatarCount; ++a) { UIButton *avatar = [self.avatarTemplate clone]; - avatar.tag = (NSInteger)a; + avatar.tag = a; avatar.hidden = NO; avatar.center = CGPointMake( self.avatarTemplate.center.x * (a + 1) + self.avatarTemplate.bounds.size.width / 2 * a, self.avatarTemplate.center.y ); - [avatar setBackgroundImage:[UIImage imageNamed:PearlString( @"avatar-%d", a )] + [avatar setBackgroundImage:[UIImage imageNamed:PearlString( @"avatar-%ld", (long)a )] forState:UIControlStateNormal]; [avatar setSelectionInSuperviewCandidate:YES isClearable:NO]; diff --git a/MasterPassword/ObjC/iOS/MPUnlockViewController.m b/MasterPassword/ObjC/iOS/MPUnlockViewController.m index 733f2269..b6afd4d9 100644 --- a/MasterPassword/ObjC/iOS/MPUnlockViewController.m +++ b/MasterPassword/ObjC/iOS/MPUnlockViewController.m @@ -54,7 +54,7 @@ avatar.center = CGPointMake( (20 + self.avatarTemplate.bounds.size.width / 2) * (a + 1) + self.avatarTemplate.bounds.size.width / 2 * a, 20 + self.avatarTemplate.bounds.size.height / 2 ); - [avatar setBackgroundImage:[UIImage imageNamed:PearlString( @"avatar-%d", a )] forState:UIControlStateNormal]; + [avatar setBackgroundImage:[UIImage imageNamed:PearlString( @"avatar-%ld", (long)a )] forState:UIControlStateNormal]; [avatar setSelectionInSuperviewCandidate:YES isClearable:NO]; avatar.layer.cornerRadius = avatar.bounds.size.height / 2; @@ -276,12 +276,6 @@ [super viewWillDisappear:animated]; } -- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { - - if ([segue.identifier isEqualToString:@"MP_Settings"]) - [self.navigationController setNavigationBarHidden:NO animated:YES]; -} - - (BOOL)prefersStatusBarHidden { return YES; @@ -292,6 +286,12 @@ return UIStatusBarAnimationSlide; } +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + + if ([segue.identifier isEqualToString:@"MP_Settings"]) + [self.navigationController setNavigationBarHidden:NO animated:YES]; +} + - (BOOL)canBecomeFirstResponder { return YES; diff --git a/MasterPassword/ObjC/iOS/MPUsersViewController.h b/MasterPassword/ObjC/iOS/MPUsersViewController.h new file mode 100644 index 00000000..651e3ef4 --- /dev/null +++ b/MasterPassword/ObjC/iOS/MPUsersViewController.h @@ -0,0 +1,40 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// MPCombinedViewController.h +// MPCombinedViewController +// +// Created by lhunath on 2014-03-08. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import "LLGitTip.h" + +@interface MPUsersViewController : UIViewController + +@property (strong, nonatomic) IBOutlet UINavigationBar *navigationBar; +@property(weak, nonatomic) IBOutlet UIView *userSelectionContainer; +@property(weak, nonatomic) IBOutlet UILabel *hintLabel; +@property(weak, nonatomic) IBOutlet UIView *gitTipTip; +@property(weak, nonatomic) IBOutlet LLGitTip *gitTipButton; +@property(weak, nonatomic) IBOutlet UITextField *entryField; +@property(weak, nonatomic) IBOutlet UILabel *entryLabel; +@property(weak, nonatomic) IBOutlet UIView *entryContainer; +@property(weak, nonatomic) IBOutlet UIView *footerContainer; +@property(weak, nonatomic) IBOutlet UICollectionView *avatarCollectionView; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *avatarCollectionCenterConstraint; +@property (strong, nonatomic) IBOutlet NSLayoutConstraint *navigationBarToTopConstraint; + +@property(assign, nonatomic) BOOL active; + +- (void)setActive:(BOOL)active animated:(BOOL)animated; + +@end diff --git a/MasterPassword/ObjC/iOS/MPUsersViewController.m b/MasterPassword/ObjC/iOS/MPUsersViewController.m new file mode 100644 index 00000000..203393f1 --- /dev/null +++ b/MasterPassword/ObjC/iOS/MPUsersViewController.m @@ -0,0 +1,742 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// MPCombinedViewController.h +// MPCombinedViewController +// +// Created by lhunath on 2014-03-08. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import "MPUsersViewController.h" +#import "MPEntities.h" +#import "MPAvatarCell.h" +#import "MPiOSAppDelegate.h" +#import "MPAppDelegate_Store.h" +#import "MPAppDelegate_Key.h" + +typedef NS_ENUM(NSUInteger, MPActiveUserState) { + /** The users are all inactive */ + MPActiveUserStateNone, + /** The selected user is activated and being logged in with */ + MPActiveUserStateLogin, + /** The selected user is activated and its user name is being asked for */ + MPActiveUserStateUserName, + /** The selected user is activated and its new master password is being asked for */ + MPActiveUserStateMasterPasswordChoice, + /** The selected user is activated and the confirmation of the previously entered master password is being asked for */ + MPActiveUserStateMasterPasswordConfirmation, + /** The selected user is activated displayed at the top with the rest of the UI inactive */ + MPActiveUserStateMinimized, +}; + +@interface MPUsersViewController() + +@property(nonatomic) MPActiveUserState activeUserState; +@property(nonatomic, strong) NSArray *userIDs; +@property(nonatomic, strong) NSTimer *marqueeTipTimer; +@property(nonatomic, strong) NSArray *marqueeTipTexts; +@property(nonatomic) NSUInteger marqueeTipTextIndex; +@end + +@implementation MPUsersViewController { + __weak id _storeObserver; + __weak id _mocObserver; + NSArray *_notificationObservers; + NSString *_masterPasswordChoice; +} + +- (void)viewDidLoad { + + [super viewDidLoad]; + + self.marqueeTipTexts = @[ + strl(@"Press and hold to change password or delete."), + strl(@"Shake for emergency generator."), + ]; + + self.view.backgroundColor = [UIColor clearColor]; + self.avatarCollectionView.allowsMultipleSelection = YES; + [self.entryField addTarget:self action:@selector(textFieldEditingChanged:) forControlEvents:UIControlEventEditingChanged]; + + [self setActive:YES animated:NO]; +} + +- (void)viewWillAppear:(BOOL)animated { + + [super viewWillAppear:animated]; + + self.userSelectionContainer.alpha = 0; + + [self observeStore]; + [self registerObservers]; + [self reloadUsers]; + + [self.marqueeTipTimer invalidate]; + self.marqueeTipTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(firedMarqueeTimer:) userInfo:nil repeats:YES]; +} + +- (void)viewWillDisappear:(BOOL)animated { + + [super viewWillDisappear:animated]; + + [self removeObservers]; + [self stopObservingStore]; + + [self.marqueeTipTimer invalidate]; +} + +- (BOOL)canBecomeFirstResponder { + + return YES; +} + +- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event { + +// if (motion == UIEventSubtypeMotionShake) +// [self emergencyOpenAnimated:YES]; +} + +#pragma mark - UITextFieldDelegate + +- (void)textFieldDidEndEditing:(UITextField *)textField { +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + + if (textField == self.entryField) { + switch (self.activeUserState) { + case MPActiveUserStateNone: { + [textField resignFirstResponder]; + break; + } + case MPActiveUserStateLogin: { + [self selectedAvatar].spinnerActive = YES; + [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + BOOL signedIn = NO, isNew = NO; + MPUserEntity *user = [self selectedUserInContext:context isNew:&isNew]; + if (!isNew && user) + signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context + usingMasterPassword:self.entryField.text]; + + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + [self selectedAvatar].spinnerActive = NO; + + if (!signedIn) { + // Sign in failed. + // TODO: warn user + return; + } + }]; + }]; + break; + } + case MPActiveUserStateUserName: { + NSString *userName = self.entryField.text; + if (![userName length]) { + // No name entered. + // TODO: warn user + return NO; + } + + [self selectedAvatar].name = userName; + self.activeUserState = MPActiveUserStateMasterPasswordChoice; + break; + } + case MPActiveUserStateMasterPasswordChoice: { + NSString *masterPassword = self.entryField.text; + if (![masterPassword length]) { + // No password entered. + // TODO: warn user + return NO; + } + + self.activeUserState = MPActiveUserStateMasterPasswordConfirmation; + break; + } + case MPActiveUserStateMasterPasswordConfirmation: { + NSString *masterPassword = self.entryField.text; + if (![masterPassword length]) { + // No password entered. + // TODO: warn user + return NO; + } + + if (![masterPassword isEqualToString:_masterPasswordChoice]) { + // Master password confirmation failed. + // TODO: warn user + self.activeUserState = MPActiveUserStateMasterPasswordChoice; + return NO; + } + + [self selectedAvatar].spinnerActive = YES; + [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + BOOL isNew = NO; + MPUserEntity *user = [self selectedUserInContext:context isNew:&isNew]; + if (isNew) { + user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass( [MPUserEntity class] ) + inManagedObjectContext:context]; + MPAvatarCell *avatarCell = [self selectedAvatar]; + user.avatar = avatarCell.avatar; + user.name = avatarCell.name; + } + + BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context usingMasterPassword:masterPassword]; + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + [self selectedAvatar].spinnerActive = NO; + + if (!signedIn) { + // Sign in failed, shouldn't happen for a new user. + // TODO: warn user + self.activeUserState = MPActiveUserStateNone; + return; + } + }]; + }]; + + break; + } + case MPActiveUserStateMinimized: { + [textField resignFirstResponder]; + break; + } + } + } + + return NO; +} + +// This isn't really in UITextFieldDelegate. We fake it from UITextFieldTextDidChangeNotification. +- (void)textFieldEditingChanged:(UITextField *)textField { + + if (textField == self.entryField) { + switch (self.activeUserState) { + case MPActiveUserStateNone: + break; + case MPActiveUserStateLogin: + break; + case MPActiveUserStateUserName: { + NSString *userName = self.entryField.text; + [self selectedAvatar].name = [userName length]? userName: strl( @"New User" ); + break; + } + case MPActiveUserStateMasterPasswordChoice: + break; + case MPActiveUserStateMasterPasswordConfirmation: + break; + case MPActiveUserStateMinimized: + break; + } + } +} + +#pragma mark - UICollectionViewDataSource + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + + if (collectionView == self.avatarCollectionView) + return [self.userIDs count] + 1; + + Throw(@"unexpected collection view: %@", collectionView); +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + + if (collectionView == self.avatarCollectionView) { + MPAvatarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MPAvatarCell reuseIdentifier] forIndexPath:indexPath]; + [cell addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(didLongPress:)]]; + [self updateVisibilityForAvatar:cell atIndexPath:indexPath animated:NO]; + [self updateModeForAvatar:cell atIndexPath:indexPath animated:NO]; + + BOOL isNew = NO; + MPUserEntity *user = [self userForIndexPath:indexPath inContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady] + isNew:&isNew]; + if (isNew) { + // New User + cell.avatar = MPAvatarAdd; + cell.name = strl( @"New User" ); + } + else { + // Existing User + cell.avatar = user.avatar; + cell.name = user.name; + } + + return cell; + } + + Throw(@"unexpected collection view: %@", collectionView); +} + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + + if (collectionView == self.avatarCollectionView) { + [self.avatarCollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally + animated:YES]; + + // Deselect all other cells. + for (NSUInteger otherItem = 0; otherItem < [collectionView numberOfItemsInSection:indexPath.section]; ++otherItem) + if (otherItem != indexPath.item) { + NSIndexPath *otherIndexPath = [NSIndexPath indexPathForItem:otherItem inSection:indexPath.section]; + [collectionView deselectItemAtIndexPath:otherIndexPath animated:YES]; + } + + BOOL isNew = NO; + MPUserEntity *user = [self userForIndexPath:indexPath inContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady] + isNew:&isNew]; + if (isNew) + self.activeUserState = MPActiveUserStateUserName; + else if (!user.keyID) + self.activeUserState = MPActiveUserStateMasterPasswordChoice; + else + self.activeUserState = MPActiveUserStateLogin; + } +} + +- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath { + + if (collectionView == self.avatarCollectionView) { + self.activeUserState = MPActiveUserStateNone; + } +} + +#pragma mark - UILongPressGestureRecognizer + +- (void)didLongPress:(UILongPressGestureRecognizer *)recognizer { + + if ([recognizer.view isKindOfClass:[MPAvatarCell class]]) { + if (recognizer.state != UIGestureRecognizerStateBegan) + // Don't show the action menu unless the state is Began. + return; + + MPAvatarCell *avatarCell = (MPAvatarCell *)recognizer.view; + NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady]; + + BOOL isNew = NO; + MPUserEntity *user = [self userForAvatar:avatarCell inContext:mainContext isNew:&isNew]; + NSManagedObjectID *userID = user.objectID; + if (isNew || !user) + return; + + [PearlSheet showSheetWithTitle:user.name + viewStyle:UIActionSheetStyleBlackTranslucent + initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) { + if (buttonIndex == [sheet cancelButtonIndex]) + return; + + if (buttonIndex == [sheet destructiveButtonIndex]) { + // Delete User + [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + NSManagedObject *user_ = [context existingObjectWithID:userID error:NULL]; + if (user_) { + [context deleteObject:user_]; + [context saveToStore]; + } + }]; + return; + } + + if (buttonIndex == [sheet firstOtherButtonIndex]) + // Reset Password + [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + MPUserEntity *user_ = (MPUserEntity *)[context existingObjectWithID:userID error:NULL]; + if (user_) + [[MPiOSAppDelegate get] changeMasterPasswordFor:user_ saveInContext:context didResetBlock:^{ + dbg(@"changing mp for user: %@, keyID: %@", user_.name, user_.keyID); + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + NSIndexPath *avatarIndexPath = [self.avatarCollectionView indexPathForCell:avatarCell]; + [self.avatarCollectionView selectItemAtIndexPath:avatarIndexPath animated:NO + scrollPosition:UICollectionViewScrollPositionNone]; + [self collectionView:self.avatarCollectionView didSelectItemAtIndexPath:avatarIndexPath]; + }]; + }]; + }]; + } cancelTitle:[PearlStrings get].commonButtonCancel + destructiveTitle:@"Delete User" otherTitles:@"Reset Password", nil]; + } +} + +#pragma mark - UIScrollViewDelegate + +- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity + targetContentOffset:(inout CGPoint *)targetContentOffset { + + if (scrollView == self.avatarCollectionView) { + CGPoint offsetToCenter = CGPointMake( + self.avatarCollectionView.bounds.size.width / 2, + self.avatarCollectionView.bounds.size.height / 2 ); + NSIndexPath *avatarIndexPath = [self.avatarCollectionView indexPathForItemAtPoint: + CGPointPlusCGPoint( *targetContentOffset, offsetToCenter )]; + CGPoint targetCenter = [self.avatarCollectionView layoutAttributesForItemAtIndexPath:avatarIndexPath].center; + *targetContentOffset = CGPointMinusCGPoint( targetCenter, offsetToCenter ); + NSAssert([self.avatarCollectionView indexPathForItemAtPoint:targetCenter].item == avatarIndexPath.item, @"should be same item"); + } +} + +#pragma mark - Private + +- (void)firedMarqueeTimer:(NSTimer *)timer { + + [UIView animateWithDuration:0.5 animations:^{ + self.hintLabel.alpha = 0; + } completion:^(BOOL finished) { + if (!finished) + return; + + self.hintLabel.text = self.marqueeTipTexts[++self.marqueeTipTextIndex % [self.marqueeTipTexts count]]; + [UIView animateWithDuration:0.5 animations:^{ + self.hintLabel.alpha = 1; + }]; + }]; +} + +- (MPAvatarCell *)selectedAvatar { + + NSArray *selectedIndexPaths = self.avatarCollectionView.indexPathsForSelectedItems; + if (![selectedIndexPaths count]) { + // No selected user. + return nil; + } + + return (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:selectedIndexPaths.firstObject]; +} + +- (MPUserEntity *)selectedUserInContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew { + + MPAvatarCell *selectedAvatar = [self selectedAvatar]; + if (!selectedAvatar) { + // No selected user. + *isNew = NO; + return nil; + } + + return [self userForAvatar:selectedAvatar inContext:context isNew:isNew]; +} + +- (MPUserEntity *)userForAvatar:(MPAvatarCell *)cell inContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew { + + return [self userForIndexPath:[self.avatarCollectionView indexPathForCell:cell] inContext:context isNew:isNew]; +} + +- (MPUserEntity *)userForIndexPath:(NSIndexPath *)indexPath inContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew { + + if ((*isNew = indexPath.item >= [self.userIDs count])) + return nil; + + NSError *error = nil; + MPUserEntity *user = (MPUserEntity *)[context existingObjectWithID:self.userIDs[indexPath.item] error:&error]; + if (error) + wrn(@"Failed to load user into context: %@", error); + + return user; +} + +- (void)updateAvatars { + + for (NSIndexPath *indexPath in self.avatarCollectionView.indexPathsForVisibleItems) + [self updateAvatarAtIndexPath:indexPath]; +} + +- (void)updateAvatarAtIndexPath:(NSIndexPath *)indexPath { + + MPAvatarCell *cell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath]; + [self updateVisibilityForAvatar:cell atIndexPath:indexPath animated:NO]; + [self updateModeForAvatar:cell atIndexPath:indexPath animated:NO]; +} + +- (void)updateVisibilityForAvatar:(MPAvatarCell *)cell atIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated { + + CGFloat current = [self.avatarCollectionView layoutAttributesForItemAtIndexPath:indexPath].center.x - + self.avatarCollectionView.contentOffset.x; + CGFloat max = self.avatarCollectionView.bounds.size.width; + + [cell setVisibility:MAX(0, MIN( 1, 1 - ABS( current / (max / 2) - 1 ) )) animated:animated]; +} + +- (void)updateModeForAvatar:(MPAvatarCell *)avatarCell atIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated { + + switch (self.activeUserState) { + case MPActiveUserStateNone: { + [self.avatarCollectionView deselectItemAtIndexPath:indexPath animated:YES]; + [avatarCell setMode:MPAvatarModeLowered animated:animated]; + break; + } + case MPActiveUserStateLogin: + case MPActiveUserStateUserName: + case MPActiveUserStateMasterPasswordChoice: + case MPActiveUserStateMasterPasswordConfirmation: { + if ([self.avatarCollectionView.indexPathsForSelectedItems containsObject:indexPath]) + [avatarCell setMode:MPAvatarModeRaisedAndActive animated:animated]; + else + [avatarCell setMode:MPAvatarModeRaisedButInactive animated:animated]; + break; + } + case MPActiveUserStateMinimized: { + if ([self.avatarCollectionView.indexPathsForSelectedItems containsObject:indexPath]) + [avatarCell setMode:MPAvatarModeRaisedAndMinimized animated:animated]; + else + [avatarCell setMode:MPAvatarModeRaisedAndHidden animated:animated]; + break; + } + } +} + +- (void)registerObservers { + + if ([_notificationObservers count]) + return; + + Weakify(self); + _notificationObservers = @[ + [[NSNotificationCenter defaultCenter] + addObserverForName:UIApplicationWillResignActiveNotification object:nil + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + Strongify(self); + +// [self emergencyCloseAnimated:NO]; + self.userSelectionContainer.alpha = 0; + }], + [[NSNotificationCenter defaultCenter] + addObserverForName:UIApplicationDidBecomeActiveNotification object:nil + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + Strongify(self); + + [self reloadUsers]; + + [UIView animateWithDuration:1 animations:^{ + self.userSelectionContainer.alpha = 1; + }]; + }], + ]; + + [self observeKeyPath:@"avatarCollectionView.contentOffset" withBlock: + ^(id from, id to, NSKeyValueChange cause, MPUsersViewController *_self) { + [_self updateAvatars]; + }]; +} + +- (void)removeObservers { + + for (id observer in _notificationObservers) + [[NSNotificationCenter defaultCenter] removeObserver:observer]; + _notificationObservers = nil; + + [self removeKeyPathObservers]; +} + +- (void)observeStore { + + Weakify(self); + + NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady]; + if (!_mocObserver && mainContext) + _mocObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:NSManagedObjectContextObjectsDidChangeNotification object:mainContext + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + Strongify(self); + [self reloadUsers]; + }]; + if (!_storeObserver) + _storeObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:USMStoreDidChangeNotification object:nil + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + Strongify(self); + [self reloadUsers]; + }]; +} + +- (void)stopObservingStore { + + if (_mocObserver) + [[NSNotificationCenter defaultCenter] removeObserver:_mocObserver]; + if (_storeObserver) + [[NSNotificationCenter defaultCenter] removeObserver:_storeObserver]; +} + +- (void)reloadUsers { + + [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + NSError *error = nil; + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )]; + fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO] ]; + NSArray *users = [context executeFetchRequest:fetchRequest error:&error]; + if (!users) { + err(@"Failed to load users: %@", error); + self.userIDs = nil; + } + + NSMutableArray *userIDs = [NSMutableArray arrayWithCapacity:[users count]]; + for (MPUserEntity *user in users) + [userIDs addObject:user.objectID]; + self.userIDs = userIDs; + }]; +} + +#pragma mark - Properties + +- (void)setActive:(BOOL)active { + + [self setActive:active animated:YES]; +} + +- (void)setActive:(BOOL)active animated:(BOOL)animated { + + _active = active; + dbg(@"active -> %d", active); + + if (active) + [self setActiveUserState:MPActiveUserStateNone animated:animated]; + else + [self setActiveUserState:MPActiveUserStateMinimized animated:animated]; +} + +- (void)setUserIDs:(NSArray *)userIDs { + + _userIDs = userIDs; + dbg(@"userIDs -> %lu", (unsigned long)[userIDs count]); + + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + [self.avatarCollectionView reloadData]; + + [UIView animateWithDuration:0.3f animations:^{ + self.userSelectionContainer.alpha = 1; + }]; + }]; +} + +- (void)setActiveUserState:(MPActiveUserState)activeUserState { + + [self setActiveUserState:activeUserState animated:YES]; +} + +- (void)setActiveUserState:(MPActiveUserState)activeUserState animated:(BOOL)animated { + + _activeUserState = activeUserState; + _masterPasswordChoice = nil; + + if (activeUserState != MPActiveUserStateMinimized && [MPiOSAppDelegate get].key) { + [[MPiOSAppDelegate get] signOutAnimated:YES]; + return; + } + + [UIView animateWithDuration:animated? 0.3f: 0 animations:^{ + MPAvatarCell *selectedAvatar = [self selectedAvatar]; + + // Set avatar modes. + for (NSUInteger item = 0; item < [self.avatarCollectionView numberOfItemsInSection:0]; ++item) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:0]; + MPAvatarCell *avatarCell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath]; + [self updateModeForAvatar:avatarCell atIndexPath:indexPath animated:NO]; + + if (selectedAvatar && avatarCell == selectedAvatar) + [self.avatarCollectionView scrollToItemAtIndexPath:indexPath + atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO]; + } + + // Set the entry container's contents. + switch (activeUserState) { + case MPActiveUserStateNone: + dbg(@"activeUserState -> none"); + break; + case MPActiveUserStateLogin: { + dbg(@"activeUserState -> login"); + self.entryLabel.text = strl( @"Enter your master password:" ); + self.entryField.text = nil; + self.entryField.secureTextEntry = YES; + break; + } + case MPActiveUserStateUserName: { + dbg(@"activeUserState -> userName"); + self.entryLabel.text = strl( @"Enter your full name:" ); + self.entryField.text = nil; + self.entryField.secureTextEntry = NO; + break; + } + case MPActiveUserStateMasterPasswordChoice: { + dbg(@"activeUserState -> masterPasswordChoice"); + self.entryLabel.text = strl( @"Choose your master password:" ); + self.entryField.text = nil; + self.entryField.secureTextEntry = YES; + break; + } + case MPActiveUserStateMasterPasswordConfirmation: { + dbg(@"activeUserState -> masterPasswordConfirmation"); + _masterPasswordChoice = self.entryField.text; + self.entryLabel.text = strl( @"Confirm your master password:" ); + self.entryField.text = nil; + self.entryField.secureTextEntry = YES; + break; + } + case MPActiveUserStateMinimized: + dbg(@"activeUserState -> minimized"); + break; + } + + // Manage the random avatar for the new user if selected. + if (selectedAvatar.avatar == MPAvatarAdd) + selectedAvatar.avatar = arc4random() % MPAvatarCount; + else { + NSIndexPath *newUserIndexPath = [NSIndexPath indexPathForItem:[_userIDs count] inSection:0]; + MPAvatarCell *newUserAvatar = (MPAvatarCell *)[[self avatarCollectionView] cellForItemAtIndexPath:newUserIndexPath]; + newUserAvatar.avatar = MPAvatarAdd; + newUserAvatar.name = strl( @"New User" ); + } + + // Manage the entry container depending on whether a user is activate or not. + switch (activeUserState) { + case MPActiveUserStateNone: { + self.navigationBarToTopConstraint.priority = UILayoutPriorityDefaultHigh; + self.avatarCollectionCenterConstraint.priority = UILayoutPriorityDefaultHigh; + self.avatarCollectionView.scrollEnabled = YES; + self.entryContainer.alpha = 0; + self.footerContainer.alpha = 1; + break; + } + case MPActiveUserStateLogin: + case MPActiveUserStateUserName: + case MPActiveUserStateMasterPasswordChoice: + case MPActiveUserStateMasterPasswordConfirmation: { + self.navigationBarToTopConstraint.priority = UILayoutPriorityDefaultHigh; + self.avatarCollectionCenterConstraint.priority = UILayoutPriorityDefaultLow; + self.avatarCollectionView.scrollEnabled = NO; + self.entryContainer.alpha = 1; + self.footerContainer.alpha = 1; + break; + } + case MPActiveUserStateMinimized: { + self.navigationBarToTopConstraint.priority = 1; + self.avatarCollectionCenterConstraint.priority = UILayoutPriorityDefaultLow; + self.avatarCollectionView.scrollEnabled = NO; + self.entryContainer.alpha = 0; + self.footerContainer.alpha = 0; + break; + } + } + [self.navigationBarToTopConstraint apply]; + [self.avatarCollectionCenterConstraint apply]; + + // Toggle the keyboard. + if (!self.entryContainer.alpha) + [self.entryField resignFirstResponder]; + } completion:^(BOOL finished) { + if (finished && self.entryContainer.alpha) + [self.entryField becomeFirstResponder]; + }]; +} + +#pragma mark - Actions + +- (IBAction)doSignOut:(UIBarButtonItem *)sender { + + [[MPiOSAppDelegate get] signOutAnimated:YES]; +} + +@end diff --git a/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard b/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard index 2ebbf993..9a26cdea 100644 --- a/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard +++ b/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard @@ -1,8 +1,8 @@ - + - + @@ -1198,6 +1198,7 @@ L4m3P4sSw0rD + @@ -2546,6 +2547,7 @@ Only site names and custom passwords are sent to iCloud. Passwords are encrypte + @@ -3173,4 +3175,4 @@ However, it means that anyone who finds your device unlocked can do the same. - \ No newline at end of file + diff --git a/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj b/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj index f6171fbc..083cddf7 100644 --- a/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj +++ b/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj @@ -12,7 +12,10 @@ 93D39262A8A97DB748213309 /* PearlEMail.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D393BB973253D4BAAC84AA /* PearlEMail.m */; }; 93D392EC39DA43C46C692C12 /* NSDictionary+Indexing.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D393B97158D7BE9332EA53 /* NSDictionary+Indexing.h */; }; 93D3932889B6B4206E66A6D6 /* PearlEMail.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D39F7C9F47BF6387FBC5C3 /* PearlEMail.h */; }; + 93D393543ACC701C018C74DA /* PearlUIView.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D393676C32D23A47E27957 /* PearlUIView.m */; }; + 93D3954FCE045A3CC7E804B7 /* MPUsersViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D399E571F61E50A9BF8FAF /* MPUsersViewController.m */; }; 93D3957237D303DE2D38C267 /* MPAvatarCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39B381350802A194BF332 /* MPAvatarCell.m */; }; + 93D3959643EACF286D0152BA /* PearlUINavigationBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39DDDAC305E8ABB4220C7 /* PearlUINavigationBar.m */; }; 93D395F08A087F8A24689347 /* NSArray+Indexing.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39067C0AFDC581794E2B8 /* NSArray+Indexing.m */; }; 93D396AA30690B256F30378A /* PearlNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3956915634581E737B38C /* PearlNavigationController.m */; }; 93D396BA1C74C4A06FD86437 /* PearlOverlay.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D3942A356B639724157982 /* PearlOverlay.h */; }; @@ -21,6 +24,7 @@ 93D399433EA75E50656040CB /* Twitter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93D394077F8FAB8167647187 /* Twitter.framework */; }; 93D399BBC0A7EC746CB1B19B /* MPLogsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D391943675426839501BB8 /* MPLogsViewController.h */; }; 93D39B842AB9A5D072810D76 /* NSError+PearlFullDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D398C95847261903D781D3 /* NSError+PearlFullDescription.h */; }; + 93D39B8F90F58A5D158DDBA3 /* MPPasswordsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3924EE15017F8A12CB436 /* MPPasswordsViewController.m */; }; 93D39C34FE35830EF5BE1D2A /* NSArray+Indexing.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D396D04E57792A54D437AC /* NSArray+Indexing.h */; }; 93D39C8AD8EAB747856B3A8C /* LLModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3923B42DA2DA18F287092 /* LLModel.m */; }; 93D39D596A2E376D6F6F5DA1 /* MPCombinedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D393310223DDB35218467A /* MPCombinedViewController.m */; }; @@ -496,22 +500,29 @@ /* Begin PBXFileReference section */ 93D39067C0AFDC581794E2B8 /* NSArray+Indexing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Indexing.m"; sourceTree = ""; }; + 93D39083C93D90C4B94541AD /* PearlUIView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlUIView.h; sourceTree = ""; }; 93D390A66F69AB1CDB0BFF93 /* LLModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLModel.h; sourceTree = ""; }; + 93D390EEC85E94D9C888643F /* PearlUINavigationBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlUINavigationBar.h; sourceTree = ""; }; 93D390FADEB325D8D54A957D /* PearlOverlay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlOverlay.m; sourceTree = ""; }; + 93D3914D7597F9A28DB9D85E /* MPPasswordsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPPasswordsViewController.h; sourceTree = ""; }; 93D391943675426839501BB8 /* MPLogsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPLogsViewController.h; sourceTree = ""; }; 93D3923B42DA2DA18F287092 /* LLModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LLModel.m; sourceTree = ""; }; + 93D3924EE15017F8A12CB436 /* MPPasswordsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPasswordsViewController.m; sourceTree = ""; }; 93D393310223DDB35218467A /* MPCombinedViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPCombinedViewController.m; sourceTree = ""; }; + 93D393676C32D23A47E27957 /* PearlUIView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlUIView.m; sourceTree = ""; }; 93D393B97158D7BE9332EA53 /* NSDictionary+Indexing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Indexing.h"; sourceTree = ""; }; 93D393BB973253D4BAAC84AA /* PearlEMail.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlEMail.m; sourceTree = ""; }; 93D394077F8FAB8167647187 /* Twitter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Twitter.framework; path = System/Library/Frameworks/Twitter.framework; sourceTree = SDKROOT; }; 93D3942A356B639724157982 /* PearlOverlay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlOverlay.h; sourceTree = ""; }; 93D3956915634581E737B38C /* PearlNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlNavigationController.m; sourceTree = ""; }; 93D396D04E57792A54D437AC /* NSArray+Indexing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Indexing.h"; sourceTree = ""; }; + 93D3971FE104BB4052484151 /* MPUsersViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPUsersViewController.h; sourceTree = ""; }; 93D39730673227EFF6DEFF19 /* MPSetupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPSetupViewController.h; sourceTree = ""; }; 93D3979190DACEBD1F6AE9F4 /* MPLogsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPLogsViewController.m; sourceTree = ""; }; 93D3983278751A530262F64E /* LLConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLConfig.h; sourceTree = ""; }; 93D398567FD02DB2647B8CF3 /* PearlNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlNavigationController.h; sourceTree = ""; }; 93D398C95847261903D781D3 /* NSError+PearlFullDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSError+PearlFullDescription.h"; sourceTree = ""; }; + 93D399E571F61E50A9BF8FAF /* MPUsersViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPUsersViewController.m; sourceTree = ""; }; 93D39A28369954D147E239BA /* MPSetupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPSetupViewController.m; sourceTree = ""; }; 93D39A3CC4D8330831FC8CB4 /* LLToggleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLToggleViewController.h; sourceTree = ""; }; 93D39AA1EE2E1E7B81372240 /* NSDictionary+Indexing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Indexing.m"; sourceTree = ""; }; @@ -521,6 +532,7 @@ 93D39C8E26B06F01566785B7 /* LLToggleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LLToggleViewController.m; sourceTree = ""; }; 93D39CF8ADF4542CDC4CD385 /* MPCombinedViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPCombinedViewController.h; sourceTree = ""; }; 93D39DA27D768B53C8B1330C /* MPAvatarCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPAvatarCell.h; sourceTree = ""; }; + 93D39DDDAC305E8ABB4220C7 /* PearlUINavigationBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlUINavigationBar.m; sourceTree = ""; }; 93D39F7C9F47BF6387FBC5C3 /* PearlEMail.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlEMail.h; sourceTree = ""; }; 93D39F9106F2CCFB94283188 /* NSError+PearlFullDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSError+PearlFullDescription.m"; sourceTree = ""; }; DA04E33D14B1E70400ECA4F3 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; @@ -1666,6 +1678,10 @@ DAFC5657172C573B00CB5CC5 /* InAppSettingsKit */, DA5BFA47147E415C00F98B1E /* Frameworks */, DA5BFA45147E415C00F98B1E /* Products */, + 93D399E571F61E50A9BF8FAF /* MPUsersViewController.m */, + 93D3971FE104BB4052484151 /* MPUsersViewController.h */, + 93D393676C32D23A47E27957 /* PearlUIView.m */, + 93D39083C93D90C4B94541AD /* PearlUIView.h */, ); sourceTree = ""; }; @@ -2514,11 +2530,15 @@ 93D39730673227EFF6DEFF19 /* MPSetupViewController.h */, 93D3979190DACEBD1F6AE9F4 /* MPLogsViewController.m */, 93D391943675426839501BB8 /* MPLogsViewController.h */, + 93D3924EE15017F8A12CB436 /* MPPasswordsViewController.m */, + 93D3914D7597F9A28DB9D85E /* MPPasswordsViewController.h */, 93D393310223DDB35218467A /* MPCombinedViewController.m */, 93D39CF8ADF4542CDC4CD385 /* MPCombinedViewController.h */, DA38D6A218CCB5BF009AEB3E /* Storyboard.storyboard */, 93D39B381350802A194BF332 /* MPAvatarCell.m */, 93D39DA27D768B53C8B1330C /* MPAvatarCell.h */, + 93D39DDDAC305E8ABB4220C7 /* PearlUINavigationBar.m */, + 93D390EEC85E94D9C888643F /* PearlUINavigationBar.h */, ); path = iOS; sourceTree = ""; @@ -3819,6 +3839,10 @@ DA095E75172F4CD8001C948B /* MPLogsViewController.m in Sources */, 93D39D596A2E376D6F6F5DA1 /* MPCombinedViewController.m in Sources */, 93D3957237D303DE2D38C267 /* MPAvatarCell.m in Sources */, + 93D39B8F90F58A5D158DDBA3 /* MPPasswordsViewController.m in Sources */, + 93D3954FCE045A3CC7E804B7 /* MPUsersViewController.m in Sources */, + 93D3959643EACF286D0152BA /* PearlUINavigationBar.m in Sources */, + 93D393543ACC701C018C74DA /* PearlUIView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MasterPassword/ObjC/iOS/PearlUINavigationBar.h b/MasterPassword/ObjC/iOS/PearlUINavigationBar.h new file mode 100644 index 00000000..3bb15eb4 --- /dev/null +++ b/MasterPassword/ObjC/iOS/PearlUINavigationBar.h @@ -0,0 +1,26 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// PearlUINavigationBar.h +// PearlUINavigationBar +// +// Created by lhunath on 2014-03-17. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import + +@interface PearlUINavigationBar : UINavigationBar + +@property (assign, nonatomic) BOOL ignoreTouches; +@property (assign, nonatomic) BOOL invisible; + +@end diff --git a/MasterPassword/ObjC/iOS/PearlUINavigationBar.m b/MasterPassword/ObjC/iOS/PearlUINavigationBar.m new file mode 100644 index 00000000..7edcab3c --- /dev/null +++ b/MasterPassword/ObjC/iOS/PearlUINavigationBar.m @@ -0,0 +1,43 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// PearlUINavigationBar.h +// PearlUINavigationBar +// +// Created by lhunath on 2014-03-17. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import "PearlUINavigationBar.h" + +@implementation PearlUINavigationBar + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + + UIView *hitView = [super hitTest:point withEvent:event]; + if (self.ignoreTouches && hitView == self) + return nil; + + return hitView; +} + +- (void)setInvisible:(BOOL)invisible { + + _invisible = invisible; + + if (invisible) { + self.translucent = YES; + self.shadowImage = [UIImage new]; + [self setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault]; + } +} + +@end diff --git a/MasterPassword/ObjC/iOS/PearlUIView.h b/MasterPassword/ObjC/iOS/PearlUIView.h new file mode 100644 index 00000000..96db6438 --- /dev/null +++ b/MasterPassword/ObjC/iOS/PearlUIView.h @@ -0,0 +1,25 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// PearlUIView.h +// PearlUIView +// +// Created by lhunath on 2014-03-17. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import + +@interface PearlUIView : UIView + +@property(assign, nonatomic) BOOL ignoreTouches; + +@end diff --git a/MasterPassword/ObjC/iOS/PearlUIView.m b/MasterPassword/ObjC/iOS/PearlUIView.m new file mode 100644 index 00000000..7e759804 --- /dev/null +++ b/MasterPassword/ObjC/iOS/PearlUIView.m @@ -0,0 +1,32 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// PearlUIView.h +// PearlUIView +// +// Created by lhunath on 2014-03-17. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import "PearlUIView.h" + +@implementation PearlUIView + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + + UIView *hitView = [super hitTest:point withEvent:event]; + if (self.ignoreTouches && hitView == self) + return nil; + + return hitView; +} + +@end diff --git a/MasterPassword/ObjC/iOS/Storyboard.storyboard b/MasterPassword/ObjC/iOS/Storyboard.storyboard index 3c786956..a8475caf 100644 --- a/MasterPassword/ObjC/iOS/Storyboard.storyboard +++ b/MasterPassword/ObjC/iOS/Storyboard.storyboard @@ -4,651 +4,33 @@ + - + - + - + - - - - - - - - - - - - - - - - - - Title - Title - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + @@ -670,13 +52,21 @@ - + + + + + + + + + - + + + - - + + + + + - + + + + @@ -768,107 +166,106 @@ - - - - + + + - - - - - - - - - - - - - - + + + + + + + + - - - + - - - - - + + - - - - + + + - + - - - - - - - - - + + - - - - - - - + + - - + + @@ -877,14 +274,13 @@ - - + @@ -892,16 +288,771 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Title + Title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -912,6 +1063,7 @@ +