ReactiveCocoa: throttle never executes / not working - objective-c

I try to subscribe to a signal with throttle, but it never executes.
I have a UISearchController (Attention: UISearchController from iOS8, not the older UISearchDisplayController, which works quiet better and has thousands of working tutorials and examples in the web) and want to make API-Requests while the user is typing.
To let the traffic being low, i don't want to start API-Requests with each key the user is pressing, but when the user stops for a while, say 500 ms after the last keypress.
Since we're unable to reference the TextField in the SearchBar of the UISearchController, we use the delegates from UISearchController:
To get the latest typed text of the Textfield in the Searchbar, I use this:
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
NSString *searchText = searchController.searchBar.text;
// strip out all the leading and trailing spaces
NSString *strippedString = [searchText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if([strippedString isEqualToString:self.currentFilter]) {
return;
}
self.currentFilter = strippedString;
}
The property currentFilter keeps the current search string.
Also, i have a RACObserve on the currentFilter-Property to react on every change which is made to this property:
[RACObserve(self, currentFilter) subscribeNext:^(NSString* x) {
NSLog(#"Current Filter: %#", x);
// do api calls and everything else
}];
Now i want to throttle this signal. But when i implement the call to throttle, nothing happens. SubscribeNext will never be called:
[[RACObserve(self, currentFilter) throttle:500] subscribeNext:^(NSString* x) {
NSLog(#"%#", x); // will never be called
}];
How to achieve to throttle inputs in a searchbar? And what is wrong here?
UPDATE
i found a workaround besides ReactiveCocoa thanks to #malcomhall. I moved the code within the updateSearchResultsForSearchController-delegate method into a separate method and schedule it with performSelector and cancel this scheduler with cancelPreviousPerformRequestsWithTarget.
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(useTextSearchQuery) object:nil];
[self performSelector:#selector(useTextSearchQuery) withObject:nil afterDelay:1.0];
}
Anyway, i want still understand how "throttle" from ReactiveCocoa is working and why not in this case :)

-throttle: accepts an NSTimeInterval, which is a floating-point specification of seconds, not milliseconds.
Given the code in the question, I expect you would see results after 500 seconds have elapsed.

Related

Cocoa app behaves diffirently with breakpoint on & off

