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

Address Completion Control #36

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
*~
*.swp
xcuserdata
Classes/apiKeys.h
11 changes: 11 additions & 0 deletions Classes/HTAddressAutocompleteTextField.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// HTAddressAutocompleteTextField.h
// HTTextFieldAutocompletionExample
//
// Created by Kav Latiolais on 8/4/14.
// Copyright (c) 2014 Hotel Tonight. All rights reserved.
//

#import "HTAutocompleteTextField.h"
@interface HTAddressAutocompleteTextField : HTAutocompleteTextField <HTAutocompleteDataSource>
@end
124 changes: 124 additions & 0 deletions Classes/HTAddressAutocompleteTextField.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//
// HTAddressAutocompleteTextField.m
// HTTextFieldAutocompletionExample
//
// Created by Kav Latiolais on 8/4/14.
// Copyright (c) 2014 Hotel Tonight. All rights reserved.
//
const CGFloat kFetchDelay = 1;
const NSInteger kBeginRemoteCompletingAfter = 3;

//Need to add your own Classes/apiKeys.h that defines:
//const NSString *kGoogleKey = @"<Your Google Key>";

#import "apiKeys.h"

#import "HTAddressAutocompleteTextField.h"
@interface HTAddressAutocompleteTextField ()
@property (nonatomic, strong) NSURLSessionDataTask *fetchAddressSuggestionTask;
@property (nonatomic, strong) NSOrderedSet *predictions;
@property (nonatomic, strong) NSTimer *fetchDelayTimer;
@end

@implementation HTAddressAutocompleteTextField

-(void)setupAutocompleteTextField {
[super setupAutocompleteTextField];

_predictions = [[NSMutableOrderedSet alloc] init];
_fetchDelayTimer = [[NSTimer alloc] initWithFireDate:nil interval:0 target:self selector:@selector(fetchData) userInfo:nil repeats:NO];
self.autocompleteDataSource = self;
}
-(void) fetchData{
if(self.fetchAddressSuggestionTask.state == NSURLSessionTaskStateSuspended){
[self.fetchAddressSuggestionTask resume];
}
_fetchDelayTimer = [[NSTimer alloc] initWithFireDate:nil interval:0 target:self selector:@selector(fetchData) userInfo:nil repeats:NO];
}

#pragma mark - HTAutocompleteTextFieldDataSource
- (BOOL)textFieldShouldReplaceCompletion:(HTAutocompleteTextField*)textField{
return YES;
}
- (void)textField:(HTAutocompleteTextField *)textField asyncCompletionForPrefix:(NSString *)prefix ignoreCase:(BOOL)ignoreCase completionHandler:(void (^)(NSString *))completionHandler {
if (prefix.length == 0) {
return completionHandler(@"");
}
NSString *locallySavedAddress;
if ([self.predictions count] > 0) {
locallySavedAddress = [self checkLocalForAddress:prefix];
}

if (locallySavedAddress) {
return completionHandler(locallySavedAddress);
} else {
if (prefix.length > kBeginRemoteCompletingAfter) {
if(self.fetchAddressSuggestionTask) {
[self.fetchAddressSuggestionTask cancel];
self.fetchAddressSuggestionTask = nil;
}

NSString *encodedPrefix = [prefix stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];


NSString *urlString = [NSString stringWithFormat:@"https://maps.googleapis.com/maps/api/place/autocomplete/json?input=%@&types=geocode&components=country:US&key=%@", encodedPrefix, kGoogleKey];

NSURL *url = [NSURL URLWithString:urlString];
self.fetchAddressSuggestionTask = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error) {
NSError *jsonError;
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
NSArray *predictions = json[@"predictions"];
NSString *address = @"";
if(predictions.count > 0){
address = predictions[0][@"description"];
address = [address stringByReplacingOccurrencesOfString:@", United States" withString:@""];
}

[self addToCache:json forPrefix:prefix];

return completionHandler(address);
}
}];
self.fetchAddressSuggestionTask.taskDescription = prefix;
[self.fetchDelayTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:kFetchDelay]];
[[NSRunLoop currentRunLoop] addTimer:self.fetchDelayTimer forMode:NSDefaultRunLoopMode];
}
}
}
-(BOOL)textFieldShouldReplaceCompletionText:(HTAutocompleteTextField *)textField {
return YES;
}
- (void) addToCache:(NSDictionary *)predictionsDictionary forPrefix:(NSString *)prefix{
NSArray *addresses = predictionsDictionary[@"predictions"];
NSMutableOrderedSet *predictions = self.predictions.mutableCopy;
for (int i = 0; i < addresses.count; i++) {

NSDictionary *location = addresses[i];
NSArray *locationTypes = location[@"types"];
NSArray *matchedSubstrings = location[@"matched_substrings"];

if(![locationTypes containsObject:@"street_address"]) break;
if(![matchedSubstrings containsObject:@{@"length": @(prefix.length), @"offset": @(0)}]) break;

NSString *title = location[@"description"];
title = [title stringByReplacingOccurrencesOfString:@", United States" withString:@""];
NSNumber *rank = @(prefix.length*10 + i);
[predictions addObject:@{@"title":title, @"rank":rank}];
}
NSSortDescriptor *rankSort = [NSSortDescriptor sortDescriptorWithKey:@"rank" ascending:YES];
[predictions sortUsingDescriptors:@[rankSort]];
self.predictions = predictions;
}

