TOPasscodeSettingsKeypadView.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. //
  2. // TOPasscodeSettingsKeypadView.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 "TOPasscodeSettingsKeypadView.h"
  23. #import "TOPasscodeSettingsKeypadButton.h"
  24. #import "TOPasscodeButtonLabel.h"
  25. #import "TOSettingsKeypadImage.h"
  26. const CGFloat kTOPasscodeSettingsKeypadButtonInnerSpacing = 7.0f;
  27. const CGFloat kTOPasscodeSettingsKeypadButtonOuterSpacing = 7.0f;
  28. const CGFloat kTOPasscodeSettingsKeypadCornderRadius = 12.0f;
  29. @interface TOPasscodeSettingsKeypadView ()
  30. @property (nonatomic, strong) UIView *separatorView;
  31. @property (nonatomic, strong) NSArray<TOPasscodeSettingsKeypadButton *> *keypadButtons;
  32. @property (nonatomic, strong) UIButton *deleteButton;
  33. @property (nonatomic, strong) UIImage *buttonBackgroundImage;
  34. @property (nonatomic, strong) UIImage *buttonTappedBackgroundImage;
  35. @end
  36. @implementation TOPasscodeSettingsKeypadView
  37. - (instancetype)initWithFrame:(CGRect)frame
  38. {
  39. if (self = [super initWithFrame:frame]) {
  40. [self setUp];
  41. }
  42. return self;
  43. }
  44. - (void)setUp
  45. {
  46. /* Button label styling */
  47. _keypadButtonNumberFont = [UIFont systemFontOfSize:32.0f weight:UIFontWeightRegular];
  48. _keypadButtonLetteringFont = [UIFont systemFontOfSize:11.0f weight:UIFontWeightRegular];
  49. _keypadButtonVerticalSpacing = 2.0f;
  50. _keypadButtonHorizontalSpacing = 3.0f;
  51. _keypadButtonLetteringSpacing = 2.0f;
  52. CGSize viewSize = self.frame.size;
  53. CGFloat height = 1.0f / [[UIScreen mainScreen] scale];
  54. self.separatorView = [[UIView alloc] initWithFrame:(CGRect){CGPointZero,{viewSize.width, height}}];
  55. self.separatorView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  56. [self addSubview:self.separatorView];
  57. [self setUpKeypadButtons];
  58. [self setUpDeleteButton];
  59. [self setUpDefaultValuesForStye:_style];
  60. [self applyTheme];
  61. }
  62. - (void)setUpKeypadButtons
  63. {
  64. NSInteger numberOfButtons = 10;
  65. NSArray *letteredTitles = @[@"ABC", @"DEF", @"GHI", @"JKL",
  66. @"MNO", @"PQRS", @"TUV", @"WXYZ"];
  67. NSMutableArray *buttons = [NSMutableArray arrayWithCapacity:10];
  68. for (NSInteger i = 0; i < numberOfButtons; i++) {
  69. NSInteger number = (i+1) % 10; // Wrap around 0 at the end
  70. TOPasscodeSettingsKeypadButton *button = [TOPasscodeSettingsKeypadButton button];
  71. button.buttonLabel.numberString = [NSString stringWithFormat:@"%ld", (long)number];
  72. button.bottomInset = 2.0f;
  73. button.tag = number;
  74. if (i > 0) {
  75. NSInteger j = i - 1;
  76. if (j < letteredTitles.count) {
  77. button.buttonLabel.letteringString = letteredTitles[j];
  78. }
  79. }
  80. [button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchDown];
  81. [self addSubview:button];
  82. [buttons addObject:button];
  83. }
  84. self.keypadButtons = [NSArray arrayWithArray:buttons];
  85. }
  86. - (void)setUpDeleteButton
  87. {
  88. UIImage *deleteIcon = [TOSettingsKeypadImage deleteIcon];
  89. self.deleteButton = [UIButton buttonWithType:UIButtonTypeSystem];
  90. [self.deleteButton setImage:deleteIcon forState:UIControlStateNormal];
  91. self.deleteButton.contentMode = UIViewContentModeCenter;
  92. self.deleteButton.frame = (CGRect){CGPointZero, deleteIcon.size};
  93. self.deleteButton.tintColor = [UIColor blackColor];
  94. [self.deleteButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
  95. [self addSubview:self.deleteButton];
  96. }
  97. - (void)setUpDefaultValuesForStye:(TOPasscodeSettingsViewStyle)style
  98. {
  99. BOOL isDark = style == TOPasscodeSettingsViewStyleDark;
  100. // Keypad label
  101. self.keypadButtonLabelTextColor = isDark ? [UIColor whiteColor] : [UIColor blackColor];
  102. self.keypadButtonForegroundColor = isDark ? [UIColor colorWithWhite:0.35f alpha:1.0f] : [UIColor whiteColor];
  103. self.keypadButtonTappedForegroundColor = isDark ? [UIColor colorWithWhite:0.45f alpha:1.0f] : [UIColor colorWithWhite:0.85f alpha:1.0f];
  104. // Button border color
  105. UIColor *borderColor = nil;
  106. if (isDark) {
  107. borderColor = [UIColor colorWithWhite:0.15f alpha:1.0f];
  108. }
  109. else {
  110. borderColor = [UIColor colorWithRed:166.0f/255.0f green:174.0f/255.0f blue:186.0f/255.0f alpha:1.0f];
  111. }
  112. self.keypadButtonBorderColor = borderColor;
  113. // Background Color
  114. UIColor *backgroundColor = nil;
  115. if (isDark) {
  116. backgroundColor = [UIColor colorWithWhite:0.18f alpha:1.0f];
  117. }
  118. else {
  119. backgroundColor = [UIColor colorWithRed:220.0f/255.0f green:225.0f/255.0f blue:232.0f/255.0f alpha:1.0f];
  120. }
  121. self.backgroundColor = backgroundColor;
  122. // Separator lines
  123. UIColor *separatorColor = nil;
  124. if (isDark) {
  125. separatorColor = [UIColor colorWithWhite:0.25f alpha:1.0f];
  126. }
  127. else {
  128. separatorColor = [UIColor colorWithWhite:0.7f alpha:1.0f];
  129. }
  130. self.separatorView.backgroundColor = separatorColor;
  131. self.deleteButton.tintColor = isDark ? [UIColor whiteColor] : [UIColor blackColor];
  132. }
  133. - (void)setUpImagesIfNeeded
  134. {
  135. if (self.buttonBackgroundImage && self.buttonTappedBackgroundImage) {
  136. return;
  137. }
  138. if (self.buttonBackgroundImage == nil) {
  139. self.buttonBackgroundImage = [TOSettingsKeypadImage buttonImageWithCornerRadius:kTOPasscodeSettingsKeypadCornderRadius
  140. foregroundColor:self.keypadButtonForegroundColor
  141. edgeColor:self.keypadButtonBorderColor
  142. edgeThickness:2.0f];
  143. }
  144. if (self.buttonTappedBackgroundImage == nil) {
  145. self.buttonTappedBackgroundImage = [TOSettingsKeypadImage buttonImageWithCornerRadius:kTOPasscodeSettingsKeypadCornderRadius
  146. foregroundColor:self.keypadButtonTappedForegroundColor
  147. edgeColor:self.keypadButtonBorderColor
  148. edgeThickness:2.0f];
  149. }
  150. for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) {
  151. button.buttonBackgroundImage = self.buttonBackgroundImage;
  152. button.buttonTappedBackgroundImage = self.buttonTappedBackgroundImage;
  153. }
  154. }
  155. - (void)applyTheme
  156. {
  157. for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) {
  158. button.buttonLabel.textColor = self.keypadButtonLabelTextColor;
  159. button.buttonLabel.letteringCharacterSpacing = self.keypadButtonLetteringSpacing;
  160. button.buttonLabel.letteringVerticalSpacing = self.keypadButtonVerticalSpacing;
  161. button.buttonLabel.letteringHorizontalSpacing = self.keypadButtonHorizontalSpacing;
  162. button.buttonLabel.numberLabelFont = self.keypadButtonNumberFont;
  163. button.buttonLabel.letteringLabelFont = self.keypadButtonLetteringFont;
  164. }
  165. }
  166. - (void)layoutSubviews
  167. {
  168. [super layoutSubviews];
  169. [self setUpImagesIfNeeded];
  170. CGFloat outerSpacing = kTOPasscodeSettingsKeypadButtonOuterSpacing;
  171. CGFloat innerSpacing = kTOPasscodeSettingsKeypadButtonInnerSpacing;
  172. CGSize viewSize = self.bounds.size;
  173. CGSize buttonSize = CGSizeZero;
  174. viewSize.width -= (outerSpacing * 2.0f);
  175. viewSize.height -= (outerSpacing * 2.0f);
  176. // Pull the buttons up to avoid overlapping the home indicator on iPhone X
  177. if (@available(iOS 11.0, *)) {
  178. viewSize.height -= self.safeAreaInsets.bottom;
  179. }
  180. // Four rows of three buttons
  181. buttonSize.width = floorf((viewSize.width - (innerSpacing * 2.0f)) / 3.0f);
  182. buttonSize.height = floorf((viewSize.height - (innerSpacing * 3.0f)) / 4.0f);
  183. CGPoint point = CGPointMake(outerSpacing, outerSpacing);
  184. CGRect buttonFrame = (CGRect){point, buttonSize};
  185. NSInteger i = 0;
  186. for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) {
  187. button.frame = buttonFrame;
  188. buttonFrame.origin.x += buttonFrame.size.width + innerSpacing;
  189. if (++i % 3 == 0) {
  190. buttonFrame.origin.x = outerSpacing;
  191. buttonFrame.origin.y += buttonFrame.size.height + innerSpacing;
  192. }
  193. if (button == self.keypadButtons.lastObject) {
  194. button.frame = buttonFrame;
  195. }
  196. }
  197. //Layout delete button
  198. CGSize boundsSize = self.bounds.size;
  199. // Adjust for home indicator on iPhone X
  200. if (@available(iOS 11.0, *)) {
  201. boundsSize.height -= self.safeAreaInsets.bottom;
  202. }
  203. CGRect frame = self.deleteButton.frame;
  204. frame.size = buttonSize;
  205. frame.origin.x = boundsSize.width - (outerSpacing + buttonSize.width * 0.5f);
  206. frame.origin.x -= (CGRectGetWidth(frame) * 0.5f);
  207. frame.origin.y = boundsSize.height - (outerSpacing + buttonSize.height * 0.5f);
  208. frame.origin.y -= (CGRectGetHeight(frame) * 0.5f);
  209. self.deleteButton.frame = frame;
  210. }
  211. #pragma mark - Interaction -
  212. - (void)buttonTapped:(id)sender
  213. {
  214. // Handler for the delete button
  215. if (sender == self.deleteButton) {
  216. if (self.deleteButtonTappedHandler) {
  217. self.deleteButtonTappedHandler();
  218. }
  219. return;
  220. }
  221. // Handler for the keypad buttons
  222. UIButton *button = (UIButton *)sender;
  223. NSInteger number = button.tag;
  224. [[UIDevice currentDevice] playInputClick];
  225. if (self.numberButtonTappedHandler) {
  226. self.numberButtonTappedHandler(number);
  227. }
  228. }
  229. #pragma mark - Accessors -
  230. - (void)setStyle:(TOPasscodeSettingsViewStyle)style
  231. {
  232. if (style == _style) {
  233. return;
  234. }
  235. _style = style;
  236. [self setUpDefaultValuesForStye:_style];
  237. [self applyTheme];
  238. }
  239. #pragma mark - Label Layout -
  240. - (void)setButtonLabelHorizontalLayout:(BOOL)buttonLabelHorizontalLayout
  241. {
  242. [self setButtonLabelHorizontalLayout:buttonLabelHorizontalLayout animated:NO];
  243. }
  244. - (void)setButtonLabelHorizontalLayout:(BOOL)horizontal animated:(BOOL)animated
  245. {
  246. if (horizontal == _buttonLabelHorizontalLayout) { return; }
  247. _buttonLabelHorizontalLayout = horizontal;
  248. for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) {
  249. if (!animated) {
  250. button.buttonLabel.horizontalLayout = horizontal;
  251. continue;
  252. }
  253. UIView *snapshotView = [button.buttonLabel snapshotViewAfterScreenUpdates:NO];
  254. snapshotView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
  255. [button addSubview:snapshotView];
  256. button.buttonLabel.horizontalLayout = horizontal;
  257. [button.buttonLabel setNeedsLayout];
  258. [button.buttonLabel layoutIfNeeded];
  259. [button.buttonLabel.layer removeAllAnimations];
  260. for (CALayer *sublayer in button.buttonLabel.layer.sublayers) {
  261. [sublayer removeAllAnimations];
  262. }
  263. button.buttonLabel.alpha = 0.0f;
  264. [UIView animateWithDuration:0.4f animations:^{
  265. button.buttonLabel.alpha = 1.0f;
  266. snapshotView.alpha = 0.0f;
  267. snapshotView.center = button.buttonLabel.center;
  268. } completion:^(BOOL complete) {
  269. [snapshotView removeFromSuperview];
  270. }];
  271. }
  272. }
  273. #pragma mark - Null Resettable Accessors -
  274. - (void)setKeypadButtonForegroundColor:(nullable UIColor *)keypadButtonForegroundColor
  275. {
  276. if (keypadButtonForegroundColor == _keypadButtonForegroundColor) { return; }
  277. _keypadButtonForegroundColor = keypadButtonForegroundColor;
  278. if (_keypadButtonForegroundColor == nil) {
  279. BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark;
  280. _keypadButtonForegroundColor = isDark ? [UIColor colorWithWhite:0.3f alpha:1.0f] : [UIColor whiteColor];
  281. }
  282. self.buttonBackgroundImage = nil;
  283. [self setNeedsLayout];
  284. }
  285. - (void)setKeypadButtonBorderColor:(nullable UIColor *)keypadButtonBorderColor
  286. {
  287. if (keypadButtonBorderColor == _keypadButtonBorderColor) { return; }
  288. _keypadButtonBorderColor = keypadButtonBorderColor;
  289. if (_keypadButtonBorderColor == nil) {
  290. BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark;
  291. UIColor *borderColor = nil;
  292. if (isDark) {
  293. borderColor = [UIColor colorWithWhite:0.2 alpha:1.0f];
  294. }
  295. else {
  296. borderColor = [UIColor colorWithRed:166.0f/255.0f green:174.0f/255.0f blue:186.0f/255.0f alpha:1.0f];
  297. }
  298. _keypadButtonBorderColor = borderColor;
  299. }
  300. self.buttonBackgroundImage = nil;
  301. [self setNeedsLayout];
  302. }
  303. - (void)setKeypadButtonTappedForegroundColor:(nullable UIColor *)keypadButtonTappedForegroundColor
  304. {
  305. if (keypadButtonTappedForegroundColor == _keypadButtonTappedForegroundColor) { return; }
  306. _keypadButtonTappedForegroundColor = keypadButtonTappedForegroundColor;
  307. if (_keypadButtonTappedForegroundColor == nil) {
  308. BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark;
  309. _keypadButtonTappedForegroundColor = isDark ? [UIColor colorWithWhite:0.4f alpha:1.0f] : [UIColor colorWithWhite:0.85f alpha:1.0f];
  310. }
  311. self.buttonTappedBackgroundImage = nil;
  312. [self setNeedsLayout];
  313. }
  314. - (void)setEnabled:(BOOL)enabled
  315. {
  316. _enabled = enabled;
  317. for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) {
  318. button.enabled = enabled;
  319. }
  320. self.deleteButton.enabled = enabled;
  321. }
  322. @end