2
0

Updated animations for activation of the passwords VC and fancier focussed user.

This commit is contained in:
Maarten Billemont
2014-03-19 20:09:25 -04:00
parent 318aca4d8f
commit d036b43d6f
22 changed files with 2261 additions and 1328 deletions

View File

@@ -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

View File

@@ -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;
}];
}

View File

@@ -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 <UICollectionViewDataSource, UICollectionViewDelegate, UITextFieldDelegate>
@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

View File

@@ -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 {

View File

@@ -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 <lhunath@lyndir.com>
* @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<UISearchBarDelegate, UISearchDisplayDelegate, UITableViewDataSource, UITableViewDelegate, UICollectionViewDataSource, UICollectionViewDelegate, UITextFieldDelegate>
@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

View File

@@ -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 <lhunath@lyndir.com>
* @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

View File

@@ -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];

View File

@@ -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;

View File

@@ -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 <lhunath@lyndir.com>
* @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 <UICollectionViewDataSource, UICollectionViewDelegate, UITextFieldDelegate>
@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

View File

@@ -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 <lhunath@lyndir.com>
* @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

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="4514" systemVersion="13C64" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" initialViewController="KZF-fe-y9n">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="5053" systemVersion="13C64" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" initialViewController="KZF-fe-y9n">
<dependencies>
<deployment defaultVersion="1536" identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3747"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3733"/>
</dependencies>
<scenes>
<!--Type View Controller - Type-->
@@ -1198,6 +1198,7 @@ L4m3P4sSw0rD</string>
<navigationItem key="navigationItem" id="zZZ-QZ-Yur"/>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<size key="freeformSize" width="305" height="402"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="hkm-U7-Dm7" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
@@ -2546,6 +2547,7 @@ Only site names and custom passwords are sent to iCloud. Passwords are encrypte
<navigationItem key="navigationItem" id="Pm8-fx-hfM"/>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<size key="freeformSize" width="305" height="402"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="LHv-Mk-8Kp" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
@@ -3173,4 +3175,4 @@ However, it means that anyone who finds your device unlocked can do the same.</s
<segue reference="rWT-Kr-cAs"/>
<segue reference="hxY-aA-ngI"/>
</inferredMetricsTieBreakers>
</document>
</document>

View File

@@ -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 = "<group>"; };
93D39083C93D90C4B94541AD /* PearlUIView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlUIView.h; sourceTree = "<group>"; };
93D390A66F69AB1CDB0BFF93 /* LLModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLModel.h; sourceTree = "<group>"; };
93D390EEC85E94D9C888643F /* PearlUINavigationBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlUINavigationBar.h; sourceTree = "<group>"; };
93D390FADEB325D8D54A957D /* PearlOverlay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlOverlay.m; sourceTree = "<group>"; };
93D3914D7597F9A28DB9D85E /* MPPasswordsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPPasswordsViewController.h; sourceTree = "<group>"; };
93D391943675426839501BB8 /* MPLogsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPLogsViewController.h; sourceTree = "<group>"; };
93D3923B42DA2DA18F287092 /* LLModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LLModel.m; sourceTree = "<group>"; };
93D3924EE15017F8A12CB436 /* MPPasswordsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPasswordsViewController.m; sourceTree = "<group>"; };
93D393310223DDB35218467A /* MPCombinedViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPCombinedViewController.m; sourceTree = "<group>"; };
93D393676C32D23A47E27957 /* PearlUIView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlUIView.m; sourceTree = "<group>"; };
93D393B97158D7BE9332EA53 /* NSDictionary+Indexing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Indexing.h"; sourceTree = "<group>"; };
93D393BB973253D4BAAC84AA /* PearlEMail.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlEMail.m; sourceTree = "<group>"; };
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 = "<group>"; };
93D3956915634581E737B38C /* PearlNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlNavigationController.m; sourceTree = "<group>"; };
93D396D04E57792A54D437AC /* NSArray+Indexing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Indexing.h"; sourceTree = "<group>"; };
93D3971FE104BB4052484151 /* MPUsersViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPUsersViewController.h; sourceTree = "<group>"; };
93D39730673227EFF6DEFF19 /* MPSetupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPSetupViewController.h; sourceTree = "<group>"; };
93D3979190DACEBD1F6AE9F4 /* MPLogsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPLogsViewController.m; sourceTree = "<group>"; };
93D3983278751A530262F64E /* LLConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLConfig.h; sourceTree = "<group>"; };
93D398567FD02DB2647B8CF3 /* PearlNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlNavigationController.h; sourceTree = "<group>"; };
93D398C95847261903D781D3 /* NSError+PearlFullDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSError+PearlFullDescription.h"; sourceTree = "<group>"; };
93D399E571F61E50A9BF8FAF /* MPUsersViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPUsersViewController.m; sourceTree = "<group>"; };
93D39A28369954D147E239BA /* MPSetupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPSetupViewController.m; sourceTree = "<group>"; };
93D39A3CC4D8330831FC8CB4 /* LLToggleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLToggleViewController.h; sourceTree = "<group>"; };
93D39AA1EE2E1E7B81372240 /* NSDictionary+Indexing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Indexing.m"; sourceTree = "<group>"; };
@@ -521,6 +532,7 @@
93D39C8E26B06F01566785B7 /* LLToggleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LLToggleViewController.m; sourceTree = "<group>"; };
93D39CF8ADF4542CDC4CD385 /* MPCombinedViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPCombinedViewController.h; sourceTree = "<group>"; };
93D39DA27D768B53C8B1330C /* MPAvatarCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPAvatarCell.h; sourceTree = "<group>"; };
93D39DDDAC305E8ABB4220C7 /* PearlUINavigationBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlUINavigationBar.m; sourceTree = "<group>"; };
93D39F7C9F47BF6387FBC5C3 /* PearlEMail.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlEMail.h; sourceTree = "<group>"; };
93D39F9106F2CCFB94283188 /* NSError+PearlFullDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSError+PearlFullDescription.m"; sourceTree = "<group>"; };
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 = "<group>";
};
@@ -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 = "<group>";
@@ -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;
};

View File

@@ -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 <lhunath@lyndir.com>
* @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 <Foundation/Foundation.h>
@interface PearlUINavigationBar : UINavigationBar
@property (assign, nonatomic) BOOL ignoreTouches;
@property (assign, nonatomic) BOOL invisible;
@end

View File

@@ -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 <lhunath@lyndir.com>
* @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

View File

@@ -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 <lhunath@lyndir.com>
* @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 <Foundation/Foundation.h>
@interface PearlUIView : UIView
@property(assign, nonatomic) BOOL ignoreTouches;
@end

View File

@@ -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 <lhunath@lyndir.com>
* @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

File diff suppressed because it is too large Load Diff