- (NSString *)checkLocalForAddress:(NSString *)prefix {
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"title BEGINSWITH %@", prefix];

NSOrderedSet *completions = [self.predictions filteredOrderedSetUsingPredicate:predicate];
if(completions.count > 0 ){
return completions[0][@"title"];
}
return nil;
}

@end
13 changes: 7 additions & 6 deletions Classes/HTAutocompleteTextField.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,25 @@
//

#import <UIKit/UIKit.h>

@class HTAutocompleteTextField;

@protocol HTAutocompleteDataSource <NSObject>

@optional
- (NSString*)textField:(HTAutocompleteTextField*)textField
completionForPrefix:(NSString*)prefix
ignoreCase:(BOOL)ignoreCase;

- (void)textField:(HTAutocompleteTextField*)textField
asyncCompletionForPrefix:(NSString*)prefix
ignoreCase:(BOOL)ignoreCase
completionHandler:(void (^)(NSString *completion))completionHandler;
- (BOOL)textFieldShouldReplaceCompletion:(HTAutocompleteTextField*)textField;
@end

@protocol HTAutocompleteTextFieldDelegate <NSObject>

@optional
- (void)autoCompleteTextFieldDidAutoComplete:(HTAutocompleteTextField *)autoCompleteField;
- (void)autocompleteTextField:(HTAutocompleteTextField *)autocompleteTextField didChangeAutocompleteText:(NSString *)autocompleteText;

@end

@interface HTAutocompleteTextField : UITextField
Expand All @@ -41,7 +43,6 @@
@property (nonatomic, assign) NSUInteger autocompleteType; // Can be used by the dataSource to provide different types of autocomplete behavior
@property (nonatomic, assign) BOOL autocompleteDisabled;
@property (nonatomic, assign) BOOL ignoreCase;
@property (nonatomic, assign) BOOL needsClearButtonSpace;
@property (nonatomic, assign) BOOL showAutocompleteButton;
@property (nonatomic, assign) id<HTAutocompleteTextFieldDelegate> autoCompleteTextFieldDelegate;

Expand All @@ -55,7 +56,7 @@
/*
* Specify a data source responsible for determining autocomplete text.
*/
@property (nonatomic, assign) id<HTAutocompleteDataSource> autocompleteDataSource;
@property (nonatomic, weak) id<HTAutocompleteDataSource> autocompleteDataSource;
+ (void)setDefaultAutocompleteDataSource:(id<HTAutocompleteDataSource>)dataSource;

