Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Center tokens. #69

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
5 changes: 1 addition & 4 deletions VENTokenField/VENBackspaceTextField.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,12 @@

@implementation VENBackspaceTextField

- (BOOL)keyboardInputShouldDelete:(UITextField *)textField
{
- (void)deleteBackward {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyboardInputShouldDelete seemed to prevent textField:shouldChangeCharactersInRange:replacementString from being called on backspaces (and possibly other unknown side effects). deleteBackward doesn't seem to have the same limitation.

if (self.text.length == 0) {
if ([self.backspaceDelegate respondsToSelector:@selector(textFieldDidEnterBackspace:)]) {
[self.backspaceDelegate textFieldDidEnterBackspace:self];
}
}

return YES;
}

@end
8 changes: 7 additions & 1 deletion VENTokenField/VENTokenField.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
- (UIColor *)tokenField:(VENTokenField *)tokenField colorSchemeForTokenAtIndex:(NSUInteger)index;
@end

typedef NS_ENUM(NSInteger, VENTokenFieldAlignment) {
VENTokenFieldAlignmentLeft = NSTextAlignmentLeft,
VENTokenFieldAlignmentCenter = NSTextAlignmentCenter
};


@interface VENTokenField : UIView

Expand All @@ -59,8 +64,9 @@
@property (assign, nonatomic) CGFloat verticalInset;
@property (assign, nonatomic) CGFloat horizontalInset;
@property (assign, nonatomic) CGFloat tokenPadding;
@property (assign, nonatomic) CGFloat minInputWidth;
@property (assign, nonatomic) CGFloat minInputWidth DEPRECATED_ATTRIBUTE;

@property (assign, nonatomic) VENTokenFieldAlignment tokenAlignment;
@property (assign, nonatomic) UIKeyboardType inputTextFieldKeyboardType;
@property (assign, nonatomic) UITextAutocorrectionType autocorrectionType;
@property (assign, nonatomic) UITextAutocapitalizationType autocapitalizationType;
Expand Down
184 changes: 149 additions & 35 deletions VENTokenField/VENTokenField.m
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
static const CGFloat VENTokenFieldDefaultHorizontalInset = 15.0;
static const CGFloat VENTokenFieldDefaultToLabelPadding = 5.0;
static const CGFloat VENTokenFieldDefaultTokenPadding = 2.0;
static const CGFloat VENTokenFieldDefaultMinInputWidth = 80.0;
static const CGFloat VENTokenFieldDefaultMaxHeight = 150.0;


Expand All @@ -44,6 +43,8 @@ @interface VENTokenField () <VENBackspaceTextFieldDelegate>
@property (strong, nonatomic) VENBackspaceTextField *inputTextField;
@property (strong, nonatomic) UIColor *colorScheme;
@property (strong, nonatomic) UILabel *collapsedLabel;
@property (strong, nonatomic) NSString *inputTextFieldText;
@property (strong, nonatomic) UILabel *placeholderTextLabel;

@end

Expand Down Expand Up @@ -91,10 +92,10 @@ - (void)setUpInit
self.verticalInset = VENTokenFieldDefaultVerticalInset;
self.horizontalInset = VENTokenFieldDefaultHorizontalInset;
self.tokenPadding = VENTokenFieldDefaultTokenPadding;
self.minInputWidth = VENTokenFieldDefaultMinInputWidth;
self.colorScheme = [UIColor blueColor];
self.toLabelTextColor = [UIColor colorWithRed:112/255.0f green:124/255.0f blue:124/255.0f alpha:1.0f];
self.inputTextFieldTextColor = [UIColor colorWithRed:38/255.0f green:39/255.0f blue:41/255.0f alpha:1.0f];
self.tokenAlignment = VENTokenFieldAlignmentLeft;

// Accessing bare value to avoid kicking off a premature layout run.
_toLabelText = NSLocalizedString(@"To:", nil);
Expand All @@ -121,7 +122,8 @@ - (void)reloadData
- (void)setPlaceholderText:(NSString *)placeholderText
{
_placeholderText = placeholderText;
self.inputTextField.placeholder = _placeholderText;
self.placeholderTextLabel.text = _placeholderText;
self.placeholderTextLabel.width = [_placeholderText sizeWithAttributes:@{NSFontAttributeName:self.placeholderTextLabel.font}].width;
}

-(void)setInputTextFieldAccessibilityLabel:(NSString *)inputTextFieldAccessibilityLabel {
Expand Down Expand Up @@ -168,6 +170,13 @@ - (NSString *)inputText
return self.inputTextField.text;
}

- (void)setTokenAlignment:(VENTokenFieldAlignment)alignment
{
_tokenAlignment = alignment;
self.collapsedLabel.textAlignment = (NSTextAlignment)alignment;
self.placeholderTextLabel.textAlignment = (NSTextAlignment)alignment;
}


#pragma mark - View Layout

Expand All @@ -191,6 +200,7 @@ - (void)layoutCollapsedLabel
CGFloat currentX = 0;
[self layoutToLabelInView:self origin:CGPointMake(self.horizontalInset, self.verticalInset) currentX:&currentX];
[self layoutCollapsedLabelWithCurrentX:&currentX];
self.collapsedLabel.textAlignment = (NSTextAlignment)self.tokenAlignment;

self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleSingleTap:)];
Expand Down Expand Up @@ -220,7 +230,7 @@ - (void)layoutTokensAndInputWithFrameAdjustment:(BOOL)shouldAdjustFrame

