11
11
#import " TOPasscodeSettingsKeypadView.h"
12
12
#import " TOPasscodeSettingsWarningLabel.h"
13
13
14
- const CGFloat kTOPasscodeSettingsLabelInputSpacing = 18 .0f ;
14
+ const CGFloat kTOPasscodeSettingsLabelInputSpacing = 15 .0f ;
15
+ const CGFloat kTOPasscodeSettingsOptionsButtonOffset = 15 .0f ;
15
16
const CGFloat kTOPasscodeKeypadMaxSizeRatio = 0 .40f ;
16
- const CGFloat kTOPasscodeKeypadMinHeight = 200 .0f ;
17
+ const CGFloat kTOPasscodeKeypadMinHeight = 165 .0f ;
17
18
const CGFloat kTOPasscodeKeypadMaxHeight = 330 .0f ;
18
19
19
20
@interface TOPasscodeSettingsViewController ()
20
21
21
- // Views
22
+ @property (nonatomic , copy ) NSString *potentialPasscode;
23
+
24
+ /* Layout Calculations */
25
+ @property (nonatomic , assign ) CGFloat verticalMidPoint;
26
+
27
+ /* Views */
22
28
@property (nonatomic , strong ) UIView *containerView;
23
29
@property (nonatomic , strong ) UILabel *titleLabel;
30
+ @property (nonatomic , strong ) UILabel *errorLabel;
31
+ @property (nonatomic , strong ) UIButton *optionsButton;
24
32
@property (nonatomic , strong ) TOPasscodeNumberInputView *numberInputView;
25
33
@property (nonatomic , strong ) TOPasscodeSettingsKeypadView *keypadView;
26
34
@property (nonatomic , strong ) TOPasscodeSettingsWarningLabel *warningLabel;
@@ -29,6 +37,8 @@ @interface TOPasscodeSettingsViewController ()
29
37
30
38
@implementation TOPasscodeSettingsViewController
31
39
40
+ #pragma mark - Object Creation -
41
+
32
42
- (instancetype )initWithStyle : (TOPasscodeSettingsViewStyle)style
33
43
{
34
44
if (self = [self initWithNibName: nil bundle: nil ]) {
@@ -53,6 +63,8 @@ - (void)setUp
53
63
_failedPasscodeAttemptCount = 0 ;
54
64
}
55
65
66
+ #pragma mark - View Set-up -
67
+
56
68
- (void )viewDidLoad {
57
69
[super viewDidLoad ];
58
70
@@ -89,13 +101,31 @@ - (void)viewDidLoad {
89
101
self.keypadView .autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
90
102
[self .view addSubview: self .keypadView];
91
103
92
- // Create label view
104
+ // Create warning label view
93
105
self.warningLabel = [[TOPasscodeSettingsWarningLabel alloc ] initWithFrame: CGRectZero];
94
106
self.warningLabel .autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
95
107
self.warningLabel .hidden = YES ;
96
108
[self .warningLabel sizeToFit ];
97
109
[self .containerView addSubview: self .warningLabel];
98
110
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
+
99
129
// Add callbacks for the keypad view
100
130
self.keypadView .numberButtonTappedHandler = ^(NSInteger number) {
101
131
NSString *numberString = [NSString stringWithFormat: @" %ld " , number];
@@ -108,25 +138,13 @@ - (void)viewDidLoad {
108
138
CGRect frame = self.containerView .frame ;
109
139
frame.size .width = self.view .bounds .size .width ;
110
140
frame.size .height = CGRectGetHeight (self.titleLabel .frame ) + CGRectGetHeight (self.numberInputView .frame )
111
- + (kTOPasscodeSettingsLabelInputSpacing * 2 .0f );
141
+ + CGRectGetHeight (self. warningLabel . frame ) + (kTOPasscodeSettingsLabelInputSpacing * 2 .0f );
112
142
self.containerView .frame = CGRectIntegral (frame);
113
143
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 ;
130
148
131
149
// Apply light/dark mode
132
150
[self applyThemeForStyle: self .style];
@@ -137,20 +155,35 @@ - (void)viewWillAppear:(BOOL)animated
137
155
[super viewWillAppear: animated];
138
156
139
157
self.state = self.requireCurrentPasscode ? TOPasscodeSettingsViewStateEnterCurrentPassword : TOPasscodeSettingsViewStateEnterNewPassword;
140
- [self updateViewsForState :self .state];
158
+ [self updateContentForState :self .state type: self .passcodeType ];
141
159
}
142
160
143
- - (void )updateViewsForState : (TOPasscodeSettingsViewState)state
161
+ #pragma mark - View Update -
162
+
163
+ - (void )updateContentForState : (TOPasscodeSettingsViewState)state type : (TOPasscodeType)type
144
164
{
145
165
BOOL confirmingPasscode = state == TOPasscodeSettingsViewStateEnterCurrentPassword;
146
166
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 );
148
172
self.warningLabel .numberOfWarnings = self.failedPasscodeAttemptCount ;
149
173
150
174
CGRect frame = self.warningLabel .frame ;
151
175
frame.origin .x = (CGRectGetWidth (self.view .frame ) - frame.size .width ) * 0 .5f ;
152
176
self.warningLabel .frame = frame;
153
177
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
154
187
switch (state) {
155
188
case TOPasscodeSettingsViewStateEnterCurrentPassword:
156
189
self.titleLabel .text = NSLocalizedString(@" Enter your passcode" , @" " );
@@ -162,6 +195,72 @@ - (void)updateViewsForState:(TOPasscodeSettingsViewState)state
162
195
self.titleLabel .text = NSLocalizedString(@" Confirm new passcode" , @" " );
163
196
break ;
164
197
}
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];
165
264
}
166
265
167
266
- (void )viewDidLayoutSubviews
@@ -187,9 +286,39 @@ - (void)viewDidLayoutSubviews
187
286
188
287
// Layout the container view
189
288
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 ;
191
290
frame.origin .y += topContentHeight;
192
291
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);
193
322
}
194
323
195
324
- (void )applyThemeForStyle : (TOPasscodeSettingsViewStyle)style
@@ -230,6 +359,23 @@ - (void)applyThemeForStyle:(TOPasscodeSettingsViewStyle)style
230
359
231
360
#pragma mark - Data Management -
232
361
- (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
233
379
{
234
380
if (![self .delegate respondsToSelector: @selector (passcodeSettingsViewController:didAttemptCurrentPasscode: )]) {
235
381
return ;
@@ -240,8 +386,83 @@ - (void)numberViewDidEnterPasscode:(NSString *)passcode
240
386
self.failedPasscodeAttemptCount ++;
241
387
[self .numberInputView resetPasscodeAnimated: YES playImpact: YES ];
242
388
}
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;
243
464
244
- [self updateViewsForState: self .state ];
465
+ [self updateContentForState: _state type: self .passcodeType ];
245
466
}
246
467
247
468
@end
0 commit comments