diff --git a/README.md b/README.md index 481f447..665f1b1 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,13 @@ Usage If you've ever used a ```UITableView```, using ```VENTokenField``` should be a breeze. -Similar to ```UITableView```, ```VENTokenField``` provides two protocols: `````` and ``````. +Similar to ```UITableView```, ```VENTokenField``` provides three protocols: ``````, `````` and ``````. ### VENTokenFieldDelegate This protocol notifies you when things happen in the token field that you might want to know about. -* ```tokenField:didEnterText:``` is called when a user hits the return key on the input field. +* ```tokenField:didEnterText:``` is called when a user hits the return key on the input field or after a suggestion is tapped.. +* ```tokenField:didSelectSuggestion:forPartialText:atIndex:``` is called when a user taps on a suggested value in the suggestion list. * ```tokenField:didDeleteTokenAtIndex:``` is called when a user deletes a token at a particular index. * ```tokenField:didChangeText:``` is called when a user changes the text in the input field. * ```tokenFieldDidBeginEditing:``` is called when the input field becomes first responder. @@ -37,6 +38,14 @@ Implement... * ```numberOfTokensInTokenField:``` to specify how many tokens you have. * ```tokenFieldCollapsedText:``` to specify what you want the token field should say in the collapsed state. +### VENTokenSuggestionDataSource +This entirely optional protocol allows you to provide info for any suggestions presented to the user. + +Implement... +* ```tokenFieldShouldPresentSuggestions:``` to specify that you want to present suggested values for tokens. +* ```tokenField:numberOfSuggestionsForPartialText:``` to specify the number of suggestions for a given input. +* ```tokenField:suggestionTitleForPartialText:atIndex:``` to specify what the title for a suggestion at a particular index should be. + Sample Project -------------- Check out the [sample project](https://github.com/venmo/VENTokenField/tree/master/VENTokenFieldSample) in this repo for sample usage. diff --git a/VENTokenField/VENSuggestionTableViewManager.h b/VENTokenField/VENSuggestionTableViewManager.h new file mode 100644 index 0000000..baadd65 --- /dev/null +++ b/VENTokenField/VENSuggestionTableViewManager.h @@ -0,0 +1,30 @@ +// +// VENSuggestionTableViewManager.h +// Pods +// +// Created by Ben Nicholas on 8/8/14. +// +// + +#import + +@class VENTokenField; +@protocol VENTokenSuggestionDataSource; + +@protocol VENSuggestionTableViewManagerDelegate +- (void)suggestionManagerDidSelectValue:(NSString *)value atIndex:(NSInteger)index; +@end + +@interface VENSuggestionTableViewManager : NSObject < UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate > + +@property (strong, nonatomic) UITableView *tableView; +@property (strong, nonatomic) VENTokenField *tokenField; +@property (assign, nonatomic) id dataSource; +@property (assign, nonatomic) id delegate; + +- (instancetype)initWithTokenField:(VENTokenField *)tokenField; + +- (void)displayTableView; +- (void)hideTableView; + +@end diff --git a/VENTokenField/VENSuggestionTableViewManager.m b/VENTokenField/VENSuggestionTableViewManager.m new file mode 100644 index 0000000..697eb35 --- /dev/null +++ b/VENTokenField/VENSuggestionTableViewManager.m @@ -0,0 +1,99 @@ +// +// VENSuggestionTableViewManager +// Pods +// +// Created by Ben Nicholas on 8/8/14. +// +// + +#import "VENSuggestionTableViewManager.h" +#import "VENTokenField.h" + +@interface VENSuggestionTableViewManager () + +@property (nonatomic, strong) NSArray *options; + +@end + +@implementation VENSuggestionTableViewManager + +- (instancetype)initWithTokenField:(VENTokenField *)tokenField +{ + self = [super init]; + if (self) { + self.tokenField = tokenField; + } + return self; +} + +- (NSString *)valueForIndexPath:(NSIndexPath *)indexPath +{ + return [self.dataSource tokenField:self.tokenField suggestionTitleForPartialText:self.tokenField.inputText atIndex:indexPath.row]; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return [self.dataSource tokenField:self.tokenField numberOfSuggestionsForPartialText:self.tokenField.inputText]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"suggestionCell"]; + + if (!cell) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"suggestionCell"]; + } + + cell.textLabel.text = [self valueForIndexPath:indexPath]; + + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate suggestionManagerDidSelectValue:[self valueForIndexPath:indexPath] atIndex:indexPath.row]; + + [tableView deselectRowAtIndexPath:indexPath animated:NO]; +} + + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView +{ + [self.tokenField resignFirstResponder]; +} + +- (void)displayTableView +{ + [self.tableView reloadData]; + [self.tokenField.window addSubview:self.tableView]; +} + +- (void)hideTableView +{ + [self.tableView removeFromSuperview]; +} + +- (UITableView *)tableView +{ + CGRect globalTokenViewFrame = [self.tokenField convertRect:self.tokenField.bounds toView:self.tokenField.window]; + CGRect newFrame = CGRectMake(CGRectGetMinX(globalTokenViewFrame), + CGRectGetMaxY(globalTokenViewFrame), + CGRectGetWidth(globalTokenViewFrame), + CGRectGetHeight(self.tokenField.window.frame) - CGRectGetMaxY(globalTokenViewFrame)); + if (!_tableView) { + _tableView = [[UITableView alloc] initWithFrame:newFrame + style:UITableViewStylePlain]; + _tableView.delegate = self; + _tableView.dataSource = self; + } else { + _tableView.frame = newFrame; + } + return _tableView; +} + +@end diff --git a/VENTokenField/VENTokenField.h b/VENTokenField/VENTokenField.h index 5d5557e..10e9363 100644 --- a/VENTokenField/VENTokenField.h +++ b/VENTokenField/VENTokenField.h @@ -26,6 +26,7 @@ @protocol VENTokenFieldDelegate @optional - (void)tokenField:(VENTokenField *)tokenField didEnterText:(NSString *)text; +- (void)tokenField:(VENTokenField *)tokenField didSelectSuggestion:(NSString *)suggestion forPartialText:(NSString *)text atIndex:(NSInteger) index; - (void)tokenField:(VENTokenField *)tokenField didDeleteTokenAtIndex:(NSUInteger)index; - (void)tokenField:(VENTokenField *)tokenField didChangeText:(NSString *)text; - (void)tokenFieldDidBeginEditing:(VENTokenField *)tokenField; @@ -38,11 +39,19 @@ - (NSString *)tokenFieldCollapsedText:(VENTokenField *)tokenField; @end +@protocol VENTokenSuggestionDataSource +@optional +- (BOOL)tokenFieldShouldPresentSuggestions:(VENTokenField *)tokenField; +- (NSInteger)tokenField:(VENTokenField *)tokenField numberOfSuggestionsForPartialText:(NSString *)text; +- (NSString *)tokenField:(VENTokenField *)tokenField suggestionTitleForPartialText:(NSString *)text atIndex:(NSInteger)index; +@end + @interface VENTokenField : UIView @property (weak, nonatomic) id delegate; @property (weak, nonatomic) id dataSource; +@property (weak, nonatomic) id suggestionDataSource; - (void)reloadData; - (void)collapse; diff --git a/VENTokenField/VENTokenField.m b/VENTokenField/VENTokenField.m index dc3cba4..cbaa03d 100644 --- a/VENTokenField/VENTokenField.m +++ b/VENTokenField/VENTokenField.m @@ -25,6 +25,7 @@ #import #import "VENToken.h" #import "VENBackspaceTextField.h" +#import "VENSuggestionTableViewManager.h" static const CGFloat VENTokenFieldDefaultVerticalInset = 7.0; static const CGFloat VENTokenFieldDefaultHorizontalInset = 15.0; @@ -34,7 +35,7 @@ static const CGFloat VENTokenFieldDefaultMaxHeight = 150.0; -@interface VENTokenField () +@interface VENTokenField () @property (strong, nonatomic) UIScrollView *scrollView; @property (strong, nonatomic) NSMutableArray *tokens; @@ -44,6 +45,7 @@ @interface VENTokenField () @property (strong, nonatomic) VENBackspaceTextField *inputTextField; @property (strong, nonatomic) UIColor *colorScheme; @property (strong, nonatomic) UILabel *collapsedLabel; +@property (strong, nonatomic) VENSuggestionTableViewManager *tableViewManager; @end @@ -102,6 +104,8 @@ - (void)collapse [self.collapsedLabel removeFromSuperview]; self.scrollView.hidden = YES; [self setHeight:self.originalHeight]; + + [self.tableViewManager hideTableView]; CGFloat currentX = 0; @@ -121,6 +125,7 @@ - (void)reloadData [self.scrollView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; self.scrollView.hidden = NO; [self removeGestureRecognizer:self.tapGestureRecognizer]; + [self.tableViewManager hideTableView]; self.tokens = [NSMutableArray array]; @@ -171,6 +176,12 @@ - (void)setColorScheme:(UIColor *)color } } +- (void)setSuggestionDataSource:(id)suggestionDataSource +{ + _suggestionDataSource = suggestionDataSource; + self.tableViewManager.dataSource = suggestionDataSource; +} + - (NSString *)inputText { return self.inputTextField.text; @@ -259,7 +270,6 @@ - (void)layoutTokensWithCurrentX:(CGFloat *)currentX currentY:(CGFloat *)current } } - #pragma mark - Private - (CGFloat)heightForToken @@ -345,6 +355,23 @@ - (void)inputTextFieldDidChange:(UITextField *)textField if ([self.delegate respondsToSelector:@selector(tokenField:didChangeText:)]) { [self.delegate tokenField:self didChangeText:textField.text]; } + if ([self suggests]) { + if (textField.text.length > 0) { + [self.tableViewManager displayTableView]; + } else { + [self.tableViewManager hideTableView]; + } + + } +} + +- (VENSuggestionTableViewManager *)tableViewManager +{ + if (!_tableViewManager) { + _tableViewManager = [[VENSuggestionTableViewManager alloc] initWithTokenField:self]; + _tableViewManager.delegate = self; + } + return _tableViewManager; } - (void)handleSingleTap:(UITapGestureRecognizer *)gestureRecognizer @@ -418,6 +445,14 @@ - (NSUInteger)numberOfTokens return 0; } +- (BOOL)suggests +{ + if ([self.suggestionDataSource respondsToSelector:@selector(tokenFieldShouldPresentSuggestions:)]) { + return [self.suggestionDataSource tokenFieldShouldPresentSuggestions:self]; + } + return NO; +} + - (NSString *)collapsedText { if ([self.dataSource respondsToSelector:@selector(tokenFieldCollapsedText:)]) { @@ -426,6 +461,18 @@ - (NSString *)collapsedText return @""; } +#pragma mark - VENSuggestionTableViewManagerDelegate + +- (void)suggestionManagerDidSelectValue:(NSString *)value atIndex:(NSInteger)index +{ + NSString *fieldText = self.inputText; + if ([self.delegate respondsToSelector:@selector(tokenField:didEnterText:)]) { + [self.delegate tokenField:self didEnterText:value]; + } + if ([self.delegate respondsToSelector:@selector(tokenField:didSelectSuggestion:forPartialText:atIndex:)]) { + [self.delegate tokenField:self didSelectSuggestion:value forPartialText:fieldText atIndex:index]; + } +} #pragma mark - UITextFieldDelegate diff --git a/VENTokenFieldSample/ViewController.m b/VENTokenFieldSample/ViewController.m index b50205d..c124748 100644 --- a/VENTokenFieldSample/ViewController.m +++ b/VENTokenFieldSample/ViewController.m @@ -9,9 +9,11 @@ #import "ViewController.h" #import "VENTokenField.h" -@interface ViewController () +@interface ViewController () @property (weak, nonatomic) IBOutlet VENTokenField *tokenField; @property (strong, nonatomic) NSMutableArray *names; +@property (strong, nonatomic) NSArray *knownNames; +@property (strong, nonatomic) NSArray *filteredNames; @end @implementation ViewController @@ -20,8 +22,10 @@ - (void)viewDidLoad { [super viewDidLoad]; self.names = [NSMutableArray array]; + self.knownNames = @[@"Ayaka", @"Mark", @"Neeraj", @"Octocat", @"Octavius", @"Ben"]; self.tokenField.delegate = self; self.tokenField.dataSource = self; + self.tokenField.suggestionDataSource = self; self.tokenField.placeholderText = NSLocalizedString(@"Enter names here", nil); [self.tokenField setColorScheme:[UIColor colorWithRed:61/255.0f green:149/255.0f blue:206/255.0f alpha:1.0f]]; [self.tokenField becomeFirstResponder]; @@ -46,6 +50,11 @@ - (void)tokenField:(VENTokenField *)tokenField didEnterText:(NSString *)text [self.tokenField reloadData]; } +- (void)tokenField:(VENTokenField *)tokenField didSelectSuggestion:(NSString *)suggestion forPartialText:(NSString *)text atIndex:(NSInteger)index +{ + NSLog(@"Added suggested value: %@", suggestion); +} + - (void)tokenField:(VENTokenField *)tokenField didDeleteTokenAtIndex:(NSUInteger)index { [self.names removeObjectAtIndex:index]; @@ -70,4 +79,23 @@ - (NSString *)tokenFieldCollapsedText:(VENTokenField *)tokenField return [NSString stringWithFormat:@"%lu people", [self.names count]]; } +#pragma mark - VENTokenSuggestionDataSource + +- (BOOL)tokenFieldShouldPresentSuggestions:(VENTokenField *)tokenField +{ + return YES; +} + +- (NSInteger)tokenField:(VENTokenField *)tokenField numberOfSuggestionsForPartialText:(NSString *)text +{ + self.filteredNames = [self.knownNames filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF BEGINSWITH[c] %@", text]]; + + return self.filteredNames.count; +} + +- (NSString *)tokenField:(VENTokenField *)tokenField suggestionTitleForPartialText:(NSString *)text atIndex:(NSInteger)index +{ + return self.filteredNames[index]; +} + @end diff --git a/VENTokenFieldSampleTests/VENTokenFieldSampleTests.m b/VENTokenFieldSampleTests/VENTokenFieldSampleTests.m index 89746d9..000e0ef 100644 --- a/VENTokenFieldSampleTests/VENTokenFieldSampleTests.m +++ b/VENTokenFieldSampleTests/VENTokenFieldSampleTests.m @@ -16,7 +16,8 @@ @implementation VENTokenFieldSampleTests - (void)testBasicFlow { - [tester enterTextIntoCurrentFirstResponder:@"Ayaka\n"]; + [tester enterText:@"Ayaka" intoViewWithAccessibilityLabel:@"To"]; + [tester enterTextIntoCurrentFirstResponder:@"\n"]; [tester waitForViewWithAccessibilityLabel:@"Ayaka,"]; [tester enterTextIntoCurrentFirstResponder:@"Mark\n"]; @@ -38,6 +39,14 @@ - (void)testBasicFlow [tester waitForAbsenceOfViewWithAccessibilityLabel:@"Octocat,"]; } +- (void)testSuggestionFlow +{ + [tester enterText:@"Be" intoViewWithAccessibilityLabel:@"To"]; + [tester waitForViewWithAccessibilityLabel:@"Ben"]; + [tester tapViewWithAccessibilityLabel:@"Ben"]; + [tester waitForViewWithAccessibilityLabel:@"Ben,"]; +} + - (void)testResignFirstResponder { [tester tapViewWithAccessibilityLabel:@"To"];