[self.scrollView setContentSize:CGSizeMake(self.scrollView.contentSize.width, currentY + [self heightForToken])];

[self updateInputTextField];
[self updatePlaceholderTextLabel];

if (inputFieldShouldBecomeFirstResponder) {
[self inputTextFieldBecomeFirstResponder];
Expand Down Expand Up @@ -250,16 +260,9 @@ - (void)layoutScrollView

- (void)layoutInputTextFieldWithCurrentX:(CGFloat *)currentX currentY:(CGFloat *)currentY
{
CGFloat inputTextFieldWidth = self.scrollView.contentSize.width - *currentX;
if (inputTextFieldWidth < self.minInputWidth) {
inputTextFieldWidth = self.scrollView.contentSize.width;
*currentY += [self heightForToken];
*currentX = 0;
}

VENBackspaceTextField *inputTextField = self.inputTextField;
inputTextField.text = @"";
inputTextField.frame = CGRectMake(*currentX, *currentY + 1, inputTextFieldWidth, [self heightForToken] - 1);
inputTextField.text = self.inputTextFieldText;
inputTextField.frame = CGRectMake(*currentX, *currentY + 1, self.scrollView.contentSize.width - *currentX, [self heightForToken] - 1);
inputTextField.tintColor = self.colorScheme;
[self.scrollView addSubview:inputTextField];
}
Expand Down Expand Up @@ -298,7 +301,6 @@ - (void)layoutTokensWithCurrentX:(CGFloat *)currentX currentY:(CGFloat *)current
for (NSUInteger i = 0; i < [self numberOfTokens]; i++) {
NSString *title = [self titleForTokenAtIndex:i];
VENToken *token = [[VENToken alloc] init];

__weak VENToken *weakToken = token;
__weak VENTokenField *weakSelf = self;
token.didTapTokenBlock = ^{
Expand All @@ -310,20 +312,88 @@ - (void)layoutTokensWithCurrentX:(CGFloat *)currentX currentY:(CGFloat *)current

[self.tokens addObject:token];

if (*currentX + token.width <= self.scrollView.contentSize.width) { // token fits in current line
token.frame = CGRectMake(*currentX, *currentY, token.width, token.height);
} else {
*currentY += token.height;
*currentX = 0;
CGFloat tokenWidth = token.width;
if (tokenWidth > self.scrollView.contentSize.width) { // token is wider than max width
tokenWidth = self.scrollView.contentSize.width;
}
token.frame = CGRectMake(*currentX, *currentY, tokenWidth, token.height);
}
token.frame = [self frameForToken:token currentX:currentX currentY:currentY];
*currentX += token.width + self.tokenPadding;
[self.scrollView addSubview:token];
}

VENToken *placeholderToken = [[VENToken alloc] init];
[placeholderToken setTitleText:[self.inputTextFieldText stringByAppendingString:@"| "]]; // account for text field cursor and space
placeholderToken.frame = [self frameForToken:placeholderToken currentX:currentX currentY:currentY];

[self realignTokens:[self.tokens arrayByAddingObject:placeholderToken]
currentX:currentX
alignment:self.tokenAlignment];
}

- (CGRect)frameForToken:(VENToken *)token currentX:(CGFloat *)currentX currentY:(CGFloat *)currentY {
if (*currentX + token.width <= self.scrollView.contentSize.width) { // token fits in current line
return CGRectMake(*currentX, *currentY, token.width, token.height);
} else {
*currentY += token.height;
*currentX = 0;
CGFloat tokenWidth = token.width;
if (tokenWidth > self.scrollView.contentSize.width) { // token is wider than max width
tokenWidth = self.scrollView.contentSize.width;
}
return CGRectMake(*currentX, *currentY, tokenWidth, token.height);
}
}

- (void)realignTokens:(NSArray *)tokens
currentX:(CGFloat *)currentX
alignment:(VENTokenFieldAlignment)alignment
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (VENToken *token in tokens) {
if (dict[@(token.y)]) {
dict[@(token.y)] = [dict[@(token.y)] arrayByAddingObject:token];
} else {
dict[@(token.y)] = @[token];
}
}
// Could be parallelized: compute all new frames on background threads.
// Once done, assign new frames to tokens on main thread.
CGFloat currentY = 0;
for (NSNumber *rowY in [dict allKeys]) {
[self realignRowOfTokens:dict[rowY] currentX:currentX currentY:&currentY alignment:alignment];
}
}