/*
Expand Down
69 changes: 51 additions & 18 deletions Classes/HTAutocompleteTextField.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,12 @@
static NSObject<HTAutocompleteDataSource> *DefaultAutocompleteDataSource = nil;

@interface HTAutocompleteTextField ()

@property (nonatomic, strong) NSString *autocompleteString;
@property (nonatomic, strong) UIButton *autocompleteButton;

@end

@implementation HTAutocompleteTextField

- (id)initWithFrame:(CGRect)frame
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
Expand Down Expand Up @@ -190,7 +187,8 @@ - (void)updateAutocompleteLabel
[self.autocompleteLabel setText:self.autocompleteString];
[self.autocompleteLabel sizeToFit];
[self.autocompleteLabel setFrame: [self autocompleteRectForBounds:self.bounds]];

[self sendActionsForControlEvents:UIControlEventEditingChanged];

if ([self.autoCompleteTextFieldDelegate respondsToSelector:@selector(autocompleteTextField:didChangeAutocompleteText:)]) {
[self.autoCompleteTextFieldDelegate autocompleteTextField:self didChangeAutocompleteText:self.autocompleteString];
}
Expand All @@ -202,36 +200,51 @@ - (void)refreshAutocompleteText
{
id <HTAutocompleteDataSource> dataSource = nil;

if ([self.autocompleteDataSource respondsToSelector:@selector(textField:completionForPrefix:ignoreCase:)])
if ([self.autocompleteDataSource respondsToSelector:@selector(textField:completionForPrefix:ignoreCase:)] || [self.autocompleteDataSource respondsToSelector:@selector(textField:asyncCompletionForPrefix:ignoreCase:completionHandler:)])
{
dataSource = (id <HTAutocompleteDataSource>)self.autocompleteDataSource;
}
else if ([DefaultAutocompleteDataSource respondsToSelector:@selector(textField:completionForPrefix:ignoreCase:)])
else if ([DefaultAutocompleteDataSource respondsToSelector:@selector(textField:completionForPrefix:ignoreCase:)] || [DefaultAutocompleteDataSource respondsToSelector:@selector(textField:asyncCompletionForPrefix:ignoreCase:completionHandler:)])
{
dataSource = DefaultAutocompleteDataSource;
}

if (dataSource)
{
self.autocompleteString = [dataSource textField:self completionForPrefix:self.text ignoreCase:self.ignoreCase];

if (self.autocompleteString.length > 0)
{
if (self.text.length == 0 || self.text.length == 1)
{
[self updateAutocompleteButtonAnimated:YES];
}
if([dataSource respondsToSelector:@selector(textField:completionForPrefix:ignoreCase:)]){
[self completeRefreshText:[dataSource textField:self completionForPrefix:self.text ignoreCase:self.ignoreCase]];

} else if([dataSource respondsToSelector:@selector(textField:asyncCompletionForPrefix:ignoreCase:completionHandler:)]){
[self updateAutocompleteLabel];
NSString *asyncText = self.text.copy;
[dataSource textField:self asyncCompletionForPrefix:self.text ignoreCase:self.ignoreCase completionHandler:^(NSString *completion) {
if([self.text isEqualToString:asyncText]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self completeRefreshText:completion];
});
}
}];
}

[self updateAutocompleteLabel];
}
}
}

-(void)completeRefreshText:(NSString *)text {
self.autocompleteString = text;
if (self.autocompleteString.length > 0)
{
if (self.text.length == 0 || self.text.length == 1)
{
[self updateAutocompleteButtonAnimated:YES];
}
}
[self updateAutocompleteLabel];
}

- (BOOL)commitAutocompleteText
{
NSString *currentText = self.text;
if ([self.autocompleteString isEqualToString:@""] == NO
if (self.autocompleteString.length > 0
&& self.autocompleteDisabled == NO)
{
self.text = [NSString stringWithFormat:@"%@%@", self.text, self.autocompleteString];
Expand All @@ -252,7 +265,27 @@ - (void)forceRefreshAutocompleteText
}

#pragma mark - Accessors
@synthesize autocompleteString = _autocompleteString;

-(NSString *)autocompleteString {
id<HTAutocompleteDataSource> datasource;
if([self.autocompleteDataSource respondsToSelector:@selector(textFieldShouldReplaceCompletion:)]) {
datasource = self.autocompleteDataSource;
} else if([DefaultAutocompleteDataSource respondsToSelector:@selector(textFieldShouldReplaceCompletion:)]) {
datasource = DefaultAutocompleteDataSource;
}

if([datasource textFieldShouldReplaceCompletion:self]) {
NSStringCompareOptions *options = self.ignoreCase? NSCaseInsensitiveSearch : 0;

if([_autocompleteString rangeOfString:self.text options:options].location == 0){
return [_autocompleteString substringFromIndex:self.text.length];
} else {
return @"";
}
}
return _autocompleteString;
}
- (void)setAutocompleteString:(NSString *)autocompleteString
{
_autocompleteString = autocompleteString;
Expand Down
16 changes: 14 additions & 2 deletions HTTextFieldAutocompletionExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
5EE15CDE1992AE7B00657904 /* HTAsyncAutocompleteManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EE15CDD1992AE7B00657904 /* HTAsyncAutocompleteManager.m */; };
5EE15CE11992C8C500657904 /* HTAddressAutocompleteTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 5EE15CE01992C8C500657904 /* HTAddressAutocompleteTextField.m */; };
8D86FEEE16E96D4800534BDD /* MainStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8D86FEED16E96D4800534BDD /* MainStoryboard.storyboard */; };
8DF4A805168B7C610025E16D /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DF4A804168B7C610025E16D /* UIKit.framework */; };
8DF4A807168B7C610025E16D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DF4A806168B7C610025E16D /* Foundation.framework */; };
Expand All @@ -28,6 +30,10 @@

