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

2
External/LoveLyndir vendored

@ -1 +1 @@
Subproject commit 97eafd9b59f84bd9e3fc3cb4313df8b3c034e766
Subproject commit 0b858644cb58c357e055b41c2747ad9d45fa9ce3

2
External/Pearl vendored

@ -1 +1 @@
Subproject commit 575b409cca36eabfaacf0a963ed259454cb8ec66
Subproject commit 673217287de2920d21ef84f1d6811d8250f187b4

View File

@ -7,7 +7,7 @@ CF_EXTERN_C_BEGIN
/*!
\brief The Reveal Log level bit flags.
\discussion These flags are addative. Ie, you should bitwise OR them together.
\discussion These flags are additive. i.e. you should bitwise OR them together.
\seealso IBARevealLoggerSetLevelMask
\seealso IBARevealLoggerGetLevelMask

Binary file not shown.

View File

@ -5,13 +5,11 @@
<key>IDESourceControlProjectFavoriteDictionaryKey</key>
<false/>
<key>IDESourceControlProjectIdentifier</key>
<string>D4AB9F0C-D746-4319-AABF-B24705099AED</string>
<string>3D68B2F1-988A-48C3-8450-B37D43BDFE92</string>
<key>IDESourceControlProjectName</key>
<string>MasterPassword</string>
<key>IDESourceControlProjectOriginsDictionary</key>
<dict>
<key>2B6DA448-3730-4F84-B2C3-51272E0D42F3</key>
<string>ssh://github.com/lhunath/DCIntrospect.git</string>
<key>5263993D-5FE8-464F-B66E-B0F7C2DFF410</key>
<string>ssh://github.com/lhunath/UbiquityStoreManager.git</string>
<key>6A449EC2-A2A3-4635-9C5F-A811E011EAC3</key>
@ -24,8 +22,10 @@
<string>git://github.com/lhunath/uicolor-utilities.git</string>
<key>B0F634DD-AEE1-4F0D-AE35-4FAF51AD1B5A</key>
<string>git://github.com/lhunath/RHStatusItemView.git</string>
<key>CBA93B91-B799-4CC6-85B6-749792B76DD4</key>
<string>ssh://github.com/lhunath/InAppSettingsKit.git</string>
<key>CDDE92CF-0136-4DE0-8318-80EDB5C8CAF9</key>
<string>git://github.com/lhunath/InAppSettingsKit.git</string>
<key>D5CE8AB8-2F69-4A08-A2CE-93C70E0F0567</key>
<string>https://github.com/lhunath/DCIntrospect.git</string>
<key>E4C8E206-229C-4DA8-A130-0C544DEC7E07</key>
<string>git://github.com/jonmarimba/jrswizzle.git</string>
<key>FF42A9E0-F41C-42FC-88CD-F2CCDE15DBB6</key>
@ -35,8 +35,6 @@
<string>MasterPassword.xcworkspace</string>
<key>IDESourceControlProjectRelativeInstallPathDictionary</key>
<dict>
<key>2B6DA448-3730-4F84-B2C3-51272E0D42F3</key>
<string>../External/DCIntrospect</string>
<key>5263993D-5FE8-464F-B66E-B0F7C2DFF410</key>
<string>../External/UbiquityStoreManager</string>
<key>6A449EC2-A2A3-4635-9C5F-A811E011EAC3</key>
@ -49,8 +47,10 @@
<string>../External/Pearl/External/uicolor-utilities</string>
<key>B0F634DD-AEE1-4F0D-AE35-4FAF51AD1B5A</key>
<string>../External/RHStatusItemView</string>
<key>CBA93B91-B799-4CC6-85B6-749792B76DD4</key>
<key>CDDE92CF-0136-4DE0-8318-80EDB5C8CAF9</key>
<string>../External/InAppSettingsKit</string>
<key>D5CE8AB8-2F69-4A08-A2CE-93C70E0F0567</key>
<string>../External/DCIntrospect</string>
<key>E4C8E206-229C-4DA8-A130-0C544DEC7E07</key>
<string>../External/Pearl/External/jrswizzle</string>
<key>FF42A9E0-F41C-42FC-88CD-F2CCDE15DBB6</key>
@ -68,7 +68,7 @@
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
<string>public.vcs.git</string>
<key>IDESourceControlWCCIdentifierKey</key>
<string>2B6DA448-3730-4F84-B2C3-51272E0D42F3</string>
<string>D5CE8AB8-2F69-4A08-A2CE-93C70E0F0567</string>
<key>IDESourceControlWCCName</key>
<string>DCIntrospect</string>
</dict>
@ -84,7 +84,7 @@
<key>IDESourceControlRepositoryExtensionIdentifierKey</key>
<string>public.vcs.git</string>
<key>IDESourceControlWCCIdentifierKey</key>
<string>CBA93B91-B799-4CC6-85B6-749792B76DD4</string>
<string>CDDE92CF-0136-4DE0-8318-80EDB5C8CAF9</string>
<key>IDESourceControlWCCName</key>
<string>InAppSettingsKit</string>
</dict>

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>

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