Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UISearchResultsUpdating w/ ReactiveCocoa

I'm building a UISearchController in which users will type a username, and the application will fetch results from a web service.

I want to throttle the requests to reduce network calls while the user is typing. Using ReactiveCocoa how would one go about implementing this?

class SearchResultsUpdater: NSObject, UISearchResultsUpdating {
    func updateSearchResultsForSearchController(searchController: UISearchController) {
        let text = searchController.searchBar.text
        let dataSource = searchResultsController.tableView.dataSource as! ...     
    }
}
like image 228
Kyle Decot Avatar asked Apr 30 '15 20:04

Kyle Decot


3 Answers

Actually is very good approach to solve this problem with ReactiveCocoa.

You want to get the text while the user is typing or in 'Reactive' word you want “Stream” of input text, If your user is a fast typer you want to perform request to server only if the search text is unchanged for a short amount of time, You can do it yourself (using Delegate, NSTimer), but with ReactiveCocoa is really simple and readable.

@property (weak, nonatomic) IBOutlet UISearchBar *searchBar;

[[textSignal throttle:0.4]subscribeNext:^(NSString* searchText) {
    [[SearchService sharedInstance]search:searchText completed:^(NSString *searchResult, NSError *error) {
       NSLog(@"searchResult: %@",searchResult);
    }];
}];

Let's say your class SearchService returns the searchText and searchText length after 2.5 seconds.

 @implementation SearchService

typedef void(^CompletedResults)(NSString *searchResult, NSError *error);

- (void)search:(NSString *)text completed:(CompletedResults)handler {

    NSString *retVal = [NSString stringWithFormat:@"%@ = %@", text, @([text length])];

    // Here you should to do your network call and and return the response string
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [NSThread sleepForTimeInterval:2.5];
        if (handler){
            handler(retVal, nil);
        }
    });
}

And with just line of code you throttle the input text.

actually ReactiveCocoa doesn't provide a category of UISearchBar, but it's not so complicated to implement (You can find UISearchBar(RAC) category hire)

Important thing you want to ask yourself is, what will happen if you’ve already sent request to the server and before you get the answer the user continue to typing? You probably want to cancel the previous request (and release all the resources) and send a new request to the server with the new search text. again you can do it yourself but with ReactiveCocoa is very simple, if you just start thinking about things as signals.

You should to wrap your search service that return "stream" of result from the server.

@implementation SearchService

- (RACSignal *)search:(NSString *)text {

    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        [self search:text completed:^(NSString *searchResult, NSError *error) {
            [subscriber sendNext:searchResult];
            [subscriber sendCompleted];
        }];
        return nil;
    }];
}

Now all you have to do is to map each search text to signal of result from the server and call to switchToLatest.

[[[[textSignal throttle:0.4]
map:^id(NSString* searchText) {
    return [[SearchService sharedInstance]search:searchText];
}]
switchToLatest]
subscribeNext:^(NSString* searchResult) {
    NSLog(@"searchResult: %@",searchResult);
}];

And one more thing, probably when you get the response from the server you want to update the UI. And you have to do it on the main thread. Also here with ReactiveCocoa it is really simple, just add deliverOn:RACScheduler.mainThreadScheduler.

[[[[[textSignal throttle:0.4]
map:^id(NSString* searchText) {
     NSLog(@"Get Text after throttle");
    return [[SearchService sharedInstance]search:searchText];
}]
switchToLatest]
deliverOn:RACScheduler.mainThreadScheduler]
subscribeNext:^(NSString* searchResult) {

    if ([NSThread isMainThread]){
        NSLog(@"is MainThread");
    }
    else{
         NSLog(@"is not MainThread");
    }
    NSLog(@"searchResult: %@",searchResult);
}];

Good luck :)

If you write your code with Swift, take a look at ReactiveSwift GitHub - Reactive extensions for Swift, Inspired by ReactiveCocoa

like image 114
Guy Kahlon Avatar answered Sep 20 '22 01:09

Guy Kahlon


Sorry I'm not very familiar with the RAC Swift API, but this is achievable in the Objective-C version of RAC by calling the bufferWithTime:onScheduler: method on a RACSignal, so it'll undoubtedly have a Swift counterpart.

Example:

double sampleRate = 2.0;
[[textField.rac_textSignal bufferWithTime:sampleRate onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(RACTuple * x) {
    NSLog(@"%@", x.last); //Prints the latest string in the tuple.
}];

Incorporating this with UISearchController:

double sampleRate = 2.0;
[[[self rac_signalForSelector:@selector(searchBar:textDidChange:) fromProtocol:@protocol(UISearchBarDelegate)] 
bufferWithTime:sampleRate onScheduler:[RACScheduler mainThreadScheduler]] 
subscribeNext:^id(RACTuple * x) {
    NSLog(@"%@", x.last);
}];

Here's a blog post about giving a UISearchController a rac_textSignal option, so that you don't have to implement a delegate function yourself, whereas with the code above, you'll need to still have an empty searchBar:textDidChange: function in SearchResultsUpdater.

like image 29
Charles Maria Avatar answered Sep 19 '22 01:09

Charles Maria


This may not be what you are looking for, but it may help you get there. I used the NSTimer extension in this gist: https://gist.github.com/natecook1000/b0285b518576b22c4dc8

let (keySignal, keySink) = Signal<String, NoError>.pipe()

func createIntervalSignal(interval: Double) -> Signal<(), NoError> {
    return Signal {
        sink in
        NSTimer.schedule(repeatInterval: interval) { timer in
            sendNext(sink, ())
        }
        return nil
    }
}

func textFieldChanged(sender:UITextField) {
    sendNext(keySink, sender.text)
}

let sendNetworkRequestSignal = keySignal |> sampleOn(createIntervalSignal(1.0))
let disposeThis = sendNetworkRequestSignal |> observe(next: { stringVal in  }) //send requests in the closure
like image 24
jjc2661 Avatar answered Sep 20 '22 01:09

jjc2661