Skip to content

Commit 86c5656

Browse files
committed
Added more logic to settings
1 parent a59ab99 commit 86c5656

File tree

5 files changed

+266
-30
lines changed

5 files changed

+266
-30
lines changed

TOPasscodeViewController/TOPasscodeSettingsViewController.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
@class TOPasscodeSettingsViewController;
1313

1414
typedef NS_ENUM(NSInteger, TOPasscodeSettingsViewState) {
15+
TOPasscodeSettingsViewStateEnterCurrentPassword,
1516
TOPasscodeSettingsViewStateEnterNewPassword,
16-
TOPasscodeSettingsViewStateConfirmNewPassword,
17-
TOPasscodeSettingsViewStateEnterCurrentPassword
17+
TOPasscodeSettingsViewStateConfirmNewPassword
1818
};
1919

2020
NS_ASSUME_NONNULL_BEGIN
@@ -51,6 +51,9 @@ NS_ASSUME_NONNULL_BEGIN
5151
/** Set the visual style of the view controller (light or dark) */
5252
@property (nonatomic, assign) TOPasscodeSettingsViewStyle style;
5353

54+
/** The input type of the passcode */
55+
@property (nonatomic, assign) TOPasscodeType passcodeType;
56+
5457
/** The number of incorrect passcode attempts the user has made. Use this property to decide when to disable input. */
5558
@property (nonatomic, assign) NSInteger failedPasscodeAttemptCount;
5659

TOPasscodeViewController/TOPasscodeSettingsViewController.m

Lines changed: 247 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,24 @@
1111
#import "TOPasscodeSettingsKeypadView.h"
1212
#import "TOPasscodeSettingsWarningLabel.h"
1313

14-
const CGFloat kTOPasscodeSettingsLabelInputSpacing = 18.0f;
14+
const CGFloat kTOPasscodeSettingsLabelInputSpacing = 15.0f;
15+
const CGFloat kTOPasscodeSettingsOptionsButtonOffset = 15.0f;
1516
const CGFloat kTOPasscodeKeypadMaxSizeRatio = 0.40f;
16-
const CGFloat kTOPasscodeKeypadMinHeight = 200.0f;
17+
const CGFloat kTOPasscodeKeypadMinHeight = 165.0f;
1718
const CGFloat kTOPasscodeKeypadMaxHeight = 330.0f;
1819

1920
@interface TOPasscodeSettingsViewController ()
2021

21-
// Views
22+
@property (nonatomic, copy) NSString *potentialPasscode;
23+
24+
/* Layout Calculations */
25+
@property (nonatomic, assign) CGFloat verticalMidPoint;
26+
27+
/* Views */
2228
@property (nonatomic, strong) UIView *containerView;
2329
@property (nonatomic, strong) UILabel *titleLabel;
30+
@property (nonatomic, strong) UILabel *errorLabel;
31+
@property (nonatomic, strong) UIButton *optionsButton;
2432
@property (nonatomic, strong) TOPasscodeNumberInputView *numberInputView;
2533
@property (nonatomic, strong) TOPasscodeSettingsKeypadView *keypadView;
2634
@property (nonatomic, strong) TOPasscodeSettingsWarningLabel *warningLabel;
@@ -29,6 +37,8 @@ @interface TOPasscodeSettingsViewController ()
2937

3038
@implementation TOPasscodeSettingsViewController
3139

