TOPasscodeVariableInputView.m 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. //
  2. // TOPasscodeVariableInputView.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 "TOPasscodeVariableInputView.h"
  23. #import "TOPasscodeCircleImage.h"
  24. @interface TOPasscodeVariableInputView ()
  25. @property (nonatomic, strong) UIImage *backgroundImage; // The outline image for this view
  26. @property (nonatomic, strong) UIImage *circleImage; // The circle image representing a single character
  27. @property (nonatomic, strong) NSMutableArray<UIImageView *> *circleViews;
  28. @end
  29. @implementation TOPasscodeVariableInputView
  30. #pragma mark - Class Creation -
  31. - (instancetype)initWithFrame:(CGRect)frame
  32. {
  33. if (self = [super initWithFrame:frame]) {
  34. _outlineThickness = 1.0f;
  35. _outlineCornerRadius = 5.0f;
  36. _circleDiameter = 11.0f;
  37. _circleSpacing = 7.0f;
  38. _outlinePadding = (CGSize){10,10};
  39. _maximumVisibleLength = 12;
  40. }
  41. return self;
  42. }
  43. #pragma mark - View Setup -
  44. - (void)setUpImageForCircleViews
  45. {
  46. if (self.circleImage != nil) { return; }
  47. self.circleImage = [TOPasscodeCircleImage circleImageOfSize:_circleDiameter inset:0.0f padding:1.0f antialias:YES];
  48. for (UIImageView *circleView in self.circleViews) {
  49. circleView.image = self.circleImage;
  50. [circleView sizeToFit];
  51. }
  52. }
  53. - (void)setUpCircleViewsForLength:(NSInteger)length
  54. {
  55. // Set up the number of circle views if needed
  56. if (self.circleViews.count == length) { return; }
  57. if (self.circleViews == nil) {
  58. self.circleViews = [NSMutableArray arrayWithCapacity:_maximumVisibleLength];
  59. }
  60. // Reduce the number of views
  61. while (self.circleViews.count > length) {
  62. UIImageView *circleView = self.circleViews.lastObject;
  63. [circleView removeFromSuperview];
  64. [self.circleViews removeLastObject];
  65. }
  66. // Increase the number of views
  67. [UIView performWithoutAnimation:^{
  68. while (self.circleViews.count < length) {
  69. UIImageView *circleView = [[UIImageView alloc] initWithImage:self.circleImage];
  70. circleView.alpha = 0.0f;
  71. [self addSubview:circleView];
  72. [self.circleViews addObject:circleView];
  73. }
  74. }];
  75. }
  76. - (void)setUpBackgroundImage
  77. {
  78. if (self.backgroundImage != nil) { return; }
  79. self.backgroundImage = [[self class] backgroundImageWithThickness:_outlineThickness cornerRadius:_outlineCornerRadius];
  80. self.image = self.backgroundImage;
  81. }
  82. #pragma mark - View Layout -
  83. - (void)sizeToFit
  84. {
  85. CGRect frame = self.frame;
  86. // Calculate the width
  87. frame.size.width = self.outlineThickness * 2.0f;
  88. frame.size.width += (self.outlinePadding.width * 2.0f);
  89. frame.size.width += (self.maximumVisibleLength * (self.circleDiameter+2.0f)); // +2 for padding
  90. frame.size.width += ((self.maximumVisibleLength - 1) * self.circleSpacing);
  91. // Height
  92. frame.size.height = self.outlineThickness * 2.0f;
  93. frame.size.height += self.outlinePadding.height * 2.0f;
  94. frame.size.height += self.circleDiameter;
  95. self.frame = CGRectIntegral(frame);
  96. }
  97. - (void)layoutSubviews
  98. {
  99. [super layoutSubviews];
  100. // Genearate the background image if we don't have one yet
  101. [self setUpBackgroundImage];
  102. // Set up the circle view image
  103. [self setUpImageForCircleViews];
  104. // Set up the circle views
  105. [self setUpCircleViewsForLength:self.maximumVisibleLength];
  106. // Layout the circle views for the current length
  107. CGRect frame = CGRectZero;
  108. frame.size = self.circleImage.size;
  109. frame.origin.y = CGRectGetMidY(self.bounds) - (frame.size.height * 0.5f);
  110. frame.origin.x = self.outlinePadding.width + self.outlineThickness;
  111. for (UIImageView *circleView in self.circleViews) {
  112. circleView.frame = frame;
  113. frame.origin.x += frame.size.width + self.circleSpacing;
  114. }
  115. }
  116. #pragma mark - Accessors -
  117. - (void)setOutlineThickness:(CGFloat)outlineThickness
  118. {
  119. if (_outlineThickness == outlineThickness) { return; }
  120. _outlineThickness = outlineThickness;
  121. self.backgroundImage = nil;
  122. [self setNeedsLayout];
  123. }
  124. - (void)setOutlineCornerRadius:(CGFloat)outlineCornerRadius
  125. {
  126. if (_outlineCornerRadius == outlineCornerRadius) { return; }
  127. _outlineCornerRadius = outlineCornerRadius;
  128. self.backgroundImage = nil;
  129. [self setNeedsLayout];
  130. }
  131. - (void)setCircleDiameter:(CGFloat)circleDiameter
  132. {
  133. if (_circleDiameter == circleDiameter) { return; }
  134. _circleDiameter = circleDiameter;
  135. self.circleImage = nil;
  136. [self setUpImageForCircleViews];
  137. }
  138. - (void)setCircleSpacing:(CGFloat)circleSpacing
  139. {
  140. if (_circleSpacing == circleSpacing) { return; }
  141. _circleSpacing = circleSpacing;
  142. [self sizeToFit];
  143. [self setNeedsLayout];
  144. }
  145. - (void)setOutlinePadding:(CGSize)outlinePadding
  146. {
  147. if (CGSizeEqualToSize(outlinePadding, _outlinePadding)) { return; }
  148. _outlinePadding = outlinePadding;
  149. [self sizeToFit];
  150. [self setNeedsLayout];
  151. }
  152. - (void)setMaximumVisibleLength:(NSInteger)maximumVisibleLength
  153. {
  154. if (_maximumVisibleLength == maximumVisibleLength) { return; }
  155. _maximumVisibleLength = maximumVisibleLength;
  156. [self setUpCircleViewsForLength:maximumVisibleLength];
  157. [self sizeToFit];
  158. [self setNeedsLayout];
  159. }
  160. - (void)setLength:(NSInteger)length
  161. {
  162. [self setLength:length animated:NO];
  163. }
  164. - (void)setLength:(NSInteger)length animated:(BOOL)animated
  165. {
  166. if (length == _length) { return; }
  167. _length = length;
  168. void (^animationBlock)(void) = ^{
  169. NSInteger i = 0;
  170. for (UIImageView *circleView in self.circleViews) {
  171. circleView.alpha = i < length ? 1.0f : 0.0f;
  172. i++;
  173. }
  174. };
  175. if (!animated) {
  176. animationBlock();
  177. return;
  178. }
  179. [UIView animateWithDuration:0.4f animations:animationBlock];
  180. }
  181. #pragma mark - Image Creation -
  182. + (UIImage *)backgroundImageWithThickness:(CGFloat)thickness cornerRadius:(CGFloat)radius
  183. {
  184. CGFloat inset = thickness / 2.0f;
  185. CGFloat dimension = (radius * 2.0f) + 2.0f;
  186. CGRect frame = CGRectZero;
  187. frame.origin = CGPointMake(inset, inset);
  188. frame.size = CGSizeMake(dimension, dimension);
  189. CGSize canvasSize = frame.size;
  190. canvasSize.width += thickness;
  191. canvasSize.height += thickness;
  192. UIGraphicsBeginImageContextWithOptions(canvasSize, NO, 0.0f);
  193. {
  194. UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:radius];
  195. path.lineWidth = thickness;
  196. [[UIColor blackColor] setStroke];
  197. [path stroke];
  198. }
  199. UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  200. UIGraphicsEndImageContext();
  201. UIEdgeInsets insets = UIEdgeInsetsMake(radius+1, radius+1, radius+1, radius+1);
  202. image = [image resizableImageWithCapInsets:insets];
  203. return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  204. }
  205. @end