TOPasscodeSettingsViewController.m 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. //
  2. // TOPasscodeSettingsViewController.m
  3. //
  4. // Copyright 2017 Timothy Oliver. All rights reserved.
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to
  8. // deal in the Software without restriction, including without limitation the
  9. // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
  10. // sell copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  17. // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  20. // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
  21. // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. #import "TOPasscodeSettingsViewController.h"
  23. #import "TOPasscodeInputField.h"
  24. #import "TOPasscodeSettingsKeypadView.h"
  25. #import "TOPasscodeSettingsWarningLabel.h"
  26. const CGFloat kTOPasscodeSettingsLabelInputSpacing = 15.0f;
  27. const CGFloat kTOPasscodeSettingsOptionsButtonOffset = 15.0f;
  28. const CGFloat kTOPasscodeKeypadMaxSizeRatio = 0.40f;
  29. const CGFloat kTOPasscodeKeypadMinHeight = 185.0f; // was 165
  30. const CGFloat kTOPasscodeKeypadMaxHeight = 330.0f;
  31. @interface TOPasscodeSettingsViewController ()
  32. @property (nonatomic, copy) NSString *potentialPasscode;
  33. /* Layout Calculations */
  34. @property (nonatomic, assign) CGFloat verticalMidPoint;
  35. @property (nonatomic, assign) CGRect keyboardFrame;
  36. @property (nonatomic, readonly) CGRect contentOverlapFrame; // Either the keypad or the system keyboard
  37. /* Views */
  38. @property (nonatomic, strong) UIView *containerView;
  39. @property (nonatomic, strong) UILabel *titleLabel;
  40. @property (nonatomic, strong) UILabel *errorLabel;
  41. @property (nonatomic, strong) UIButton *optionsButton;
  42. @property (nonatomic, strong) TOPasscodeInputField *inputField;
  43. @property (nonatomic, strong) TOPasscodeSettingsKeypadView *keypadView;
  44. @property (nonatomic, strong) TOPasscodeSettingsWarningLabel *warningLabel;
  45. /* Bar Items */
  46. @property (nonatomic, strong) UIBarButtonItem *nextBarButtonItem;
  47. @property (nonatomic, strong) UIBarButtonItem *doneBarButtonItem;
  48. /* Style */
  49. @property (nonatomic, assign) TOPasscodeSettingsViewStyle style;
  50. @end
  51. @implementation TOPasscodeSettingsViewController
  52. #pragma mark - Object Creation -
  53. - (instancetype)init
  54. {
  55. if (self = [self initWithNibName:nil bundle:nil]) {
  56. [self setUp];
  57. }
  58. return self;
  59. }
  60. - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
  61. {
  62. if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
  63. [self setUp];
  64. }
  65. return self;
  66. }
  67. - (void)setUp
  68. {
  69. if (@available(iOS 13.0, *)) {
  70. if ([self.traitCollection userInterfaceStyle] == UIUserInterfaceStyleDark) {
  71. self.style = TOPasscodeSettingsViewStyleDark;
  72. } else {
  73. self.style = TOPasscodeSettingsViewStyleLight;
  74. }
  75. } else {
  76. self.style = TOPasscodeSettingsViewStyleLight;
  77. }
  78. [self applyThemeForStyle:_style];
  79. _failedPasscodeAttemptCount = 0;
  80. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
  81. }
  82. - (void)dealloc
  83. {
  84. [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil];
  85. }
  86. #pragma mark - View Set-up -
  87. - (void)viewDidLoad {
  88. [super viewDidLoad];
  89. __weak typeof(self) weakSelf = self;
  90. self.title = NSLocalizedString(@"Enter Passcode", @"");
  91. // Create container view
  92. self.containerView = [[UIView alloc] initWithFrame:CGRectZero];
  93. self.containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin
  94. | UIViewAutoresizingFlexibleBottomMargin;
  95. [self.view addSubview:self.containerView];
  96. // Create title label
  97. self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
  98. self.titleLabel.font = [UIFont systemFontOfSize:17.0f];
  99. self.titleLabel.textAlignment = NSTextAlignmentCenter;
  100. self.titleLabel.textColor = [UIColor blackColor];
  101. self.titleLabel.text = @"Enter your passcode";
  102. self.titleLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
  103. [self.titleLabel sizeToFit];
  104. [self.containerView addSubview:self.titleLabel];
  105. // Create number view
  106. self.inputField = [[TOPasscodeInputField alloc] init];
  107. self.inputField.tintColor = [UIColor blackColor];
  108. self.inputField.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
  109. self.inputField.passcodeCompletedHandler = ^(NSString *passcode) { [weakSelf inputViewDidCompletePasscode:passcode]; };
  110. [self.inputField sizeToFit];
  111. [self.containerView addSubview:self.inputField];
  112. // Create keypad view
  113. self.keypadView = [[TOPasscodeSettingsKeypadView alloc] initWithFrame:CGRectZero];
  114. self.keypadView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
  115. [self.view addSubview:self.keypadView];
  116. // Create warning label view
  117. self.warningLabel = [[TOPasscodeSettingsWarningLabel alloc] initWithFrame:CGRectZero];
  118. self.warningLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
  119. self.warningLabel.hidden = YES;
  120. [self.warningLabel sizeToFit];
  121. [self.containerView addSubview:self.warningLabel];
  122. // Create error label view
  123. self.errorLabel = [[UILabel alloc] initWithFrame:CGRectZero];
  124. self.errorLabel.text = NSLocalizedString(@"Passcodes didn't match. Try again.", @"");
  125. self.errorLabel.textAlignment = NSTextAlignmentCenter;
  126. self.errorLabel.font = [UIFont systemFontOfSize:15.0f];
  127. self.errorLabel.numberOfLines = 0;
  128. self.errorLabel.hidden = YES;
  129. [self.errorLabel sizeToFit];
  130. [self.containerView addSubview:self.errorLabel];
  131. // Create Options button
  132. self.optionsButton = [UIButton buttonWithType:UIButtonTypeSystem];
  133. [self.optionsButton setTitle:NSLocalizedString(@"Passcode Options", @"") forState:UIControlStateNormal];
  134. self.optionsButton.titleLabel.font = [UIFont systemFontOfSize:15.0f];
  135. [self.optionsButton sizeToFit];
  136. self.optionsButton.hidden = _hideOptionsButton;
  137. [self.optionsButton addTarget:self action:@selector(optionsCodeButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
  138. [self.view addSubview:self.optionsButton];
  139. // Add callbacks for the keypad view
  140. self.keypadView.numberButtonTappedHandler = ^(NSInteger number) {
  141. NSString *numberString = [NSString stringWithFormat:@"%ld", (long)number];
  142. [weakSelf.inputField appendPasscodeCharacters:numberString animated:NO];
  143. };
  144. self.keypadView.deleteButtonTappedHandler = ^{ [weakSelf.inputField deletePasscodeCharactersOfCount:1 animated:NO]; };
  145. // Set height of the container view (This will never change)
  146. CGRect frame = self.containerView.frame;
  147. frame.size.width = self.view.bounds.size.width;
  148. frame.size.height = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.inputField.frame)
  149. + CGRectGetHeight(self.warningLabel.frame) + (kTOPasscodeSettingsLabelInputSpacing * 2.0f);
  150. self.containerView.frame = CGRectIntegral(frame);
  151. //Work out the vertical offset of the container view assuming the warning label doesn't count
  152. self.verticalMidPoint = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.inputField.frame)
  153. + kTOPasscodeSettingsLabelInputSpacing;
  154. self.verticalMidPoint *= 0.5f;
  155. // Bar button items
  156. self.nextBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Next", @"") style:UIBarButtonItemStylePlain target:self action:@selector(nextButtonTapped:)];
  157. self.doneBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(doneButtonTapped:)];
  158. // Apply light/dark mode
  159. [self applyThemeForStyle:self.style];
  160. }
  161. - (void)viewWillAppear:(BOOL)animated
  162. {
  163. [super viewWillAppear:animated];
  164. self.state = self.requireCurrentPasscode ? TOPasscodeSettingsViewStateEnterCurrentPasscode : TOPasscodeSettingsViewStateEnterNewPasscode;
  165. [self updateContentForState:self.state type:self.passcodeType animated:NO];
  166. }
  167. #pragma mark - View Update -
  168. - (void)updateContentForState:(TOPasscodeSettingsViewState)state type:(TOPasscodeType)type animated:(BOOL)animated
  169. {
  170. BOOL variableSizePasscode = (type >= TOPasscodeTypeCustomNumeric);
  171. // Update the visibility of the options button
  172. if (_hideOptionsButton) {
  173. self.optionsButton.hidden = YES;
  174. } else {
  175. self.optionsButton.hidden = !(state == TOPasscodeSettingsViewStateEnterNewPasscode);
  176. }
  177. // Clear the input view
  178. self.inputField.passcode = nil;
  179. // Disable the input view
  180. self.inputField.enabled = NO;
  181. //Update the warning label
  182. [self updateWarningLabelForState:state];
  183. // Change the input view if needed
  184. if (!variableSizePasscode) {
  185. self.inputField.style = TOPasscodeInputFieldStyleFixed;
  186. self.inputField.fixedInputView.length = (self.passcodeType == TOPasscodeTypeSixDigits) ? 6 : 4;
  187. }
  188. else {
  189. self.inputField.style = TOPasscodeInputFieldStyleVariable;
  190. }
  191. // Update text depending on state
  192. switch (state) {
  193. case TOPasscodeSettingsViewStateEnterCurrentPasscode:
  194. self.titleLabel.text = NSLocalizedString(@"Enter your passcode", @"");
  195. self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.nextBarButtonItem : nil;
  196. if (@available(iOS 9.0, *)) {
  197. self.inputField.returnKeyType = UIReturnKeyContinue;
  198. }
  199. else {
  200. self.inputField.returnKeyType = UIReturnKeyNext;
  201. }
  202. break;
  203. case TOPasscodeSettingsViewStateEnterNewPasscode:
  204. self.titleLabel.text = NSLocalizedString(@"Enter a new passcode", @"");
  205. self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.nextBarButtonItem : nil;
  206. if (@available(iOS 9.0, *)) {
  207. self.inputField.returnKeyType = UIReturnKeyContinue;
  208. }
  209. else {
  210. self.inputField.returnKeyType = UIReturnKeyNext;
  211. }
  212. break;
  213. case TOPasscodeSettingsViewStateConfirmNewPasscode:
  214. self.titleLabel.text = NSLocalizedString(@"Confirm new passcode", @"");
  215. self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.doneBarButtonItem : nil;
  216. self.inputField.returnKeyType = UIReturnKeyDone;
  217. break;
  218. }
  219. CGRect frame = CGRectZero;
  220. // Reload the 'Done' button
  221. [self.inputField reloadInputViews];
  222. // Resize text label to fit new text
  223. [self.titleLabel sizeToFit];
  224. frame = self.titleLabel.frame;
  225. frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f;
  226. self.titleLabel.frame = CGRectIntegral(frame);
  227. // Resize passcode view
  228. [self.inputField sizeToFit];
  229. frame = self.inputField.frame;
  230. frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f;
  231. self.inputField.frame = CGRectIntegral(frame);
  232. // If we're the alphanumeric type, present the keyboard
  233. if (type == TOPasscodeTypeCustomAlphanumeric) {
  234. self.inputField.enabled = YES;
  235. [self.inputField becomeFirstResponder];
  236. }
  237. else {
  238. if (self.inputField.isFirstResponder) {
  239. [self.inputField resignFirstResponder];
  240. }
  241. }
  242. // If not animated, force a blanket re-layout
  243. if (!animated) {
  244. [self viewDidLayoutSubviews];
  245. return;
  246. }
  247. // If animated, perform the animation
  248. [UIView animateWithDuration:0.3f animations:^{
  249. [self viewDidLayoutSubviews];
  250. }];
  251. }
  252. - (void)updateWarningLabelForState:(TOPasscodeSettingsViewState)state
  253. {
  254. BOOL confirmingPasscode = state == TOPasscodeSettingsViewStateEnterCurrentPasscode;
  255. // Update the warning label
  256. self.warningLabel.hidden = !(confirmingPasscode && self.failedPasscodeAttemptCount > 0);
  257. self.warningLabel.numberOfWarnings = self.failedPasscodeAttemptCount;
  258. CGRect frame = self.warningLabel.frame;
  259. frame.origin.x = (CGRectGetWidth(self.view.frame) - frame.size.width) * 0.5f;
  260. self.warningLabel.frame = frame;
  261. }
  262. - (void)transitionToState:(TOPasscodeSettingsViewState)state animated:(BOOL)animated
  263. {
  264. // Preserve the current view state
  265. UIView *snapshot = nil;
  266. BOOL reverseDirection = state < self.state;
  267. // If animated, take a snapshot of the current container view
  268. if (animated) {
  269. snapshot = [self.containerView snapshotViewAfterScreenUpdates:NO];
  270. snapshot.frame = self.containerView.frame;
  271. [self.view addSubview:snapshot];
  272. }
  273. self.errorLabel.hidden = YES;
  274. // Update the layout for the new state
  275. self.state = state;
  276. // Cancel out now if we're not animating
  277. if (!animated) {
  278. return;
  279. }
  280. // Place the live container off screen to the right
  281. CGFloat multiplier = reverseDirection ? -1.0f : 1.0f;
  282. self.containerView.frame = CGRectOffset(self.containerView.frame, self.view.frame.size.width * multiplier, 0.0f);
  283. // Update the options button alpha depending on transition state
  284. self.optionsButton.hidden = _hideOptionsButton;
  285. self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPasscode) ? 0.0f : 1.0f;
  286. // Perform an animation where the snapshot slides off, and the new container slides in
  287. id animationBlock = ^{
  288. snapshot.frame = CGRectOffset(snapshot.frame, -self.view.frame.size.width * multiplier, 0.0f);
  289. self.containerView.frame = CGRectOffset(self.containerView.frame, -self.view.frame.size.width * multiplier, 0.0f);
  290. self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPasscode) ? 1.0f : 0.0f;
  291. };
  292. // Clean up by removing the snapshot view
  293. id completionBlock = ^(BOOL complete) {
  294. [snapshot removeFromSuperview];
  295. };
  296. // Perform the animation
  297. [UIView animateWithDuration:0.4f
  298. delay:0.0f
  299. usingSpringWithDamping:1.0f
  300. initialSpringVelocity:0.7f
  301. options:0
  302. animations:animationBlock
  303. completion:completionBlock];
  304. }
  305. - (void)viewDidLayoutSubviews
  306. {
  307. [super viewDidLayoutSubviews];
  308. CGSize viewSize = self.view.bounds.size;
  309. // Layout the keypad view
  310. CGRect frame = self.keypadView.frame;
  311. frame.size.height = viewSize.height * kTOPasscodeKeypadMaxSizeRatio;
  312. frame.size.height = MAX(frame.size.height, kTOPasscodeKeypadMinHeight);
  313. frame.size.height = MIN(frame.size.height, kTOPasscodeKeypadMaxHeight);
  314. frame.size.width = viewSize.width;
  315. frame.origin.y = viewSize.height;
  316. if (self.passcodeType != TOPasscodeTypeCustomAlphanumeric) {
  317. frame.origin.y -= frame.size.height;
  318. }
  319. self.keypadView.frame = CGRectIntegral(frame);
  320. BOOL horizontalLayout = frame.size.height < kTOPasscodeKeypadMinHeight + FLT_EPSILON;
  321. BOOL animated = ([self.view.layer animationForKey:@"bounds.size"] != nil);
  322. [self.keypadView setButtonLabelHorizontalLayout:horizontalLayout animated:animated];
  323. CGFloat topContentHeight = self.topLayoutGuide.length;
  324. // Layout the container view
  325. frame = self.containerView.frame;
  326. frame.origin.y = (((viewSize.height - (topContentHeight + self.contentOverlapFrame.size.height))) * 0.5f) - self.verticalMidPoint;
  327. frame.origin.y += topContentHeight;
  328. self.containerView.frame = CGRectIntegral(frame);
  329. // Layout the passcode options button
  330. frame = self.optionsButton.frame;
  331. frame.origin.y = CGRectGetMinY(self.contentOverlapFrame) - kTOPasscodeSettingsOptionsButtonOffset - CGRectGetHeight(frame);
  332. frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f;
  333. self.optionsButton.frame = frame;
  334. // Set frame of title label
  335. frame = self.titleLabel.frame;
  336. frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f;
  337. self.titleLabel.frame = CGRectIntegral(frame);
  338. // Set frame of number pad
  339. frame = self.inputField.frame;
  340. frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f;
  341. frame.origin.y = (CGRectGetHeight(self.titleLabel.frame) + kTOPasscodeSettingsLabelInputSpacing);
  342. self.inputField.frame = CGRectIntegral(frame);
  343. // Set the frame for the warning view
  344. frame = self.warningLabel.frame;
  345. frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f;
  346. frame.origin.y = CGRectGetMaxY(self.inputField.frame) + kTOPasscodeSettingsLabelInputSpacing;
  347. self.warningLabel.frame = CGRectIntegral(frame);
  348. // Set the frame of the error view
  349. frame = self.errorLabel.frame;
  350. frame.size = [self.errorLabel sizeThatFits:CGSizeMake(300.0f, CGFLOAT_MAX)];
  351. frame.origin.y = CGRectGetMaxY(self.inputField.frame) + kTOPasscodeSettingsLabelInputSpacing;
  352. frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f;
  353. self.errorLabel.frame = CGRectIntegral(frame);
  354. }
  355. - (void)applyThemeForStyle:(TOPasscodeSettingsViewStyle)style
  356. {
  357. BOOL isDark = (style == TOPasscodeSettingsViewStyleDark);
  358. // Set background color
  359. UIColor *backgroundColor;
  360. if (isDark) {
  361. backgroundColor = [UIColor colorWithWhite:0.15f alpha:1.0f];
  362. }
  363. else {
  364. backgroundColor = [UIColor colorWithRed:235.0f/255.0f green:235.0f/255.0f blue:241.0f/255.0f alpha:1.0f];
  365. }
  366. self.view.backgroundColor = backgroundColor;
  367. // Set the style of the keypad view
  368. self.keypadView.style = style;
  369. // Set the color for the input content
  370. UIColor *inputColor = isDark ? [UIColor whiteColor] : [UIColor blackColor];
  371. // Set the label style
  372. self.titleLabel.textColor = inputColor;
  373. // Set the number input tint
  374. self.inputField.tintColor = inputColor;
  375. // Set the tint color of the incorrect warning label
  376. UIColor *warningColor = nil;
  377. if (isDark) {
  378. warningColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f];
  379. }
  380. else {
  381. warningColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f];
  382. }
  383. }
  384. #pragma mark - Data Management -
  385. - (void)inputViewDidCompletePasscode:(NSString *)passcode
  386. {
  387. switch (self.state) {
  388. case TOPasscodeSettingsViewStateEnterCurrentPasscode:
  389. [self validateCurrentPasscodeAttemptWithPasscode:passcode];
  390. break;
  391. case TOPasscodeSettingsViewStateEnterNewPasscode:
  392. [self didReceiveNewPasscode:passcode];
  393. break;
  394. case TOPasscodeSettingsViewStateConfirmNewPasscode:
  395. [self confirmNewPasscode:passcode];
  396. break;
  397. }
  398. }
  399. - (void)validateCurrentPasscodeAttemptWithPasscode:(NSString *)passcode
  400. {
  401. if (![self.delegate respondsToSelector:@selector(passcodeSettingsViewController:didAttemptCurrentPasscode:)]) {
  402. return;
  403. }
  404. BOOL correct = [self.delegate passcodeSettingsViewController:self didAttemptCurrentPasscode:passcode];
  405. if (!correct) {
  406. [self.inputField resetPasscodeAnimated:YES playImpact:YES];
  407. self.failedPasscodeAttemptCount++;
  408. }
  409. else {
  410. [self transitionToState:TOPasscodeSettingsViewStateEnterNewPasscode animated:YES];
  411. }
  412. }
  413. - (void)didReceiveNewPasscode:(NSString *)passcode
  414. {
  415. self.potentialPasscode = passcode;
  416. [self transitionToState:TOPasscodeSettingsViewStateConfirmNewPasscode animated:YES];
  417. }
  418. - (void)confirmNewPasscode:(NSString *)passcode
  419. {
  420. if (![passcode isEqualToString:self.potentialPasscode]) {
  421. [self transitionToState:TOPasscodeSettingsViewStateEnterNewPasscode animated:YES];
  422. self.errorLabel.hidden = NO;
  423. return;
  424. }
  425. if (![self.delegate respondsToSelector:@selector(passcodeSettingsViewController:didChangeToNewPasscode:ofType:)]) {
  426. return;
  427. }
  428. [self.delegate passcodeSettingsViewController:self didChangeToNewPasscode:self.potentialPasscode ofType:self.passcodeType];
  429. }
  430. #pragma mark - System Keyboard Handling -
  431. - (void)keyboardWillChangeFrame:(NSNotification *)notification
  432. {
  433. self.keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
  434. [self viewDidLayoutSubviews];
  435. }
  436. - (CGRect)contentOverlapFrame
  437. {
  438. if (self.passcodeType < TOPasscodeTypeCustomAlphanumeric) {
  439. return self.keypadView.frame;
  440. }
  441. // Work out where our view is in relation to the screen
  442. UIWindow *window = self.view.window;
  443. CGRect viewFrame = [self.view.superview convertRect:self.view.frame toView:window];
  444. CGFloat overlap = CGRectGetMaxY(viewFrame) - CGRectGetMinY(self.keyboardFrame);
  445. CGRect overlapFrame = self.keyboardFrame;
  446. overlapFrame.origin.y = MIN(viewFrame.size.height - overlap, viewFrame.size.height);
  447. overlapFrame.size.height = MAX(overlap, 0.0f);
  448. return overlapFrame;
  449. }
  450. #pragma mark - Button Callbacks -
  451. - (void)optionsCodeButtonTapped:(id)sender
  452. {
  453. UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
  454. UIAlertActionStyle style = UIAlertActionStyleDefault;
  455. __weak typeof(self) weakSelf = self;
  456. NSArray *types = @[@(TOPasscodeTypeFourDigits),
  457. @(TOPasscodeTypeSixDigits),
  458. @(TOPasscodeTypeCustomNumeric),
  459. @(TOPasscodeTypeCustomAlphanumeric)
  460. ];
  461. NSArray *titles = @[NSLocalizedString(@"4-Digit Numeric Code", @""),
  462. NSLocalizedString(@"6-Digit Numeric Code", @""),
  463. NSLocalizedString(@"Custom Numeric Code", @""),
  464. NSLocalizedString(@"Custom Alphanumeric Code", @"")];
  465. // Add all the buttons
  466. for (NSInteger i = 0; i < types.count; i++) {
  467. TOPasscodeType type = [types[i] integerValue];
  468. if (type == self.passcodeType) { continue; }
  469. id handler = ^(UIAlertAction *action) {
  470. [weakSelf setPasscodeType:type];
  471. };
  472. [alertController addAction:[UIAlertAction actionWithTitle:titles[i] style:style handler:handler]];
  473. }
  474. // Cancel button
  475. [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleCancel handler:nil]];
  476. alertController.modalPresentationStyle = UIModalPresentationPopover;
  477. alertController.popoverPresentationController.sourceView = self.optionsButton;
  478. alertController.popoverPresentationController.sourceRect = self.optionsButton.bounds;
  479. alertController.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionDown | UIPopoverArrowDirectionUp;
  480. [self presentViewController:alertController animated:YES completion:nil];
  481. }
  482. - (void)nextButtonTapped:(id)sender
  483. {
  484. [self inputViewDidCompletePasscode:self.inputField.passcode];
  485. }
  486. - (void)doneButtonTapped:(id)sender
  487. {
  488. [self inputViewDidCompletePasscode:self.inputField.passcode];
  489. }
  490. #pragma mark - Accessors -
  491. - (void)setPasscodeType:(TOPasscodeType)passcodeType
  492. {
  493. [self setPasscodeType:passcodeType animated:NO];
  494. }
  495. - (void)setPasscodeType:(TOPasscodeType)passcodeType animated:(BOOL)animated
  496. {
  497. if (_passcodeType == passcodeType) { return; }
  498. _passcodeType = passcodeType;
  499. [self updateContentForState:self.state type:_passcodeType animated:animated];
  500. }
  501. - (void)setState:(TOPasscodeSettingsViewState)state
  502. {
  503. if (_state == state) { return; }
  504. _state = state;
  505. [self updateContentForState:_state type:self.passcodeType animated:NO];
  506. }
  507. - (void)setFailedPasscodeAttemptCount:(NSInteger)failedPasscodeAttemptCount
  508. {
  509. if (_failedPasscodeAttemptCount == failedPasscodeAttemptCount) { return; }
  510. _failedPasscodeAttemptCount = failedPasscodeAttemptCount;
  511. [self updateWarningLabelForState:self.state];
  512. }
  513. @end