- (void)realignRowOfTokens:(NSArray *)tokens
currentX:(CGFloat *)currentX
currentY:(CGFloat *)currentY
alignment:(VENTokenFieldAlignment)alignment
{
// This method is unreliant on the order of tokens. It could be faster if it was reliant.
VENToken *firstToken = [tokens firstObject];
VENToken *lastToken = [tokens firstObject];
CGFloat totalLineWidth = 0;
for (VENToken *token in tokens) {
if (token.x < firstToken.x) {
firstToken = token;
}
if (token.x > lastToken.x) {
lastToken = token;
}
totalLineWidth += token.width;
}
CGFloat frameAdjustment = 0;
switch (self.tokenAlignment) {
case VENTokenFieldAlignmentCenter: {
frameAdjustment = (self.scrollView.contentSize.width - firstToken.x - totalLineWidth) / 2.0;
break;
}
default:
break;
}
for (VENToken *token in tokens) {
token.x += frameAdjustment;
}
if (firstToken.y >= *currentY) {
*currentX = lastToken.x;
*currentY = lastToken.y;
}
}


Expand Down Expand Up @@ -371,6 +441,21 @@ - (UILabel *)toLabel
return _toLabel;
}

- (UILabel *)placeholderTextLabel
{
if (!_placeholderTextLabel) {
_placeholderTextLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0,
[self heightForToken])];
_placeholderTextLabel.font = self.inputTextField.font;
_placeholderTextLabel.textAlignment = (NSTextAlignment)self.tokenAlignment;
_placeholderTextLabel.textColor = [UIColor colorWithWhite:.8 alpha:1];
[_placeholderTextLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self.inputTextField
action:@selector(becomeFirstResponder)]];
_placeholderTextLabel.userInteractionEnabled = YES;
}
return _placeholderTextLabel;
}

- (void)adjustHeightForCurrentY:(CGFloat)currentY
{
if (currentY + [self heightForToken] > CGRectGetHeight(self.frame)) { // needs to grow
Expand Down Expand Up @@ -400,7 +485,6 @@ - (VENBackspaceTextField *)inputTextField
_inputTextField.tintColor = self.colorScheme;
_inputTextField.delegate = self;
_inputTextField.backspaceDelegate = self;
_inputTextField.placeholder = self.placeholderText;
_inputTextField.accessibilityLabel = self.inputTextFieldAccessibilityLabel ?: NSLocalizedString(@"To", nil);
_inputTextField.inputAccessoryView = self.inputTextFieldAccessoryView;
[_inputTextField addTarget:self action:@selector(inputTextFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
Expand Down Expand Up @@ -475,9 +559,24 @@ - (void)setCursorVisibility
}
}

- (void)updateInputTextField
- (void)updatePlaceholderTextLabel
{
self.inputTextField.placeholder = [self.tokens count] ? nil : self.placeholderText;
if (![self.tokens count] && self.inputTextFieldText.length == 0) {
switch (self.tokenAlignment) {
case VENTokenFieldAlignmentCenter:
self.placeholderTextLabel.centerX = self.inputTextField.x;
self.inputTextField.x = self.placeholderTextLabel.x;
break;
case VENTokenFieldAlignmentLeft:
self.placeholderTextLabel.x = self.inputTextField.x;
break;
default:
break;
}
[self.scrollView insertSubview:self.placeholderTextLabel belowSubview:self.inputTextField];
} else {
[self.placeholderTextLabel removeFromSuperview];
}
}

- (void)focusInputTextField
Expand All @@ -498,11 +597,14 @@ - (UIColor *)colorSchemeForTokenAtIndex:(NSUInteger)index {
return self.colorScheme;
}


#pragma mark - Data Source

- (NSString *)titleForTokenAtIndex:(NSUInteger)index
{
if ([self.dataSource respondsToSelector:@selector(tokenField:titleForTokenAtIndex:)]) {
if (index == [self numberOfTokens]) {
return self.inputTextFieldText;
} else if ([self.dataSource respondsToSelector:@selector(tokenField:titleForTokenAtIndex:)]) {
return [self.dataSource tokenField:self titleForTokenAtIndex:index];
}

Expand All @@ -528,16 +630,23 @@ - (NSString *)collapsedText
}


#pragma mark - UITextFieldDelegate
#pragma mark - Delegate

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
- (void)didEnterText:(NSString *)text {
if ([self.delegate respondsToSelector:@selector(tokenField:didEnterText:)]) {
if ([textField.text length]) {
[self.delegate tokenField:self didEnterText:textField.text];
if ([text length]) {
self.inputTextFieldText = @"";
[self.delegate tokenField:self didEnterText:text];
}
}

}


#pragma mark - UITextFieldDelegate

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[self didEnterText:textField.text];
return NO;
}

Expand All @@ -551,6 +660,11 @@ - (void)textFieldDidBeginEditing:(UITextField *)textField
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
[self unhighlightAllTokens];
if (textField == self.inputTextField) {
self.inputTextFieldText = [textField.text stringByReplacingCharactersInRange:range withString:string];
[self reloadData];
return NO;
}
return YES;
}

Expand Down