From d5d33da12fd2ee7c289bbfd6db3c572665e70b04 Mon Sep 17 00:00:00 2001 From: Maarten Billemont Date: Mon, 17 Apr 2017 21:57:08 -0400 Subject: [PATCH] Fixed UI issues with passwords list and drop-down animation + support for phrase and name default types. [FIXED] Fixed issues with animating changes in the passwords list during certain & multiple events. [FIXED] Slightly broken UI prior to drop-down animation & improved animation a bit. [ADDED] Phrase & Name default password types. --- platform-darwin/External/Pearl | 2 +- .../Source/Mac/MPInitialWindowController.m | 8 +- .../Source/Mac/MPPasswordWindowController.m | 38 +++--- .../Source/iOS/MPPasswordsViewController.m | 73 ++++++++---- platform-darwin/Source/iOS/MPPopdownSegue.m | 43 ++++--- .../Source/iOS/MPPreferencesViewController.h | 4 +- .../Source/iOS/MPPreferencesViewController.m | 60 ++++++++-- .../Source/iOS/Storyboard.storyboard | 112 +++++++++++------- 8 files changed, 211 insertions(+), 129 deletions(-) diff --git a/platform-darwin/External/Pearl b/platform-darwin/External/Pearl index eee65e95..8853d299 160000 --- a/platform-darwin/External/Pearl +++ b/platform-darwin/External/Pearl @@ -1 +1 @@ -Subproject commit eee65e95220fdc1c775dbecdc795d64da1dad047 +Subproject commit 8853d299b2ac9b54e67be4da54176122efb9a0a4 diff --git a/platform-darwin/Source/Mac/MPInitialWindowController.m b/platform-darwin/Source/Mac/MPInitialWindowController.m index 3673c8f6..8b3d84a8 100644 --- a/platform-darwin/Source/Mac/MPInitialWindowController.m +++ b/platform-darwin/Source/Mac/MPInitialWindowController.m @@ -28,10 +28,10 @@ [super windowDidLoad]; - [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:self.window - queue:nil usingBlock:^(NSNotification *note) { - [MPMacAppDelegate get].initialWindowController = nil; - }]; + PearlAddNotificationObserver( NSWindowWillCloseNotification, self.window, nil, ^(id host, NSNotification *note) { + PearlRemoveNotificationObserversFrom( host ); + [MPMacAppDelegate get].initialWindowController = nil; + } ); } #pragma mark - Actions diff --git a/platform-darwin/Source/Mac/MPPasswordWindowController.m b/platform-darwin/Source/Mac/MPPasswordWindowController.m index 2f542103..28dedd6e 100644 --- a/platform-darwin/Source/Mac/MPPasswordWindowController.m +++ b/platform-darwin/Source/Mac/MPPasswordWindowController.m @@ -41,40 +41,38 @@ [self replaceFonts:self.window.contentView]; prof_rewind( @"replaceFonts" ); - [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowDidBecomeKeyNotification object:self.window - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + PearlAddNotificationObserver( NSWindowDidBecomeKeyNotification, self.window, [NSOperationQueue mainQueue], + ^(id host, NSNotification *note) { prof_new( @"didBecomeKey" ); [self.window makeKeyAndOrderFront:nil]; prof_rewind( @"fadeIn" ); [self updateUser]; prof_finish( @"updateUser" ); - }]; - [[NSNotificationCenter defaultCenter] addObserverForName:NSWindowWillCloseNotification object:self.window - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + } ); + PearlAddNotificationObserver( NSWindowWillCloseNotification, self.window, [NSOperationQueue mainQueue], + ^(id host, NSNotification *note) { + PearlRemoveNotificationObservers(); + NSWindow *sheet = [self.window attachedSheet]; if (sheet) [self.window endSheet:sheet]; - }]; - [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationWillResignActiveNotification object:nil - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + } ); + PearlAddNotificationObserver( NSApplicationWillResignActiveNotification, nil, [NSOperationQueue mainQueue], + ^(id host, NSNotification *note) { [self.window close]; - }]; - [[NSNotificationCenter defaultCenter] addObserverForName:MPSignedInNotification object:nil - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - [self updateUser]; - }]; - [[NSNotificationCenter defaultCenter] addObserverForName:MPSignedOutNotification object:nil - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { - [self updateUser]; - }]; + } ); + PearlAddNotificationObserver( MPSignedInNotification, nil, [NSOperationQueue mainQueue], ^(id host, NSNotification *note) { + [self updateUser]; + } ); + PearlAddNotificationObserver( MPSignedOutNotification, nil, [NSOperationQueue mainQueue], ^(id host, NSNotification *note) { + [self updateUser]; + } ); [self observeKeyPath:@"sitesController.selection" withBlock:^(id from, id to, NSKeyValueChange cause, id _self) { - prof_new( @"sitesController.selection" ); [_self updateSelection]; - prof_finish( @"updateSelection" ); }]; prof_rewind( @"observers" ); - NSSearchFieldCell *siteFieldCell = (NSSearchFieldCell *)self.siteField.cell; + NSSearchFieldCell *siteFieldCell = self.siteField.cell; siteFieldCell.searchButtonCell = nil; siteFieldCell.cancelButtonCell = nil; diff --git a/platform-darwin/Source/iOS/MPPasswordsViewController.m b/platform-darwin/Source/iOS/MPPasswordsViewController.m index b332bb89..249ff728 100644 --- a/platform-darwin/Source/iOS/MPPasswordsViewController.m +++ b/platform-darwin/Source/iOS/MPPasswordsViewController.m @@ -46,6 +46,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { NSUInteger _transientItem; NSCharacterSet *_siteNameAcceptableCharactersSet; NSArray *_fuzzyGroups; + NSMutableArray *_passwordCollectionViewUpdatesBatch; } #pragma mark - Life @@ -62,6 +63,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { _backgroundColor = self.passwordCollectionView.backgroundColor; _darkenedBackgroundColor = [_backgroundColor colorWithAlphaComponent:0.6f]; _transientItem = NSNotFound; + _passwordCollectionViewUpdatesBatch = [NSMutableArray arrayWithCapacity:4]; self.view.backgroundColor = [UIColor clearColor]; [self.passwordCollectionView automaticallyAdjustInsetsForKeyboard]; @@ -181,10 +183,13 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { + Weakify( self ); + if (controller == _fetchedResultsController) { - @try { - [self.passwordCollectionView performBatchUpdates:^{ - [self fetchedItemsDidUpdate]; + @synchronized (_passwordCollectionViewUpdatesBatch) { + [_passwordCollectionViewUpdatesBatch addObject:[^{ + Strongify( self ); + switch (type) { case NSFetchedResultsChangeInsert: [self.passwordCollectionView insertItemsAtIndexPaths:@[ newIndexPath ]]; @@ -193,18 +198,35 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { [self.passwordCollectionView deleteItemsAtIndexPaths:@[ indexPath ]]; break; case NSFetchedResultsChangeMove: - [self.passwordCollectionView moveItemAtIndexPath:indexPath toIndexPath:newIndexPath]; + if (![indexPath isEqual:newIndexPath]) + [self.passwordCollectionView moveItemAtIndexPath:indexPath toIndexPath:newIndexPath]; break; case NSFetchedResultsChangeUpdate: [self.passwordCollectionView reloadItemsAtIndexPaths:@[ indexPath ]]; break; } - } completion:nil]; - } - @catch (NSException *exception) { - wrn( @"While updating password cells: %@", [exception fullDescription] ); - [self.passwordCollectionView reloadData]; + } copy]]; } + + [controller.managedObjectContext performBlock:^{ + PearlMainQueueOperation( ^{ + @try { + [self.passwordCollectionView performBatchUpdates:^{ + [self updateTransientItem]; + + @synchronized (_passwordCollectionViewUpdatesBatch) { + for (VoidBlock block in _passwordCollectionViewUpdatesBatch) + block(); + [_passwordCollectionViewUpdatesBatch removeAllObjects]; + } + } completion:nil]; + } + @catch (NSException *exception) { + wrn( @"While updating password cells: %@", [exception fullDescription] ); + [self.passwordCollectionView reloadData]; + } + } ); + }]; } } @@ -290,25 +312,24 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { }]; } -- (void)fetchedItemsDidUpdate { +- (void)updateTransientItem { NSString *query = self.query; - _showTransientItem = [query length] > 0; - NSUInteger objects = ((id)self.fetchedResultsController.sections[0]).numberOfObjects; - if (_showTransientItem && objects == 1 && - [[[self.fetchedResultsController.fetchedObjects firstObject] name] isEqualToString:query]) - _showTransientItem = NO; - if ([self.passwordCollectionView numberOfSections] > 0) { - if (!_showTransientItem && _transientItem != NSNotFound) - [self.passwordCollectionView deleteItemsAtIndexPaths: - @[ [NSIndexPath indexPathForItem:_transientItem inSection:0] ]]; - else if (_showTransientItem && _transientItem == NSNotFound) - [self.passwordCollectionView insertItemsAtIndexPaths: - @[ [NSIndexPath indexPathForItem:objects inSection:0] ]]; - else if (_transientItem != NSNotFound) - [self.passwordCollectionView reloadItemsAtIndexPaths: - @[ [NSIndexPath indexPathForItem:_transientItem inSection:0] ]]; + _showTransientItem = [query length] > 0 && ![[[self.fetchedResultsController.sections[0] objects] filteredArrayUsingPredicate: + [NSPredicate predicateWithBlock:^BOOL(MPSiteEntity *site, NSDictionary *bindings) { + return [site.name isEqualToString:query]; + }]] count]; + if (!_showTransientItem && _transientItem != NSNotFound) + [self.passwordCollectionView deleteItemsAtIndexPaths: + @[ [NSIndexPath indexPathForItem:_transientItem inSection:0] ]]; + else if (_showTransientItem && _transientItem == NSNotFound) { + NSUInteger objects = [self.fetchedResultsController.sections[0] numberOfObjects]; + [self.passwordCollectionView insertItemsAtIndexPaths: + @[ [NSIndexPath indexPathForItem:objects inSection:0] ]]; } + else if (_transientItem != NSNotFound) + [self.passwordCollectionView reloadItemsAtIndexPaths: + @[ [NSIndexPath indexPathForItem:_transientItem inSection:0] ]]; } - (void)registerObservers { @@ -422,7 +443,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) { PearlMainQueue( ^{ @try { [self.passwordCollectionView performBatchUpdates:^{ - [self fetchedItemsDidUpdate]; + [self updateTransientItem]; NSInteger fromSections = self.passwordCollectionView.numberOfSections; NSInteger toSections = [self numberOfSectionsInCollectionView:self.passwordCollectionView]; diff --git a/platform-darwin/Source/iOS/MPPopdownSegue.m b/platform-darwin/Source/iOS/MPPopdownSegue.m index c0bb1645..a42e2dc7 100644 --- a/platform-darwin/Source/iOS/MPPopdownSegue.m +++ b/platform-darwin/Source/iOS/MPPopdownSegue.m @@ -22,8 +22,6 @@ @implementation MPPopdownSegue { } -static char UnwindingObserverKey; - - (void)perform { MPPasswordsViewController *passwordsVC; @@ -39,19 +37,20 @@ static char UnwindingObserverKey; [passwordsVC.popdownContainer addConstraintsWithVisualFormats:@[ @"H:|[popdownView]|", @"V:|[popdownView]|" ] options:0 metrics:nil views:NSDictionaryOfVariableBindings( popdownView )]; - [UIView animateWithDuration:0.3f animations:^{ - [[passwordsVC.popdownToTopConstraint updatePriority:1] layoutIfNeeded]; - } completion:^(BOOL finished) { - [popdownVC didMoveToParentViewController:passwordsVC]; + [passwordsVC.popdownToTopConstraint layoutIfNeeded]; - id observer = [[NSNotificationCenter defaultCenter] addObserverForName:MPSignedOutNotification object:nil - queue:[NSOperationQueue mainQueue] usingBlock: - ^(NSNotification *note) { - [[[MPPopdownSegue alloc] initWithIdentifier:@"unwind-popdown" source:popdownVC - destination:passwordsVC] perform]; - }]; - objc_setAssociatedObject( popdownVC, &UnwindingObserverKey, observer, OBJC_ASSOCIATION_RETAIN ); - }]; + [UIView animateWithDuration:0.6f delay:0 usingSpringWithDamping:0.75f initialSpringVelocity:1 + options:UIViewAnimationOptionCurveEaseOut animations:^{ + [[passwordsVC.popdownToTopConstraint updatePriority:1] layoutIfNeeded]; + } completion:^(BOOL finished) { + [popdownVC didMoveToParentViewController:passwordsVC]; + + PearlAddNotificationObserverTo( popdownVC, MPSignedOutNotification, nil, [NSOperationQueue mainQueue], + ^(id host, NSNotification *note) { + [[[MPPopdownSegue alloc] initWithIdentifier:@"unwind-popdown" source:popdownVC destination:passwordsVC] + perform]; + } ); + }]; } else { popdownVC = self.sourceViewController; @@ -59,16 +58,16 @@ static char UnwindingObserverKey; passwordsVC = (id)passwordsVC.parentViewController); NSAssert( passwordsVC, @"Couldn't find passwords VC to pop back to." ); - [[NSNotificationCenter defaultCenter] removeObserver:objc_getAssociatedObject( popdownVC, &UnwindingObserverKey )]; - objc_setAssociatedObject( popdownVC, &UnwindingObserverKey, nil, OBJC_ASSOCIATION_RETAIN ); + PearlRemoveNotificationObserversFrom( popdownVC ); [popdownVC willMoveToParentViewController:nil]; - [UIView animateWithDuration:0.3f delay:0 options:UIViewAnimationOptionOverrideInheritedDuration animations:^{ - [[passwordsVC.popdownToTopConstraint updatePriority:UILayoutPriorityDefaultHigh] layoutIfNeeded]; - } completion:^(BOOL finished) { - [popdownVC.view removeFromSuperview]; - [popdownVC removeFromParentViewController]; - }]; + [UIView animateWithDuration:0.4f delay:0 options:UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionOverrideInheritedDuration + animations:^{ + [[passwordsVC.popdownToTopConstraint updatePriority:UILayoutPriorityDefaultHigh] layoutIfNeeded]; + } completion:^(BOOL finished) { + [popdownVC.view removeFromSuperview]; + [popdownVC removeFromParentViewController]; + }]; } } diff --git a/platform-darwin/Source/iOS/MPPreferencesViewController.h b/platform-darwin/Source/iOS/MPPreferencesViewController.h index 57445f01..e74a3f87 100644 --- a/platform-darwin/Source/iOS/MPPreferencesViewController.h +++ b/platform-darwin/Source/iOS/MPPreferencesViewController.h @@ -29,8 +29,10 @@ @property(weak, nonatomic) IBOutlet UITableViewCell *exportCell; @property(weak, nonatomic) IBOutlet UITableViewCell *checkInconsistencies; @property(weak, nonatomic) IBOutlet UIImageView *avatarImage; -@property(weak, nonatomic) IBOutlet UISegmentedControl *generatedTypeControl; +@property(weak, nonatomic) IBOutlet UISegmentedControl *generated1TypeControl; +@property(weak, nonatomic) IBOutlet UISegmentedControl *generated2TypeControl; @property(weak, nonatomic) IBOutlet UISegmentedControl *storedTypeControl; +@property(weak, nonatomic) IBOutlet UILabel *passwordTypeExample; - (IBAction)previousAvatar:(id)sender; - (IBAction)nextAvatar:(id)sender; diff --git a/platform-darwin/Source/iOS/MPPreferencesViewController.m b/platform-darwin/Source/iOS/MPPreferencesViewController.m index 10e656e7..9d19ddb8 100644 --- a/platform-darwin/Source/iOS/MPPreferencesViewController.m +++ b/platform-darwin/Source/iOS/MPPreferencesViewController.m @@ -55,12 +55,24 @@ - (void)reload { MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserForMainThread]; - self.generatedTypeControl.selectedSegmentIndex = [self generatedSegmentIndexForType:activeUser.defaultType]; - self.storedTypeControl.selectedSegmentIndex = [self storedSegmentIndexForType:activeUser.defaultType]; self.avatarImage.image = [UIImage imageNamed:strf( @"avatar-%lu", (unsigned long)activeUser.avatar )]; self.savePasswordSwitch.on = activeUser.saveKey; self.touchIDSwitch.on = activeUser.touchID; self.touchIDSwitch.enabled = self.savePasswordSwitch.on && [[MPiOSAppDelegate get] isFeatureUnlocked:MPProductTouchID]; + + MPSiteType defaultType = activeUser.defaultType; + self.generated1TypeControl.selectedSegmentIndex = [self generated1SegmentIndexForType:defaultType]; + self.generated2TypeControl.selectedSegmentIndex = [self generated2SegmentIndexForType:defaultType]; + self.storedTypeControl.selectedSegmentIndex = [self storedSegmentIndexForType:defaultType]; + PearlNotMainQueue( ^{ + NSString *examplePassword = nil; + if (defaultType & MPSiteTypeClassGenerated) + examplePassword = [MPAlgorithmDefault generatePasswordForSiteNamed:@"test" ofType:defaultType + withCounter:1 usingKey:[MPiOSAppDelegate get].key]; + PearlMainQueue( ^{ + self.passwordTypeExample.text = [examplePassword length]? [NSString stringWithFormat:@"eg. %@", examplePassword]: nil; + } ); + } ); } #pragma mark - UITableViewDelegate @@ -88,14 +100,18 @@ [self dismissPopup]; [[MPiOSAppDelegate get] signOutAnimated:YES]; } + if (cell == self.feedbackCell) [[MPiOSAppDelegate get] showFeedbackWithLogs:YES forVC:self]; + if (cell == self.exportCell) [[MPiOSAppDelegate get] showExportForVC:self]; + if (cell == self.showHelpCell) { MPPasswordsViewController *passwordsVC = [self dismissPopup]; [passwordsVC performSegueWithIdentifier:@"guide" sender:self]; } + if (cell == self.checkInconsistencies) [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { if ([[MPiOSAppDelegate get] findAndFixInconsistenciesSaveInContext:context] == MPFixableResultNoProblems) @@ -140,11 +156,13 @@ } ); }]; - if (sender == self.generatedTypeControl || sender == self.storedTypeControl) { - if (sender == self.generatedTypeControl) + if (sender == self.generated1TypeControl || sender == self.generated2TypeControl || sender == self.storedTypeControl) { + if (sender != self.generated1TypeControl) + self.generated1TypeControl.selectedSegmentIndex = -1; + if (sender != self.generated2TypeControl) + self.generated2TypeControl.selectedSegmentIndex = -1; + if (sender != self.storedTypeControl) self.storedTypeControl.selectedSegmentIndex = -1; - else if (sender == self.storedTypeControl) - self.generatedTypeControl.selectedSegmentIndex = -1; MPSiteType defaultType = [self typeForSelectedSegment]; [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { @@ -226,10 +244,17 @@ - (MPSiteType)typeForSelectedSegment { - NSInteger selectedGeneratedIndex = self.generatedTypeControl.selectedSegmentIndex; + NSInteger selectedGenerated1Index = self.generated1TypeControl.selectedSegmentIndex; + NSInteger selectedGenerated2Index = self.generated2TypeControl.selectedSegmentIndex; NSInteger selectedStoredIndex = self.storedTypeControl.selectedSegmentIndex; - switch (selectedGeneratedIndex) { + switch (selectedGenerated1Index) { + case 0: + return MPSiteTypeGeneratedPhrase; + case 1: + return MPSiteTypeGeneratedName; + default: + switch (selectedGenerated2Index) { case 0: return MPSiteTypeGeneratedMaximum; case 1: @@ -250,13 +275,26 @@ case 1: return MPSiteTypeStoredDevicePrivate; default: - Throw( @"unsupported selected type index: generated=%ld, stored=%ld", (long)selectedGeneratedIndex, - (long)selectedStoredIndex ); + Throw( @"unsupported selected type index: generated1=%ld, generated2=%ld, stored=%ld", + (long)selectedGenerated1Index, (long)selectedGenerated2Index, (long)selectedStoredIndex ); } } + } } -- (NSInteger)generatedSegmentIndexForType:(MPSiteType)type { +- (NSInteger)generated1SegmentIndexForType:(MPSiteType)type { + + switch (type) { + case MPSiteTypeGeneratedPhrase: + return 0; + case MPSiteTypeGeneratedName: + return 1; + default: + return -1; + } +} + +- (NSInteger)generated2SegmentIndexForType:(MPSiteType)type { switch (type) { case MPSiteTypeGeneratedMaximum: diff --git a/platform-darwin/Source/iOS/Storyboard.storyboard b/platform-darwin/Source/iOS/Storyboard.storyboard index 83d19308..20f06c85 100644 --- a/platform-darwin/Source/iOS/Storyboard.storyboard +++ b/platform-darwin/Source/iOS/Storyboard.storyboard @@ -606,7 +606,7 @@ - + @@ -616,10 +616,10 @@ - + - + - + - + - + - + - + + + - + - - + + + + - + - + - + - + @@ -858,10 +880,10 @@ - + - + @@ -899,10 +921,10 @@ - + - + - + - + - + - + - + - +