123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- //
- // TOPasscodeInputField.m
- //
- // Copyright 2017 Timothy Oliver. All rights reserved.
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to
- // deal in the Software without restriction, including without limitation the
- // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- // sell copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
- // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
- // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
- // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- #import "TOPasscodeInputField.h"
- #import "TOPasscodeVariableInputView.h"
- #import "TOPasscodeFixedInputView.h"
- #import <AudioToolbox/AudioToolbox.h>
- @interface TOPasscodeInputField ()
- // Convenience getters
- @property (nonatomic, readonly) UIView *inputField; // Returns whichever input field is currently visible
- @property (nonatomic, readonly) NSInteger maximumPasscodeLength; // The mamximum number of characters allowed (0 if uncapped)
- @property (nonatomic, strong, readwrite) TOPasscodeFixedInputView *fixedInputView;
- @property (nonatomic, strong, readwrite) TOPasscodeVariableInputView *variableInputView;
- @property (nonatomic, strong, readwrite) UIButton *submitButton;
- @property (nonatomic, strong, readwrite) UIVisualEffectView *visualEffectView;
- @end
- @implementation TOPasscodeInputField
- #pragma mark - View Set-up -
- - (instancetype)initWithFrame:(CGRect)frame
- {
- if (self = [super initWithFrame:frame]) {
- [self setUp];
- [self setUpForStyle:TOPasscodeInputFieldStyleFixed];
- }
- return self;
- }
- - (instancetype)initWithStyle:(TOPasscodeInputFieldStyle)style
- {
- if (self = [self initWithFrame:CGRectZero]) {
- _style = style;
- [self setUp];
- [self setUpForStyle:style];
- }
- return self;
- }
- - (void)setUp
- {
- self.backgroundColor = [UIColor clearColor];
- _submitButtonSpacing = 4.0f;
- _submitButtonVerticalSpacing = 5.0f;
- _visualEffectView = [[UIVisualEffectView alloc] initWithEffect:nil];
- [self addSubview:_visualEffectView];
- }
- - (void)setUpForStyle:(TOPasscodeInputFieldStyle)style
- {
- if (self.inputField) {
- [self.inputField removeFromSuperview];
- self.variableInputView = nil;
- self.fixedInputView = nil;
- }
- if (style == TOPasscodeInputFieldStyleVariable) {
- self.variableInputView = [[TOPasscodeVariableInputView alloc] init];
- [self.visualEffectView.contentView addSubview:self.variableInputView];
- }
- else {
- self.fixedInputView = [[TOPasscodeFixedInputView alloc] init];
- [self.visualEffectView.contentView addSubview:self.fixedInputView];
- }
- // Set the frame for the currently visible input view
- [self.inputField sizeToFit];
- // Size this view to match
- [self sizeToFit];
- }
- #pragma mark - View Layout -
- - (void)sizeToFit
- {
- // Resize the view to encompass the current input view
- CGRect frame = self.frame;
- [self.inputField sizeToFit];
- frame.size = self.inputField.frame.size;
- if (self.horizontalLayout) {
- frame.size.height += self.submitButtonVerticalSpacing + CGRectGetHeight(self.submitButton.frame);
- }
- self.frame = CGRectIntegral(frame);
- }
- - (void)layoutSubviews
- {
- [super layoutSubviews];
- self.visualEffectView.frame = self.inputField.bounds;
- if (!self.submitButton) { return; }
- [self.submitButton sizeToFit];
- [self bringSubviewToFront:self.submitButton];
- CGRect frame = self.submitButton.frame;
- if (!self.horizontalLayout) {
- frame.origin.x = CGRectGetMaxX(self.bounds) + self.submitButtonSpacing;
- frame.origin.y = (CGRectGetHeight(self.bounds) - CGRectGetHeight(frame)) * 0.5f;
- }
- else {
- frame.origin.x = (CGRectGetWidth(self.frame) - frame.size.width) * 0.5f;
- frame.origin.y = CGRectGetMaxY(self.inputField.frame) + self.submitButtonVerticalSpacing;
- }
- self.submitButton.frame = CGRectIntegral(frame);
- }
- #pragma mark - Interaction -
- - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- {
- [super touchesBegan:touches withEvent:event];
- if (!self.enabled) { return; }
- self.contentAlpha = 0.5f;
- }
- - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
- {
- [super touchesCancelled:touches withEvent:event];
- if (!self.enabled) { return; }
- [UIView animateWithDuration:0.3f animations:^{
- self.contentAlpha = 1.0f;
- }];
- [self becomeFirstResponder];
- }
- - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
- {
- CGRect frame = self.bounds;
- frame.size.width += self.submitButton.frame.size.width + (self.submitButtonSpacing * 2.0f);
- frame.size.height += self.submitButtonVerticalSpacing;
- if (CGRectContainsPoint(frame, point)) {
- return YES;
- }
- return NO;
- }
- - (id)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
- if ([[super hitTest:point withEvent:event] isEqual:self.submitButton]) {
- if (CGRectContainsPoint(self.submitButton.frame, point)) {
- return self.submitButton;
- } else {
- return self;
- }
- }
- return [super hitTest:point withEvent:event];
- }
- #pragma mark - Text Input Protocol -
- - (BOOL)canBecomeFirstResponder { return self.enabled; }
- - (BOOL)hasText { return self.passcode.length > 0; }
- - (void)insertText:(NSString *)text
- {
- if ([text isEqualToString:@"\n"]) {
- if (self.passcodeCompletedHandler) { self.passcodeCompletedHandler(self.passcode); }
- return;
- }
- [self appendPasscodeCharacters:text animated:NO];
- }
- - (void)deleteBackward
- {
- [self deletePasscodeCharactersOfCount:1 animated:YES];
- }
- - (UIKeyboardType)keyboardType { return UIKeyboardTypeASCIICapable; }
- - (UITextAutocorrectionType)autocorrectionType { return UITextAutocorrectionTypeNo; }
- - (UIReturnKeyType)returnKeyType { return UIReturnKeyGo; }
- - (BOOL)enablesReturnKeyAutomatically { return YES; }
- #pragma mark - Text Input -
- - (void)setPasscode:(NSString *)passcode animated:(BOOL)animated
- {
- if (passcode == self.passcode) { return; }
- _passcode = passcode;
- BOOL passcodeIsComplete = NO;
- if (self.fixedInputView) {
- [self.fixedInputView setHighlightedLength:_passcode.length animated:animated];
- passcodeIsComplete = _passcode.length >= self.maximumPasscodeLength;
- }
- else {
- [self.variableInputView setLength:_passcode.length animated:animated];
- }
- if (self.submitButton) {
- self.submitButton.hidden = (_passcode.length == 0);
- [self bringSubviewToFront:self.submitButton];
- }
- if (passcodeIsComplete && self.passcodeCompletedHandler) {
- self.passcodeCompletedHandler(_passcode);
- }
- [self reloadInputViews];
- }
- - (void)appendPasscodeCharacters:(NSString *)characters animated:(BOOL)animated
- {
- if (characters == nil) { return; }
- if (self.maximumPasscodeLength > 0 && self.passcode.length >= self.maximumPasscodeLength) { return; }
- if (_passcode == nil) { _passcode = @""; }
- [self setPasscode:[_passcode stringByAppendingString:characters] animated:animated];
- }
- - (void)deletePasscodeCharactersOfCount:(NSInteger)deleteCount animated:(BOOL)animated
- {
- if (deleteCount <= 0 || self.passcode.length <= 0) { return; }
- [self setPasscode:[self.passcode substringToIndex:(self.passcode.length - 1)] animated:animated];
- }
- - (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact
- {
- [self setPasscode:nil animated:animated];
- // Play a negative impact effect
- if (@available(iOS 9.0, *)) {
- // https://stackoverflow.com/questions/41444274/how-to-check-if-haptic-engine-uifeedbackgenerator-is-supported
- if (impact) { AudioServicesPlaySystemSoundWithCompletion(1521, nil); }
- }
- if (!animated) { return; }
- CGPoint center = self.center;
- CGPoint offset = center;
- offset.x -= self.frame.size.width * 0.3f;
- // Play the view sliding out and then springing back in
- id completionBlock = ^(BOOL finished) {
- [UIView animateWithDuration:1.0f
- delay:0.0f
- usingSpringWithDamping:0.15f
- initialSpringVelocity:10.0f
- options:0 animations:^{
- self.center = center;
- }completion:nil];
- };
- [UIView animateWithDuration:0.05f animations:^{
- self.center = offset;
- }completion:completionBlock];
- if (!self.submitButton) { return; }
- [UIView animateWithDuration:0.7f animations:^{
- self.submitButton.alpha = 0.0f;
- } completion:^(BOOL complete) {
- self.submitButton.alpha = 1.0f;
- self.submitButton.hidden = YES;
- }];
- }
- #pragma mark - Button Callbacks -
- - (void)submitButtonTapped:(id)sender
- {
- if (self.passcodeCompletedHandler) {
- self.passcodeCompletedHandler(self.passcode);
- }
- }
- #pragma mark - Private Accessors -
- - (UIView *)inputField
- {
- if (self.fixedInputView) {
- return (UIView *)self.fixedInputView;
- }
- return (UIView *)self.variableInputView;
- }
- - (NSInteger)maximumPasscodeLength
- {
- if (self.style == TOPasscodeInputFieldStyleFixed) {
- return self.fixedInputView.length;
- }
- return 0;
- }
- #pragma mark - Public Accessors -
- - (void)setShowSubmitButton:(BOOL)showSubmitButton
- {
- if (_showSubmitButton == showSubmitButton) {
- return;
- }
- _showSubmitButton = showSubmitButton;
- if (!_showSubmitButton) {
- [self.submitButton removeFromSuperview];
- self.submitButton = nil;
- return;
- }
- self.submitButton = [UIButton buttonWithType:UIButtonTypeSystem];
- [self.submitButton setTitle:@"OK" forState:UIControlStateNormal];
- [self.submitButton addTarget:self action:@selector(submitButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
- [self.submitButton.titleLabel setFont:[UIFont systemFontOfSize:18.0f]];
- self.submitButton.hidden = YES;
- [self addSubview:self.submitButton];
- [self setNeedsLayout];
- }
- - (void)setSubmitButtonSpacing:(CGFloat)submitButtonSpacing
- {
- if (submitButtonSpacing == _submitButtonSpacing) { return; }
- _submitButtonSpacing = submitButtonSpacing;
- [self setNeedsLayout];
- }
- - (void)setSubmitButtonFontSize:(CGFloat)submitButtonFontSize
- {
- if (submitButtonFontSize == _submitButtonFontSize) { return; }
- _submitButtonFontSize = submitButtonFontSize;
- self.submitButton.titleLabel.font = [UIFont systemFontOfSize:_submitButtonFontSize];
- [self.submitButton sizeToFit];
- [self setNeedsLayout];
- }
- - (void)setStyle:(TOPasscodeInputFieldStyle)style
- {
- if (style == _style) { return; }
- _style = style;
- [self setUpForStyle:_style];
- }
- - (void)setPasscode:(NSString *)passcode
- {
- [self setPasscode:passcode animated:NO];
- }
- - (void)setContentAlpha:(CGFloat)contentAlpha
- {
- _contentAlpha = contentAlpha;
- self.inputField.alpha = contentAlpha;
- self.submitButton.alpha = contentAlpha;
- }
- - (void)setHorizontalLayout:(BOOL)horizontalLayout
- {
- [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f];
- }
- - (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration
- {
- if (_horizontalLayout == horizontalLayout) {
- return;
- }
- UIView *snapshotView = nil;
- if (self.submitButton && self.submitButton.hidden == NO && animated) {
- snapshotView = [self.submitButton snapshotViewAfterScreenUpdates:NO];
- snapshotView.frame = self.submitButton.frame;
- [self addSubview:snapshotView];
- }
- _horizontalLayout = horizontalLayout;
- if (!animated || !self.submitButton) {
- [self sizeToFit];
- [self setNeedsLayout];
- return;
- }
- self.submitButton.alpha = 0.0f;
- [self setNeedsLayout];
- [self layoutIfNeeded];
- id animationBlock = ^{
- self.submitButton.alpha = 1.0f;
- snapshotView.alpha = 0.0f;
- };
- id completionBlock = ^(BOOL complete) {
- [snapshotView removeFromSuperview];
- [self bringSubviewToFront:self.submitButton];
- };
- [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock];
- }
- @end
|