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

Suggestions #17

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: ```<VENTokenFieldDelegate>``` and ```<VENTokenFieldDataSource>```.
Similar to ```UITableView```, ```VENTokenField``` provides three protocols: ```<VENTokenFieldDelegate>```, ```<VENTokenFieldDataSource>``` and ```<VENTokenSuggestionDataSource>```.

### 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.
Expand All @@ -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.
Expand Down
30 changes: 30 additions & 0 deletions VENTokenField/VENSuggestionTableViewManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// VENSuggestionTableViewManager.h
// Pods
//
// Created by Ben Nicholas on 8/8/14.
//
//

#import <UIKit/UIKit.h>

@class VENTokenField;
@protocol VENTokenSuggestionDataSource;

@protocol VENSuggestionTableViewManagerDelegate <NSObject>
- (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<VENTokenSuggestionDataSource> dataSource;
@property (assign, nonatomic) id<VENSuggestionTableViewManagerDelegate> delegate;

- (instancetype)initWithTokenField:(VENTokenField *)tokenField;

- (void)displayTableView;
- (void)hideTableView;

@end
99 changes: 99 additions & 0 deletions VENTokenField/VENSuggestionTableViewManager.m
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions VENTokenField/VENTokenField.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
@protocol VENTokenFieldDelegate <NSObject>
@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;
Expand All @@ -38,11 +39,19 @@
- (NSString *)tokenFieldCollapsedText:(VENTokenField *)tokenField;
@end

@protocol VENTokenSuggestionDataSource <NSObject>
@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<VENTokenFieldDelegate> delegate;
@property (weak, nonatomic) id<VENTokenFieldDataSource> dataSource;
@property (weak, nonatomic) id<VENTokenSuggestionDataSource> suggestionDataSource;

- (void)reloadData;
- (void)collapse;
Expand Down
51 changes: 49 additions & 2 deletions VENTokenField/VENTokenField.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#import <FrameAccessor/FrameAccessor.h>
#import "VENToken.h"
#import "VENBackspaceTextField.h"
#import "VENSuggestionTableViewManager.h"

static const CGFloat VENTokenFieldDefaultVerticalInset = 7.0;
static const CGFloat VENTokenFieldDefaultHorizontalInset = 15.0;
Expand All @@ -34,7 +35,7 @@
static const CGFloat VENTokenFieldDefaultMaxHeight = 150.0;


@interface VENTokenField () <VENBackspaceTextFieldDelegate>
@interface VENTokenField () <VENBackspaceTextFieldDelegate, VENSuggestionTableViewManagerDelegate>

@property (strong, nonatomic) UIScrollView *scrollView;
@property (strong, nonatomic) NSMutableArray *tokens;
Expand All @@ -44,6 +45,7 @@ @interface VENTokenField () <VENBackspaceTextFieldDelegate>
@property (strong, nonatomic) VENBackspaceTextField *inputTextField;
@property (strong, nonatomic) UIColor *colorScheme;
@property (strong, nonatomic) UILabel *collapsedLabel;
@property (strong, nonatomic) VENSuggestionTableViewManager *tableViewManager;

@end

Expand Down Expand Up @@ -102,6 +104,8 @@ - (void)collapse
[self.collapsedLabel removeFromSuperview];
self.scrollView.hidden = YES;
[self setHeight:self.originalHeight];

[self.tableViewManager hideTableView];

CGFloat currentX = 0;

Expand All @@ -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];

Expand Down Expand Up @@ -171,6 +176,12 @@ - (void)setColorScheme:(UIColor *)color
}
}

- (void)setSuggestionDataSource:(id<VENTokenSuggestionDataSource>)suggestionDataSource
{
_suggestionDataSource = suggestionDataSource;
self.tableViewManager.dataSource = suggestionDataSource;
}

- (NSString *)inputText
{
return self.inputTextField.text;
Expand Down Expand Up @@ -259,7 +270,6 @@ - (void)layoutTokensWithCurrentX:(CGFloat *)currentX currentY:(CGFloat *)current
}
}


#pragma mark - Private

- (CGFloat)heightForToken
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:)]) {
Expand All @@ -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

Expand Down
30 changes: 29 additions & 1 deletion VENTokenFieldSample/ViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
#import "ViewController.h"
#import "VENTokenField.h"

@interface ViewController () <VENTokenFieldDelegate, VENTokenFieldDataSource>
@interface ViewController () <VENTokenFieldDelegate, VENTokenFieldDataSource, VENTokenSuggestionDataSource>
@property (weak, nonatomic) IBOutlet VENTokenField *tokenField;
@property (strong, nonatomic) NSMutableArray *names;
@property (strong, nonatomic) NSArray *knownNames;
@property (strong, nonatomic) NSArray *filteredNames;
@end

@implementation ViewController
Expand All @@ -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];
Expand All @@ -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];
Expand All @@ -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
11 changes: 10 additions & 1 deletion VENTokenFieldSampleTests/VENTokenFieldSampleTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand All @@ -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"];
Expand Down