Important update: I found out that most part of my question was based on a false premise (see my answer below). Notifications actually got to the receiver, they just got there too fast. (Although, it still doesn't explain why the behavior with breakpoint and without it was different.)
I'm developing the app that calculates the hashes of files given to it. The calculation takes place in SHHashComputer. It's an abstract class (well, intended to be abstract, as there are no abstract classes in Objective C) that takes the file path and creates an NSInvocationOperation. It, in turn, calls the method (void)computeAndSendHash, which uses the file path saved in the object to compute hash and sends it as notification. The actual computing takes place in (NSString*)computeHash method that child classes need to override.
Here's SHHashComputer.m:
- (NSString*)computeHash {
return [NSString stringWithFormat:#"unimplemented hash for file %#", self.path];
}
- (void)computeAndSendHash {
NSString *result = [self computeHash];
NSString *notificationName = [NSString stringWithFormat:#"%#%#",
gotResultNotification,
self.hashType];
[[NSNotificationCenter defaultCenter] postNotificationName:notificationName
object:result];
self.operation = nil;
}
And here's SHMD5Computer.m (the child class of SHHashComputer):
- (NSString*)computeHash {
return #"MD5 test"; // it actually doesn't matter what it returns
}
I won't bother you with the receivers of notification. Let's just say that as long as I comment out the computeHash method in SHMD5Computer.m everything works just fine: the notification with text "unimplemented ..." is received & displayed in GUI. But if I don't — then it gets really interesting.
If I don't set up any breakpoints, the notification just never comes. However, if I set up a breakpoint at the declaration of computeHash in SHMD5Computer.h and then step over until the line 'self.operation = nil', and continue execution at that point, the notification gets to destination. If I don't stop there, the debugger suddenly switches to the state as if it isn't debugging anything, and the app freezes.
I don't think that 'WTF' is a good form for a question here, so let me put it this way: am I missing something? Are there errors in my code? What can cause this type of behavior in xcode? How can I fix this?
(If you'll want to get all my code to reproduce it, I'll gladly give it to you.)
More experiments:
If I continute execution exactly after stopping at breakpoint, the application encounters EXC_BAD_ACCESS error in the code that receives the notification, at the last line:
id newResult = [newResultNotification object];
if (newResult == nil)
[NSException raise:#"No object"
format:#"Expected object with notification!"];
else if (![newResult isKindOfClass:[NSString class]])
[NSException raise:#"Not NSString"
format:#"Expected NSString object!"];
else
self.result = (NSString*) newResult;
[self.textField setStringValue:self.result];
When I tried to reproduce the previous experiment, something even stranger happenned. In my debug setup, I have two hash computer objects: one SHMD5HashComputer (which we're talking about), and one SHHashComputer (which, of course, produces the "unimpemented" hash). In all previous experiments, as long as app didn't crash, the notification form SHHashComputer always successfully arrived. But in this case, both notifications didn't arrive, and the app didn't crash. (All the steps are exactly the same as in previous one).
As Josh Caswell pointer out in the comments, I wasn't using the notifications correctly. I should've sent the object itself as notification object, as described in documentation. I fixed that, and I'm getting exactly the same results. (Which means that I fixed it correctly, because sometimes the notifications work correctly, and also that it wasn't the problem).
More updates:
The notification that I'm sending should arrive at SHHashResultViewController. That's how I create it and register for notification:
- (id)initWithHashType:(NSString *)hashType {
self = [self initWithNibName:#"SHHashResultView" bundle:[NSBundle mainBundle]];
if (self) {
[self setHashType:hashType];
}
return self;
}
- (void)setHashType:(NSString *)hashType {
[self.label setStringValue:[NSString stringWithFormat:#"%#:", hashType]];
_hashType = hashType;
NSString *notificationName = [NSString stringWithFormat:#"%#%#",
gotResultNotification,
_hashType];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(gotResult:)
name:notificationName
object:nil];
}
Actually, the question was based on a false premise. I thought that notification never came through because I never saw the information displayed in the GUI; however, my error was in the code of controllers (not published there) which made possible the situation in which the GUI first got results of hash calculation and only after that got information about a new input — which resulted in deleting all the text and activating progress animation.

Perform action after label is updated in ios

I am using a pin screen for login to my app. The pin screen consists of four labels and a hidden text field. When the user enters text via the keypad, I update the labels with a symbol. This works fine, except that the last label does not get actually get updated before login begins, and remains empty while the login process is completed.
These are the relevant bits of code:
//an observer has been added elsewhere
- (void)textDidChange:(NSNotification *)notification
{
UITextField *field = [notification object];
if (field == inputField)
{
NSString *newText = field.text;
if ([newText length] <= pinLength) [self updatePINDisplay];
}
}
-(void)updatePINDisplay
{
if ([pinText length] > pinLength) return;
for (NSInteger ii = 0; ii < [pinText length]; ii++)
{
UILabel *label = [pinFields objectAtIndex:ii];
[label setText:#"x"];
}
for (NSInteger ii = [pinText length]; ii < pinLength; ii++)
{
UILabel *label = [pinFields objectAtIndex:ii];
[label setText:[NSString string]];
}
if ([pinText length] == pinLength) [self login];
}
The problem arises because [self login] launches other processes which happen before the last pin label is updated, so the login occurs while the last box is still empty.
I have worked around the problem by replacing
[self login]
with
[self performSelector:#selector(login) withObject:nil afterDelay:0.1]
but I don't like the arbitrary time delay. I was hoping that maybe there was a delegate method that I could use to launch my login code after the label has been drawn. Something like:
-(void)labelDidGetDrawn
Any other (non-hack) solution is also welcome:-)
Thanks!
Based on your description, it sounds like the problem is that the 4th item doesn't get drawn until after the [self login] finishes, which is indicative that the login procedure takes some time. In iOS, drawing doesn't happen immediately, which is why you're only getting the draw if you defer the login until after the OS has an opportunity to update the display.
You have used one reasonable solution here. Another (arguably less of a hack) is to have your -[self login] spawn the login on a separate thread, or at least using an asynchronous mechanism (such as the asynchronous modes of NSURLConnection, assuming you're making a network request). Then your main thread will quickly return control to iOS and your box will draw.
With Grand Central Dispatch, you could do most of this by having the -[self login] place the network code on a background thread, and have the background thread call back to your main thread when complete. However, this can cause some problems if you want to respond to user events during the login process.
If you can, using NSURLConnection asynchronously, and setting up the delegate to report back to you when the operation is complete is probably the best choice, as it gives you the operation to cancel the NSURLConnection during the login process if the user requests it.
How about:
[label setNeedsDisplay:YES];
if ([pinText length] == pinLength) [self login];
Yes, that notification exists, in a way. The label will be drawn during the next iteration of the run loop. So do your login at the end of the next run loop iteration, for instance using a performSelector:afterDelay:0 or maybe using
dispatch_async (dispatch_get_main_queue (), ^{ [self login]; });
But a) this depends on the order of execution of rendering versus timers and dispatch_queues. If rendering happens before timer execution, you're all set.
And b) don't block the main thread. Try to perform the login in a background thread/concurrent queue, or do it asynchronously on the main thread if you're using, e.g., NSURLConnection.

NSXMLParser and multiple calls in view's life-cycle.

Currently i'm having trouble using two functions in my -(void)viewDidLoad, both of these functions uses NSUrlRequest to send HTTPPost to a webservice to recieve data.
It works fine untill [self bar] decides to kick in before [self foo] is completely finished. So, is there any smart way of checking if [self bar] is completely finished before starting [self foo]?
-(void)viewDidLoad{
[self foo]; // initiates a nsxmlparsercall to a webservice to get values.
[self bar]; // relies on the values recieved from [self foo] to make it's own call.
/* However, [self bar] always crashes before initiating it's request.
/* It crashes when the variables that are being sent with the poststring
/* are being set, as they are null.
/* Which means that the `[self foo]` doesnt get completed before starting [self bar];
}
I might be very off at this point, i've even considered overriding -(void)viewDidload and setting a bool to control when it's ok to fire the second function, but that seems like super poor coding..
Any suggestions and/or tips on how to point me in the right direction will be highly appreciated. Thanks in advance.
I best place to put your function will be one of the delegate methods of nsxmlparser that is
- (void)parserDidEndDocument:(NSXMLParser *)parser
{
[self bar];
}
This fine if you are parsing the response on a background thread and it doesn't matter if the function bar is called on main thread or background thread.
But if you want to call the bar function specifically on main thread then you can use this function
[self performSelectorOnMainThread:#SEL(bar) withObject:nil waitUntilDone:YES];
you mean in [self foo] function you want to parse some thing and when its completely parsed then you want to call [self bar]; function right?
okay then you can fire a notification when parsing gets completed. in by this notification you can call the method you want.

How can I make a method stall for a fixed amount of time?

I have an app that calls a sometimes-fast, sometimes-slow method. I know an upper bound for how long it will take (2 seconds). I'd like to set a timer to start when the method is called, run the code, but then not produce the output until 2 seconds has passed, no matter how long it actually takes. That way the user perceives the action as always taking the same amount of time. How can I implement this?
What I would like is something along the lines of this:
-(IBAction)doStuff {
// START A TIMER, LOOK BUSY
[activityIndicator startAnimating];
... real work happens here ...
... NSString *coolString gets assigned ...
// WHEN TIMER == 2 SECONDS, REVEAL COOLNESS
[activityIndicator stopAnimating];
[textField setText:coolString];
}
There are a couple of ways to delay an action in Cocoa. The easiest may be to use performSelector:withObject:afterDelay:. This method sets up a timer for you and calls the specified method when the time comes. It's an NSObject method, so your objects all get it for free.
The tricky part here is that the first method will block the main thread, so you need get it onto a background thread, and then get back to the main thread in order to update the UI. Here's a stab at it:
// Put the method which will take a while onto another thread
[self performSelectorInBackground:#selector(doWorkForUnknownTime)
withObject:nil];
// Delay the display for exactly two seconds, on the main thread
[self performSelector:#selector(displayResults)
withObject:nil
afterDelay:2.0];
- (void)doWorkForUnknownTime {
// results is an ivar
results = ...; // Perform calculations
}
- (void)displayResults {
if( !results ){
// Make sure that we really got results
[self performSelector:#selector(displayResults:)
withObject:nil
afterDelay:0.5];
return;
}
// Do the display!
}
The only other thing I can think of is to store the time that the "work" method is called in an NSDate, and check how long it took when you get the results. If it isn't two seconds yet, sleep the background thread, then call back to the main thread when you're done.
[self performSelectorInBackground:#selector(doWorkForUnknownTime:)
withObject:[NSDate date]];
- (void)doWorkForUnknownTime:(NSDate *)startTime {
// All threads must have an autorelease pool in place for Cocoa.
#autoreleasepool{
// This will take some time
NSString * results = ...; // Perform calculations
NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSinceDate:startTime];
if( elapsedTime < 2.0 ){
// Okay to do this to wait since we're on a background thread,
// although not ideal; sleeping threads are kind of wasteful.
// Try not to do this a lot.
sleep(2.0 - elapsedTime);
}
// Don't forget to retain results on the main thread!
[self performSelectorOnMainThread:#selector(displayResults:)
withObject:results
waitUntilDone:YES];
// [results release]; // if necessary
}
}
[self performSelector:#selector(myfunc) withObject: afterDelay:];
should help.
-(IBAction)doStuff {
// START A TIMER, LOOK BUSY
[activityIndicator startAnimating];
... real work happens here ...
... NSString *coolString gets assigned ...
// WHEN TIMER == 2 SECONDS, REVEAL COOLNESS
[self performSelector:#selector(revealCoolnessWithString:) withObject:coolString afterDelay:2];
}
- (void)revealCoolnessWithString:(NSString *)coolString
{
[activityIndicator stopAnimating];
[textField setText:coolString];
}
Hope this helps

Confounding Cocoa problem — program hangs unless there’s an unrecognised method call

Bear with me, this one is hard to explain. I hope some hero out there knows what’s going on here. Some history needed;
One of my cocoa objects, “Ball” represents a small graphic. It only makes sense within a view. In some of the Ball’s methods, it asks the view to redraw. Most importantly, it asks the view to redraw whenever the Ball’s position parameter is set. This is achieved in the setter.
Here’s the mouthful, as suggested:
In View.m
- (void)mouseUp:(NSEvent *)theEvent {
if (![runnerPath isEmpty]) {
[walkPath removeAllPoints];
[walkPath appendBezierPath:runnerPath];
[runnerPath removeAllPoints];
[[self held] setStep:0];
[[self held] setPath:walkPath];
[NSTimer scheduledTimerWithTimeInterval:.01 target:[self held] selector:#selector(pace) userInfo:nil repeats:YES];
}
}
In Ball.m
- (void)pace {
CGFloat juice = 10;
BOOL loop = YES;
while (loop) {
if ([self step] == [[self path] elementCount]) {
if ([[self timer] isValid]) {
[[self timer] invalidate];
}
[[self path] removeAllPoints];
// #throw([NSException exceptionWithName:#"test" reason:#"reason" userInfo:nil]);
}
if (loop) {
CGFloat distance;
NSPoint stepPoint;
if ([[self path] elementCount] > 0) {
NSPoint returnPoints[2];
[[self path] elementAtIndex:[self step] associatedPoints:returnPoints];
stepPoint = returnPoints[0];
distance = pixelDistance([self position], stepPoint);
}
if (distance <= juice) {
[self setPosition:stepPoint];
if (distance < juice) {
juice -= distance;
loop = YES;
[self setStep:[self step]+1];
} else {
loop = NO;
}
} else {
NSPoint cutPoint = moveAlongBetween([self position], stepPoint, juice);
[self setPosition:cutPoint];
loop = NO;
}
}
}
}
could you also tell how you handle exceptions? since normally an unrecognized selector will end your program. Maybe you need an exception rather than an unrecognized selector. Try:
#throw([NSException exceptionWithName:#"test" reason:#"reason" userInfo:nil]);
If this would fix it as well, you're doing something after this code which freezes the app.
edit: thanks for the code update.
There's some weird stuff going on here! I'm not going to rewrite the whole thing, so here's some pointers:
first of all: you're looping inside some routine that is called from a timer loop. Is that intended? There is no way to pause execution within that while() loop, so it will happen in a blink anyway. You would need to keep some state information in the class. E.g. adding a loop counter every time pace is called.
second: if you start a timer, it will call your selector with the timer as an argument. So define the function as -(void)pace:(NSTimer*)timer, and use timer, not [self timer] (the latter will not be your timer anyway, if you don't assign it!)
third: you're firing 100 times a second. That is a lot, and presumably higher than the refresh rate of any device you're writing this for. I think 20/sec is enough.
fourth: to be sure, if you change it to -(void)pace:(NSTimer*)timer, don't forget to use #selector(pace:) (i.e. don't forget the :)
fix those things, and if it's still broken, update your question again and put in comment so we will know. Good luck!
Try calling
for (NSView *each in [self views]) {
...
}
I'm assuming that views is an array, so fast enumeration applies to it directly and there is no need to call allObjects.
A couple of other points.
Have you set a Global breakpoint of objc_exception_throw? This will apply to all Xcode projects and is so useful I'm surprised it isn't set by default.
You say you looked at the Console for errors. I take it, then, that you didn't set a breakpoint on the code and step into it to see exactly what is happening when your execution reaches that point? Have a look at the Xcode Debugging Guide