40+
#pragma mark - Object Creation -
41+
3242
- (instancetype)initWithStyle:(TOPasscodeSettingsViewStyle)style
3343
{
3444
if (self = [self initWithNibName:nil bundle:nil]) {
@@ -53,6 +63,8 @@ - (void)setUp
5363
_failedPasscodeAttemptCount = 0;
5464
}
5565

66+
#pragma mark - View Set-up -
67+
5668
- (void)viewDidLoad {
5769
[super viewDidLoad];
5870

@@ -89,13 +101,31 @@ - (void)viewDidLoad {
89101
self.keypadView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
90102
[self.view addSubview:self.keypadView];
91103

92-
// Create label view
104+
// Create warning label view
93105
self.warningLabel = [[TOPasscodeSettingsWarningLabel alloc] initWithFrame:CGRectZero];
94106
self.warningLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
95107
self.warningLabel.hidden = YES;
96108
[self.warningLabel sizeToFit];
97109
[self.containerView addSubview:self.warningLabel];
98110

111+
// Create error label view
112+
self.errorLabel = [[UILabel alloc] initWithFrame:CGRectZero];
113+
self.errorLabel.text = NSLocalizedString(@"Passcodes didn't match. Try again.", @"");
114+
self.errorLabel.textAlignment = NSTextAlignmentCenter;
115+
self.errorLabel.font = [UIFont systemFontOfSize:15.0f];
116+
self.errorLabel.numberOfLines = 0;
117+
self.errorLabel.hidden = YES;
118+
[self.errorLabel sizeToFit];
119+
[self.containerView addSubview:self.errorLabel];
120+
121+
// Create Options button
122+
self.optionsButton = [UIButton buttonWithType:UIButtonTypeSystem];
123+
[self.optionsButton setTitle:NSLocalizedString(@"Passcode Options", @"") forState:UIControlStateNormal];
124+
self.optionsButton.titleLabel.font = [UIFont systemFontOfSize:15.0f];
125+
[self.optionsButton sizeToFit];
126+
[self.optionsButton addTarget:self action:@selector(optionsCodeButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
127+
[self.view addSubview:self.optionsButton];
128+
99129
// Add callbacks for the keypad view
100130
self.keypadView.numberButtonTappedHandler = ^(NSInteger number) {
101131
NSString *numberString = [NSString stringWithFormat:@"%ld", number];
@@ -108,25 +138,13 @@ - (void)viewDidLoad {
108138
CGRect frame = self.containerView.frame;
109139
frame.size.width = self.view.bounds.size.width;
110140
frame.size.height = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.numberInputView.frame)
111-
+ (kTOPasscodeSettingsLabelInputSpacing * 2.0f);
141+
+ CGRectGetHeight(self.warningLabel.frame) + (kTOPasscodeSettingsLabelInputSpacing * 2.0f);
112142
self.containerView.frame = CGRectIntegral(frame);
113143

114-
// Set frame of title label
115-
frame = self.titleLabel.frame;
116-
frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f;
117-
self.titleLabel.frame = CGRectIntegral(frame);
118-
119-
// Set frame of number pad
120-
frame = self.numberInputView.frame;
121-
frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f;
122-
frame.origin.y = (CGRectGetHeight(self.titleLabel.frame) + kTOPasscodeSettingsLabelInputSpacing);
123-
self.numberInputView.frame = CGRectIntegral(frame);
124-
125-
// Set the frame for the warning view
126-
frame = self.warningLabel.frame;
127-
frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f;
128-
frame.origin.y = CGRectGetMaxY(self.numberInputView.frame) + kTOPasscodeSettingsLabelInputSpacing;
129-
self.warningLabel.frame = frame;
144+
//Work out the vertical offset of the container view assuming the warning label doesn't count
145+
self.verticalMidPoint = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.numberInputView.frame)
146+
+ kTOPasscodeSettingsLabelInputSpacing;
147+
self.verticalMidPoint *= 0.5f;
130148

131149
// Apply light/dark mode
132150
[self applyThemeForStyle:self.style];
@@ -137,20 +155,35 @@ - (void)viewWillAppear:(BOOL)animated
137155
[super viewWillAppear:animated];
138156

139157
self.state = self.requireCurrentPasscode ? TOPasscodeSettingsViewStateEnterCurrentPassword : TOPasscodeSettingsViewStateEnterNewPassword;
140-
[self updateViewsForState:self.state];
158+
[self updateContentForState:self.state type:self.passcodeType];
141159
}
142160

143-
- (void)updateViewsForState:(TOPasscodeSettingsViewState)state
161+
#pragma mark - View Update -
162+
163+
- (void)updateContentForState:(TOPasscodeSettingsViewState)state type:(TOPasscodeType)type
144164
{
145165
BOOL confirmingPasscode = state == TOPasscodeSettingsViewStateEnterCurrentPassword;
146166

147-
self.warningLabel.hidden = (confirmingPasscode && self.failedPasscodeAttemptCount == 0);
167+
// Update the visibility of the options button
168+
self.optionsButton.hidden = !(state == TOPasscodeSettingsViewStateEnterNewPassword);
169+
170+
// Update the warning label
171+
self.warningLabel.hidden = !(confirmingPasscode && self.failedPasscodeAttemptCount > 0);
148172
self.warningLabel.numberOfWarnings = self.failedPasscodeAttemptCount;
149173

150174
CGRect frame = self.warningLabel.frame;
151175
frame.origin.x = (CGRectGetWidth(self.view.frame) - frame.size.width) * 0.5f;
152176
self.warningLabel.frame = frame;
153177

178+
// Reset the passcode view
179+
[self.numberInputView resetPasscodeAnimated:NO playImpact:NO];
180+
181+
// Change the input view if needed
182+
if (self.passcodeType < TOPasscodeTypeCustomNumeric) {
183+
self.numberInputView.requiredLength = (self.passcodeType == TOPasscodeTypeSixDigits) ? 6 : 4;
184+
}
185+
186+
// Update text depending on state
154187
switch (state) {
155188
case TOPasscodeSettingsViewStateEnterCurrentPassword:
156189
self.titleLabel.text = NSLocalizedString(@"Enter your passcode", @"");
@@ -162,6 +195,72 @@ - (void)updateViewsForState:(TOPasscodeSettingsViewState)state
162195
self.titleLabel.text = NSLocalizedString(@"Confirm new passcode", @"");
163196
break;
164197
}
198+
199+
// Resize text label to fit new text
200+
[self.titleLabel sizeToFit];
201+
frame = self.titleLabel.frame;
202+
frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f;
203+
self.titleLabel.frame = frame;
204+
205+
// Resize passcode view
206+
[self.numberInputView sizeToFit];
207+
frame = self.numberInputView.frame;
208+
frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f;
209+
self.numberInputView.frame = frame;
210+
}
211+
212+
- (void)transitionToState:(TOPasscodeSettingsViewState)state animated:(BOOL)animated
213+
{
214+
// Preserve the current view state
215+
UIView *snapshot = nil;
216+
217+
BOOL reverseDirection = state < self.state;
218+
219+
// If animated, take a snapshot of the current container view
220+
if (animated) {
221+
snapshot = [self.containerView snapshotViewAfterScreenUpdates:NO];
222+
snapshot.frame = self.containerView.frame;
223+
[self.view addSubview:snapshot];
224+
}
225+
226+
self.errorLabel.hidden = YES;
227+
228+
// Update the layout for the new state
229+
self.state = state;
230+
231+
// Cancel out now if we're not animating
232+
if (!animated) {
233+
return;
234+
}
235+
236+
// Place the live container off screen to the right
237+
CGFloat multiplier = reverseDirection ? -1.0f : 1.0f;
238+
self.containerView.frame = CGRectOffset(self.containerView.frame, self.view.frame.size.width * multiplier, 0.0f);
239+
240+
// Update the options button alpha depending on transition state
241+
self.optionsButton.hidden = NO;
242+
self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPassword) ? 0.0f : 1.0f;
243+
244+
// Perform an animation where the snapshot slides off, and the new container slides in
245+
id animationBlock = ^{
246+
snapshot.frame = CGRectOffset(snapshot.frame, -self.view.frame.size.width * multiplier, 0.0f);
247+
self.containerView.frame = CGRectOffset(self.containerView.frame, -self.view.frame.size.width * multiplier, 0.0f);
248+
self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPassword) ? 1.0f : 0.0f;
249+
};
250+
251+
// Clean up by removing the snapshot view
252+
id completionBlock = ^(BOOL complete) {
253+
[snapshot removeFromSuperview];
254+
};
255+
256+
// Perform the animation
257+
[UIView animateWithDuration:0.4f
258+
delay:0.0f
259+
usingSpringWithDamping:1.0f
260+
initialSpringVelocity:0.7f
261+
options:0
262+
animations:animationBlock
263+
completion:completionBlock];
165264
}
166265

167266
- (void)viewDidLayoutSubviews
@@ -187,9 +286,39 @@ - (void)viewDidLayoutSubviews
187286

188287
// Layout the container view
189288
frame = self.containerView.frame;
190-
frame.origin.y = ((viewSize.height - (topContentHeight + self.keypadView.frame.size.height)) - frame.size.height) * 0.5f;
289+
frame.origin.y = (((viewSize.height - (topContentHeight + self.keypadView.frame.size.height))) * 0.5f) - self.verticalMidPoint;
191290
frame.origin.y += topContentHeight;
192291
self.containerView.frame = CGRectIntegral(frame);
292+
293+
// Layout the passcode options button
294+
frame = self.optionsButton.frame;
295+
frame.origin.y = CGRectGetMinY(self.keypadView.frame) - kTOPasscodeSettingsOptionsButtonOffset - CGRectGetHeight(frame);
296+
frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f;
297+
self.optionsButton.frame = frame;
298+
299+
// Set frame of title label
300+
frame = self.titleLabel.frame;
301+
frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f;
302+
self.titleLabel.frame = CGRectIntegral(frame);
303+
304+
// Set frame of number pad
305+
frame = self.numberInputView.frame;
306+
frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f;
307+
frame.origin.y = (CGRectGetHeight(self.titleLabel.frame) + kTOPasscodeSettingsLabelInputSpacing);
308+
self.numberInputView.frame = CGRectIntegral(frame);
309+
310+
// Set the frame for the warning view
311+
frame = self.warningLabel.frame;
312+
frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f;
313+
frame.origin.y = CGRectGetMaxY(self.numberInputView.frame) + kTOPasscodeSettingsLabelInputSpacing;
314+
self.warningLabel.frame = CGRectIntegral(frame);
315+
316+
// Set the frame of the error view
317+
frame = self.errorLabel.frame;
318+
frame.size = [self.errorLabel sizeThatFits:CGSizeMake(300.0f, CGFLOAT_MAX)];
319+
frame.origin.y = CGRectGetMaxY(self.numberInputView.frame) + kTOPasscodeSettingsLabelInputSpacing;
320+
frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f;
321+
self.errorLabel.frame = CGRectIntegral(frame);
193322
}
194323

195324
- (void)applyThemeForStyle:(TOPasscodeSettingsViewStyle)style
@@ -230,6 +359,23 @@ - (void)applyThemeForStyle:(TOPasscodeSettingsViewStyle)style
230359

231360
#pragma mark - Data Management -
232361
- (void)numberViewDidEnterPasscode:(NSString *)passcode
362+
{
363+
switch (self.state) {
364+
case TOPasscodeSettingsViewStateEnterCurrentPassword:
365+
[self validateCurrentPasscodeAttemptWithPasscode:passcode];
366+
break;
367+
case TOPasscodeSettingsViewStateEnterNewPassword:
368+
[self didReceiveNewPasscode:passcode];
369+
break;
370+
case TOPasscodeSettingsViewStateConfirmNewPassword:
371+
[self confirmNewPasscode:passcode];
372+
break;
373+
}
374+
375+
[self updateContentForState:self.state type:self.passcodeType];
376+
}
377+
378+
- (void)validateCurrentPasscodeAttemptWithPasscode:(NSString *)passcode
233379
{
234380
if (![self.delegate respondsToSelector:@selector(passcodeSettingsViewController:didAttemptCurrentPasscode:)]) {
235381
return;
@@ -240,8 +386,83 @@ - (void)numberViewDidEnterPasscode:(NSString *)passcode
240386
self.failedPasscodeAttemptCount++;
241387
[self.numberInputView resetPasscodeAnimated:YES playImpact:YES];
242388
}
389+
else {
390+
[self transitionToState:TOPasscodeSettingsViewStateEnterNewPassword animated:YES];
391+
}
392+
}
393+
394+
- (void)didReceiveNewPasscode:(NSString *)passcode
395+
{
396+
self.potentialPasscode = passcode;
397+
[self transitionToState:TOPasscodeSettingsViewStateConfirmNewPassword animated:YES];
398+
}
399+
400+
- (void)confirmNewPasscode:(NSString *)passcode
401+
{
402+
if (![passcode isEqualToString:self.potentialPasscode]) {
403+
[self transitionToState:TOPasscodeSettingsViewStateEnterNewPassword animated:YES];
404+
self.errorLabel.hidden = NO;
405+
return;
406+
}
407+
}
408+
409+
#pragma mark - Button Callbacks -
410+
411+
- (void)optionsCodeButtonTapped:(id)sender
412+
{
413+
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
414+
UIAlertActionStyle style = UIAlertActionStyleDefault;
415+
416+
__weak typeof(self) weakSelf = self;
417+
418+
NSArray *types = @[@(TOPasscodeTypeFourDigits),
419+
@(TOPasscodeTypeSixDigits),
420+
// @(TOPasscodeTypeCustomNumeric),
421+
// @(TOPasscodeTypeCustomAlphanumeric)
422+
];
423+
424+
425+
NSArray *titles = @[NSLocalizedString(@"4-Digit Numeric Code", @""),
426+
NSLocalizedString(@"6-Digit Numeric Code", @""),
427+
NSLocalizedString(@"Custom Numeric Code", @""),
428+
NSLocalizedString(@"Custom Alphanumeric Code", @"")];
429+
430+
// Add all the buttons
431+
for (NSInteger i = 0; i < types.count; i++) {
432+
TOPasscodeType type = [types[i] integerValue];
433+
if (type == self.passcodeType) { continue; }
434+
435+
id handler = ^(UIAlertAction *action) {
436+
[weakSelf setPasscodeType:type];
437+
};
438+
[alertController addAction:[UIAlertAction actionWithTitle:titles[i] style:style handler:handler]];
439+
}
440+
441+
// Cancel button
442+
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleCancel handler:nil]];
443+
444+
alertController.modalPresentationStyle = UIModalPresentationPopover;
445+
alertController.popoverPresentationController.sourceView = self.optionsButton;
446+
alertController.popoverPresentationController.sourceRect = self.optionsButton.bounds;
447+
alertController.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionDown | UIPopoverArrowDirectionUp;
448+
[self presentViewController:alertController animated:YES completion:nil];
449+
}
450+
451+
#pragma mark - Accessors -
452+
- (void)setPasscodeType:(TOPasscodeType)passcodeType
453+
{
454+
if (_passcodeType == passcodeType) { return; }
455+
_passcodeType = passcodeType;
456+
457+
[self updateContentForState:self.state type:_passcodeType];
458+
}
459+
460+
- (void)setState:(TOPasscodeSettingsViewState)state
461+
{
462+
if (_state == state) { return; }
463+
_state = state;
243464

244-
[self updateViewsForState:self.state];
465+
[self updateContentForState:_state type:self.passcodeType];
245466
}
246467

247468
@end

0 commit comments

Comments
 (0)