/* Begin PBXFileReference section */
541886FBCF924E768C27BA1C /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; };
5EE15CDC1992AE7B00657904 /* HTAsyncAutocompleteManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTAsyncAutocompleteManager.h; sourceTree = "<group>"; };
5EE15CDD1992AE7B00657904 /* HTAsyncAutocompleteManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTAsyncAutocompleteManager.m; sourceTree = "<group>"; };
5EE15CDF1992C8C500657904 /* HTAddressAutocompleteTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTAddressAutocompleteTextField.h; path = Classes/HTAddressAutocompleteTextField.h; sourceTree = SOURCE_ROOT; };
5EE15CE01992C8C500657904 /* HTAddressAutocompleteTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTAddressAutocompleteTextField.m; path = Classes/HTAddressAutocompleteTextField.m; sourceTree = SOURCE_ROOT; };
8D86FEED16E96D4800534BDD /* MainStoryboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MainStoryboard.storyboard; sourceTree = "<group>"; };
8DF4A800168B7C610025E16D /* HTTextFieldAutocompletionExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HTTextFieldAutocompletionExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
8DF4A804168B7C610025E16D /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
Expand All @@ -49,8 +55,8 @@
8DFB04B117FD01AB00E7B196 /* ht-logo-black.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "ht-logo-black.png"; sourceTree = "<group>"; };
8DFB04B317FD01F500E7B196 /* HTEmailAutocompleteTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTEmailAutocompleteTextField.m; sourceTree = "<group>"; };
8DFB04B417FD01F500E7B196 /* HTEmailAutocompleteTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTEmailAutocompleteTextField.h; sourceTree = "<group>"; };
8DFB04B517FD01F500E7B196 /* HTAutocompleteTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTAutocompleteTextField.m; sourceTree = "<group>"; };
8DFB04B617FD01F500E7B196 /* HTAutocompleteTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTAutocompleteTextField.h; sourceTree = "<group>"; };
8DFB04B517FD01F500E7B196 /* HTAutocompleteTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTAutocompleteTextField.m; path = ../Classes/HTAutocompleteTextField.m; sourceTree = "<group>"; };
8DFB04B617FD01F500E7B196 /* HTAutocompleteTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTAutocompleteTextField.h; path = ../Classes/HTAutocompleteTextField.h; sourceTree = "<group>"; };
C80AC03B16DF7B4100755F19 /* [email protected] */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "[email protected]"; sourceTree = "<group>"; };
C80AC03C16DF7B4100755F19 /* autocompleteButton.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = autocompleteButton.png; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -107,6 +113,8 @@
8DF4A814168B7C610025E16D /* HTAppDelegate.m */,
8DF4A849168B80CD0025E16D /* HTAutocompleteManager.h */,
8DF4A84A168B80CD0025E16D /* HTAutocompleteManager.m */,
5EE15CDC1992AE7B00657904 /* HTAsyncAutocompleteManager.h */,
5EE15CDD1992AE7B00657904 /* HTAsyncAutocompleteManager.m */,
8DF4A81C168B7C610025E16D /* HTSampleFieldsTableViewController.h */,
8DF4A81D168B7C610025E16D /* HTSampleFieldsTableViewController.m */,
8D86FEED16E96D4800534BDD /* MainStoryboard.storyboard */,
Expand All @@ -132,6 +140,8 @@
8DF4A83A168B7CFC0025E16D /* Core */ = {
isa = PBXGroup;
children = (
5EE15CDF1992C8C500657904 /* HTAddressAutocompleteTextField.h */,
5EE15CE01992C8C500657904 /* HTAddressAutocompleteTextField.m */,
8DFB04B317FD01F500E7B196 /* HTEmailAutocompleteTextField.m */,
8DFB04B417FD01F500E7B196 /* HTEmailAutocompleteTextField.h */,
8DFB04B517FD01F500E7B196 /* HTAutocompleteTextField.m */,
Expand Down Expand Up @@ -220,9 +230,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5EE15CE11992C8C500657904 /* HTAddressAutocompleteTextField.m in Sources */,
8DF4A811168B7C610025E16D /* main.m in Sources */,
8DFB04B817FD01F500E7B196 /* HTAutocompleteTextField.m in Sources */,
8DF4A815168B7C610025E16D /* HTAppDelegate.m in Sources */,
5EE15CDE1992AE7B00657904 /* HTAsyncAutocompleteManager.m in Sources */,
8DFB04B717FD01F500E7B196 /* HTEmailAutocompleteTextField.m in Sources */,
8DF4A81E168B7C610025E16D /* HTSampleFieldsTableViewController.m in Sources */,
8DF4A84B168B80CD0025E16D /* HTAutocompleteManager.m in Sources */,
Expand Down
17 changes: 17 additions & 0 deletions HTTextFieldAutocompletionExample/HTAsyncAutocompleteManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// HTAsyncAutocompleteManager.h
// HTTextFieldAutocompletionExample
//
// Created by Kav Latiolais on 8/6/14.
// Copyright (c) 2014 LIFFFT.
//


#import <Foundation/Foundation.h>
#import "HTAutocompleteTextField.h"

@interface HTAsyncAutocompleteManager : NSObject <HTAutocompleteDataSource>

+ (HTAsyncAutocompleteManager *)sharedManager;

@end
Loading