UITextField & Auto Suggestion
One of the time-consuming parts of any application for users is text fields. To save invaluable time for users it’s often a good idea to provide helper tools for them. In this article, I gonna show you how to make auto suggestion feature for UITextField
. For instance, we can use it as the additional feature for the search field.
Quick Introduction
To interest you, that’s how the final project will look like:
In the beginning, let’s define what our auto suggestion feature will do:
- The auto suggestion will appear and disappear when user will begin and end typing to
UITextField
accordingly. - Data to be shown as suggestions will be taken from a local source or by the network.
- Communication between auto suggestion and
UIViewController
will be through a protocol. - UI will consist from:
UIView
container forUITableView
to set rounded corners on two sides instead all four sidesUITableView
to show the list of suggestionsUIActivityIndicatorView
to show loading state, if we need to make network connection- Two
UIView
for dim effect (One to darken and disable other parts of the screen, second for loading state ofUITableView
)
Implementation
Let’s create the new category for UITextField
called AutoSuggestion. This will make our solution for this problem more flexible. Alternatively, you can create a subclass of UITextField
if you think auto suggestion feature is too big for a category.
// UITextField+AutoSuggestion.h
#import <UIKit/UIKit.h>
@interface UITextField (AutoSuggestion)
@end
// UITextField+AutoSuggestion.m
#import "UITextField+AutoSuggestion.h"
@implementation UITextField (AutoSuggestion)
@end
Next, we need to add some properties. By default in Objective-C categories can not contain properties, but we can achieve that by using <objc/runtime.h>
library. It creates properties on runtime, Captain Obvious :)
// inside @interface in UITextField+AutoSuggestion.h
@interface UITextField (AutoSuggestion)
@property (nonatomic, strong) UIView *tableContainerView; // 1
@property (nonatomic, strong) UITableView *tableView; // 2
@property (nonatomic, strong) UIView *tableAlphaView; // 3
@property (nonatomic, strong) UIActivityIndicatorView *spinner; // 4
@property (nonatomic, strong) UIView *alphaView; // 5
@property BOOL autoSuggestionIsShowing; // 6
@property CGRect textFieldRectOnWindow; // 7
@property CGRect keyboardFrameBeginRect; // 8
@property (nonatomic, strong) NSString *fieldIdentifier; // 9
@property NSInteger maxNumberOfRows; // 10
@end
Explanation:
tableContainerView
is a view to hold ourUITableView
. We will set rounded corners on two sides instead of four by applying the mask to view usingUIBezierPath
. If we set rounded corners by this strategy toUITableView
instead than part ofUITableView
will not be seen while scrolling.- This is our
UITableView
to present data. tableAlphaView
will be used to show loading state effect.spinner
also will be used to show loading state effect.alphaView
is a view to darken and disable outer parts ofUITextField
and suggestion view while editing.autoSuggestionIsShowing
is a flag to store current state of suggestion view.textFieldRectOnWindow
is aCGRect
ofUITextField
where to show suggestion view. Needed to calculate size and position of suggestion view.keyboardFrameBeginRect
is aCGRect
of keyboard. Needed to calculate size and position of suggestion view.fieldIdentifier
helper variable to differentiate fields.maxNumberOfRows
is a variable to provide maximum number of rows to show in suggestion view instead of default number.
Now to finish we need to provide getters and setters using <objc/runtime.h>
which will be used at runtime. Below are few examples how it can be achieved for variables of type BOOL
, CGRect
, and UITableView
:
// in UITextField+AutoSuggestion.m
- (BOOL)autoSuggestionIsShowing {
return [objc_getAssociatedObject(self, @selector(autoSuggestionIsShowing)) boolValue];
}
- (void)setAutoSuggestionIsShowing:(BOOL)autoSuggestionIsShowing {
objc_setAssociatedObject(self, @selector(autoSuggestionIsShowing), @(autoSuggestionIsShowing), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UITableView *)tableView {
return objc_getAssociatedObject(self, @selector(tableView));
}
- (void)setTableView:(UITableView *)tableView {
objc_setAssociatedObject(self, @selector(tableView), tableView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (CGRect)textFieldRectOnWindow {
NSValue *textFieldRectOnWindowValue = (NSValue *)objc_getAssociatedObject(self, &textFieldRectOnWindowKey);
if (textFieldRectOnWindowValue != nil) {
return [textFieldRectOnWindowValue CGRectValue];
} else {
return CGRectZero;
}
}
- (void)setTextFieldRectOnWindow:(CGRect)textFieldRectOnWindow {
NSValue *textFieldRectOnWindowValue = [NSValue valueWithCGRect:textFieldRectOnWindow];
objc_setAssociatedObject(self, &textFieldRectOnWindowKey, textFieldRectOnWindowValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
As you can see from above code snippet, we use two methods:
-
objc_setAssociatedObject(_:_:_:_:)
From documentation:
Sets an associated value for a given object using a given key and association policy.
-
objc_getAssociatedObject(_:_:)
.From documentation:
Returns the value associated with a given object for a given key.
From my knowledge almost all type of variables can be stored and retrieved using these two methods.
Other properties implementation is straightforward, you can use above examples and try yourself.
Next steps are to present suggestion view. Therefore we need to calculate position and size for views. Since it heavily depends on keyboard frame let’s first of all create method to get keybord height. We will use observer on UIKeyboardDidShowNotification
notification to get keyboard frame.
// in UITextField+AutoSuggestion.h
@interface UITextField (AutoSuggestion)
// previous stuff
- (void)observeTextFieldChanges; // 1
@end
// in UITextField+AutoSuggestion.m
// 2
- (void)observeTextFieldChanges {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(getKeyboardHeight:)
name:UIKeyboardDidShowNotification
object:nil];
}
// 3
- (void)getKeyboardHeight:(NSNotification*)notification {
NSDictionary* keyboardInfo = [notification userInfo];
NSValue* keyboardFrameBegin = [keyboardInfo valueForKey:UIKeyboardFrameBeginUserInfoKey];
self.keyboardFrameBeginRect = [keyboardFrameBegin CGRectValue];
}
Explanation:
- We define
observeTextFieldChanges()
method in the header file because we need to start observing changes inUITextField
from other places. - Also, this method will be extended in next steps. For now, it only contains the line to observe changes for
UIKeyboardDidShowNotification
notifications. - To get the frame of the keyboard we need to get its value from the dictionary by key named
UIKeyboardFrameBeginUserInfoKey
.
Next step is to implement the logic of showing and hiding suggestions view.
// in UITextField+AutoSuggestion.m
// 1
- (void)observeTextFieldChanges {
// previous stuff
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(toggleAutoSuggestion:) name:UITextFieldTextDidChangeNotification object:self];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hideAutoSuggestion) name:UITextFieldTextDidEndEditingNotification object:self];
}
// 2
- (void)createSuggestionView {
UIWindow *appDelegateWindow = [UIApplication sharedApplication].keyWindow;
self.textFieldRectOnWindow = [self convertRect:self.bounds toView:nil];
if (!self.tableContainerView) {
self.tableContainerView = [UIView new];
self.tableContainerView.backgroundColor = [UIColor whiteColor];
}
if (!self.tableView) {
self.tableView = [[UITableView alloc] initWithFrame:self.textFieldRectOnWindow style:UITableViewStylePlain];
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.tableFooterView = [UIView new];
}
if (!self.alphaView) {
self.alphaView = [[UIView alloc] initWithFrame:appDelegateWindow.bounds];
self.alphaView.userInteractionEnabled = true;
self.alphaView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
[appDelegateWindow addSubview:self.alphaView];
}
self.tableView.frame = self.textFieldRectOnWindow;
[self.tableContainerView addSubview:self.tableView];
self.tableContainerView.frame = self.textFieldRectOnWindow;
[appDelegateWindow addSubview:self.tableContainerView];
}
// 3
- (void)showAutoSuggestion {
if (!self.autoSuggestionIsShowing) {
[self createSuggestionView];
self.autoSuggestionIsShowing = YES;
}
[self reloadContents];
}
// 4
- (void)hideAutoSuggestion {
if (self.autoSuggestionIsShowing) {
[self.alphaView removeFromSuperview];
self.alphaView = nil;
[self.tableView removeFromSuperview];
self.tableView = nil;
[self.tableContainerView removeFromSuperview];
self.tableContainerView = nil;
self.autoSuggestionIsShowing = NO;
}
}
// 5
- (void)toggleAutoSuggestion:(NSNotification *)notification {
if (self.text.length > 0) {
[self showAutoSuggestion];
} else {
[self hideAutoSuggestion];
}
}
// 6
- (void)reloadContents {
[self updateHeight];
[self updateCornerRadius];
[self checkForEmptyState];
[self.tableView reloadData];
}
// 7
- (void)updateHeight {
NSInteger numberOfResults = [self tableView:self.tableView numberOfRowsInSection:0];
NSInteger maxRowsToShow = self.maxNumberOfRows != 0 ? self.maxNumberOfRows : DEFAULT_MAX_NUM_OF_ROWS;
CGFloat cellHeight = DEFAULT_ROW_HEIGHT;
if ([self.tableView numberOfRowsInSection:0] > 0) {
cellHeight = [self tableView:self.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
}
CGFloat height = MIN(maxRowsToShow, numberOfResults) * cellHeight; // check if numberOfResults < maxRowsToShow
height = MAX(height, cellHeight); // if 0 results, set height = `cellHeight`
CGRect frame = self.textFieldRectOnWindow;
if ([self showSuggestionViewBelow]) {
CGFloat maxHeight = [UIScreen mainScreen].bounds.size.height - (frame.origin.y + frame.size.height) - INSET - self.keyboardFrameBeginRect.size.height; // max possible height
height = MIN(height, maxHeight); // set height = `maxHeight` if it's smaller than current `height`
frame.origin.y += frame.size.height;
} else {
CGFloat maxHeight = frame.origin.y - INSET; // max possible height
height = MIN(height, maxHeight); // set height = `maxHeight` if it's smaller than current `height`
frame.origin.y -= height;
}
frame.size.height = height;
self.tableView.frame = CGRectMake(0, 0, frame.size.width, frame.size.height);
self.tableContainerView.frame = frame;
}
// 8
- (void)updateCornerRadius {
// code snippet from SO answer (http://stackoverflow.com/a/13163693/1760199)
UIRectCorner corners = (UIRectCornerBottomLeft | UIRectCornerBottomRight);
if (![self showSuggestionViewBelow]) {
corners = (UIRectCornerTopLeft | UIRectCornerTopRight);
}
UIBezierPath *maskPath = [UIBezierPath
bezierPathWithRoundedRect:self.tableContainerView.bounds
byRoundingCorners:corners
cornerRadii:CGSizeMake(6, 6)
];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath;
self.tableContainerView.layer.mask = maskLayer;
}
// 9
- (void)checkForEmptyState {
if ([self tableView:self.tableView numberOfRowsInSection:0] == 0) {
UILabel *emptyTableLabel = [[UILabel alloc] initWithFrame:self.tableView.bounds];
emptyTableLabel.textAlignment = NSTextAlignmentCenter;
emptyTableLabel.font = [UIFont systemFontOfSize:16];
emptyTableLabel.textColor = [UIColor grayColor];
emptyTableLabel.text = @"No matches";
self.tableView.backgroundView = emptyTableLabel;
} else {
self.tableView.backgroundView = nil;
}
}
// 10
- (BOOL)showSuggestionViewBelow {
CGRect frame = self.textFieldRectOnWindow;
return frame.origin.y + frame.size.height/2.0 < ([UIScreen mainScreen].bounds.size.height - self.keyboardFrameBeginRect.size.height)/2.0;
}
Explanation:
- Add extra observers to handle
UITextField
changes. - In this method, we create all base views and setting initial frames.
- Method to show suggestion view.
- Method to hide suggestion view.
- Use above two methods to toggle between states.
- This method calls other methods to recalculate suggestion view frame, update corners and set text if data is empty.
- In this method, we decide where suggestion view should be shown (above or below) and also calculate the size.
- This method used to set corners on top or bottom regarding the position of suggestion view.
- Helper method to set text if data is empty.
- Another helper method to decide where to show suggestion view.
Sorry, if too many lines of code are present. I’ve very little experience in writing tutorials. I hope you will understand and forgive me :).
Now we need to implement UITableViewDatasource
and UITableViewDelegate
protocols but before let’s define our own. It will be used to interact with outer classes. Data to be shown will be passed through the protocol and also it will notify when something changed.
// in UITextField+AutoSuggestion.h
@protocol UITextFieldAutoSuggestionDataSource <NSObject>
- (UITableViewCell *)autoSuggestionField:(UITextField *)field tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath forText:(NSString *)text; // 1
- (NSInteger)autoSuggestionField:(UITextField *)field tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section forText:(NSString *)text; // 2
@optional
- (void)autoSuggestionField:(UITextField *)field textChanged:(NSString *)text; // 3
- (CGFloat)autoSuggestionField:(UITextField *)field tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath forText:(NSString *)text; // 4
- (void)autoSuggestionField:(UITextField *)field tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath forText:(NSString *)text; // 5
@end
@interface UITextField (AutoSuggestion)
// previous stuff
@property (weak, nonatomic) id<UITextFieldAutoSuggestionDataSource> autoSuggestionDataSource; // 6
@end
// in UITextField+AutoSuggestion.m
// 7
- (id<UITextFieldAutoSuggestionDataSource>)autoSuggestionDataSource {
return objc_getAssociatedObject(self, @selector(autoSuggestionDataSource));
}
- (void)setAutoSuggestionDataSource:(id<UITextFieldAutoSuggestionDataSource>)autoSuggestionDataSource {
objc_setAssociatedObject(self, @selector(autoSuggestionDataSource), autoSuggestionDataSource, OBJC_ASSOCIATION_ASSIGN);
}
Explanation:
- Using this method developers will have an opportunity to provide their custom designed cells.
- This method will ask the number of suggestions to show.
- This and below two methods are optional. We can use this method to handle field changes.
- Custom height for
UITableViewCell
. By default, height is 44 points. - Do something if the cell was selected.
- This is a variable to store our data source.
- Again we must define getter and setter for the new property.
Let’s now implement UITableViewDatasource
and UITableViewDelegate
methods combining with our own protocol.
// in UITextField+AutoSuggestion.h
// 1
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
// 2
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
BOOL implementsDatasource = self.autoSuggestionDataSource && [self.autoSuggestionDataSource respondsToSelector:@selector(autoSuggestionField:tableView:numberOfRowsInSection:forText:)];
NSAssert(implementsDatasource, @"UITextField must implement data source before using auto suggestion.");
return [self.autoSuggestionDataSource autoSuggestionField:self tableView:tableView numberOfRowsInSection:section forText:self.text];
}
// 3
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
BOOL implementsDatasource = self.autoSuggestionDataSource && [self.autoSuggestionDataSource respondsToSelector:@selector(autoSuggestionField:tableView:cellForRowAtIndexPath:forText:)];
NSAssert(implementsDatasource, @"UITextField must implement data source before using auto suggestion.");
return [self.autoSuggestionDataSource autoSuggestionField:self tableView:tableView cellForRowAtIndexPath:indexPath forText:self.text];
}
// 4
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.autoSuggestionDataSource && [self.autoSuggestionDataSource respondsToSelector:@selector(autoSuggestionField:tableView:heightForRowAtIndexPath:forText:)]) {
[self.autoSuggestionDataSource autoSuggestionField:self tableView:tableView heightForRowAtIndexPath:indexPath forText:self.text];
}
return DEFAULT_ROW_HEIGHT;
}
// 5
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if (self.autoSuggestionDataSource && [self.autoSuggestionDataSource respondsToSelector:@selector(autoSuggestionField:tableView:didSelectRowAtIndexPath:forText:)]) {
[self.autoSuggestionDataSource autoSuggestionField:self tableView:tableView didSelectRowAtIndexPath:indexPath forText:self.text];
}
[self hideAutoSuggestion];
}
// 6
- (void)toggleAutoSuggestion:(NSNotification *)notification {
if (self.text.length > 0) {
[self showAutoSuggestion];
if ([self.autoSuggestionDataSource respondsToSelector:@selector(autoSuggestionField:textChanged:)]) {
[self.autoSuggestionDataSource autoSuggestionField:self textChanged:self.text];
}
} else {
[self hideAutoSuggestion];
}
}
Explanation:
- The number of sections always will be 1.
- The number of rows will be asked from
UITextField
data source we created. If not implemented, assert will stop the application and show the error message. - The same as above but instead this method will ask for
UITableViewCell
. - Height for cells. By default is
DEFAULT_ROW_HEIGHT
which is equal to 44 points. - This method will hide auto suggestion view and call data source method if implemented.
- In above code snippets, we already implemented this method. Now we extend it by adding our data source to it. Data source method will be fired if implemented.
And the last step is to add loading state.
// in UITextField+AutoSuggestion.h
- (void)setLoading:(BOOL)loading;
// in UITextField+AutoSuggestion.m
- (void)setLoading:(BOOL)loading {
if (loading) {
if (!self.tableAlphaView) {
self.tableAlphaView = [[UIView alloc] initWithFrame:self.tableView.bounds];
self.tableAlphaView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.8];
[self.tableView addSubview:self.tableAlphaView];
self.spinner = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(0, 0, 24, 24)];
self.spinner.center = self.tableAlphaView.center;
self.spinner.color = [UIColor blackColor];
[self.tableAlphaView addSubview:self.spinner];
[self.spinner startAnimating];
}
} else {
if (self.tableAlphaView) {
[self.spinner startAnimating];
[self.spinner removeFromSuperview];
self.spinner = nil;
[self.tableAlphaView removeFromSuperview];
self.tableAlphaView = nil;
}
}
}
To use this category we should specify which class will conform to the data source and implement its methods.
For example in demo project it looks like this:
// 1
@interface ViewController () </* other protocols */, UITextFieldAutoSuggestionDataSource>
@end
@implementation ViewController
// in `viewDidLoad:` or any other method where views are loaded
// 2
- (void)viewDidLoad {
// other stuff
self.textField1.autoSuggestionDataSource = self;
self.textField1.fieldIdentifier = @"FIELD_ID";
[self.textField1 observeTextFieldChanges];
}
@end
#pragma mark - UITextFieldAutoSuggestionDataSource
// 3
- (UITableViewCell *)autoSuggestionField:(UITextField *)field tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath forText:(NSString *)text {
static NSString *cellIdentifier = @"AutoSuggestionCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
// configure cell and fill with some data
cell.textLabel.text = @"some data";
return cell;
}
// 4
- (NSInteger)autoSuggestionField:(UITextField *)field tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section forText:(NSString *)text {
return DATA.count; // where DATA is your suggestion model
}
// 5
- (CGFloat)autoSuggestionField:(UITextField *)field tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath forText:(NSString *)text {
return 50; // as an example
}
// 6
- (void)autoSuggestionField:(UITextField *)field tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath forText:(NSString *)text {
self.textField2.text = DATA.count[indexPath.row];
}
Explanation:
- Conforming to the protocol of our category.
- Setting which field should have auto suggestion feature and calling
observeTextFieldChanges()
method to start observing changes.
Steps 3, 4, 5 and 6 and just implementations of data source methods.
That’s it! It was good experience writing our own UITextField
category.
Conclusion
As we can see categories are the powerful feature of the Objective-C language. By using it we can extend the functionality of any base API class.
But they should be used carefully. For example, when I first implemented auto suggestion feature I implemented the dealloc
method. After that, strange memory crashes occurred. Which means - do not override dealloc
method in categories.
Thank you for your attention!
PS: Full code can be found at this link.
Leave a Comment