I have a class, EventPresentationController, that displays data in an Event object. The class needs to be passed an Event object, but sometimes that object needs to be downloaded from a remote service first. I don't want the controller to have any knowledge of networking stuff, so I thought that instead of passing in an Event object, I'd pass it a RAC signal. Then if the object exists I can send the signal immediately, but if the object needs to be downloaded, I can download it then send the signal. However, I'd like for the controller to display an activity indicator if in fact a download needs to occur.
I see that RACCommand has an executing signal I can subscribe to, so I suppose I can pass that instead of an event, but semantically it seems weird to be initializing a view controller with a "command" (vs. an event, or a signal that will deliver an event). Is using RACCommand the right thing to do?
That's an interesting question. I think your instincts are right that you should pass the view controller a signal.
So let's go with that. Then let's suppose your view controller has a activityIndicator property.
You should be able to do something like:
- (id)initWithEventSignal:(RACSignal *)signal {
// ... init stuff ...
// Send a YES and then a NO when `signal` completes. If `signal`
// immediately sends a value, then it will immediately send NO.
RACSignal *loading = [[[RACSignal return:#YES] takeUntil:signal] concat:[RACSignal return:#NO]];
RAC(self.activityIndicator, hidden) = [loading not];
RAC(self, event) = signal;
// ... other stuff ...
}
We're using -takeUntil: to short-circuit our loading signal when the event signal sends its result.
Related
Sorry for the multi-question post, but they're all somewhat related. I apologize if some of this is obvious, I'm still trying to wrap my head around ReactiveCocoa.
I have a UIViewController that's being passed a RACSignal whose values it needs to display. This signal will either emit a model object immediately if it already exists on the device, or if the model needs to be fetched, when the fetch finishes; so, basically the signal is a promise. This allows me to remove all knowledge of network from the view controller.
if the user dismisses the view controller, I no longer need whatever the signal will emit. Is this a normal thing to do in the view controller?
- (void) dealloc
{
[_modelSignalDisposable dispose] ; // Dispose of the subscription to the signal
}
Let's say that the device loses its connection while a model fetch is occurring, and this causes the signal to emit an error. In this case I may display something like a reload button to let the user try again. When the user taps the reload button, can I just resubscribe to the same signal even though it's completed? If not, how do I "reset" the signal? Remember, the view controller can't really re-create the signal from scratch since it has no knowledge of how it was created to begin with.
The signal in question is actually a subclass of RACSubject called a Command (has nothing to do with a RACCommand). The idea behind it is that the Command starts network fetching on subscribe and thus overrides -subscribe: to start a network operation. If the view controller goes out of scope due to user interaction I'd like for the subscription disposal to cancel any related fetch operations. Is this the right pattern?
- (RACDisposable *) subscribe:(id<RACSubscriber>)subscriber
{
RACDisposable *rd = [super subscribe:subscriber] ;
[self.remoteService startFetching] ;
#weakify(self);
return [RACDisposable disposableWithBlock:^
{
#strongify(self) ;
[self.remoteService cancelFetching] ;
[rd dispose] ;
}] ;
}
if the user dismisses the view controller, I no longer need whatever the signal will emit. Is this a normal thing to do in the view controller?
Yep, that's totally reasonable. Though I'd use -takeUntil:self.rac_willDeallocSignal instead, so that I don't have to bother with keeping the disposable around.
Let's say that the device loses its connection while a model fetch is occurring, and this causes the signal to emit an error. In this case I may display something like a reload button to let the user try again. When the user taps the reload button, can I just resubscribe to the same signal even though it's completed? If not, how do I "reset" the signal? Remember, the view controller can't really re-create the signal from scratch since it has no knowledge of how it was created to begin with.
You can simply re-subscribe if the signal is a cold signal. That is, if the signal does something on subscription. Usually this means you created the signal with +[RACSignal createSignal:].
The signal in question is actually a subclass of RACSubject called a Command (has nothing to do with a RACCommand). The idea behind it is that the Command starts network fetching on subscribe and thus overrides -subscribe: to start a network operation. If the view controller goes out of scope due to user interaction I'd like for the subscription disposal to cancel any related fetch operations. Is this the right pattern?
You have the right idea, but you can just use +[RACSignal createSignal:] instead of subclassing RACSubject.
When is executed the onDraw method of a DynamicForm exactly?
When I first load my page (a puremvc.Mediator page) and draw my DynamicForm with an onDraw method, I enter in this onDraw method. Then I destroy the view component, remove the Mediator from the Facade, and re-load the page. DynamicForm is being created again, the viewComponent too, but I don't enter in the onDraw method. Just as if the object has already been drawn! But the thing is it was destroyed, its parent too, and the parent of the parent, too.
What could be the problem?
OK, I got it.
The thing is that I use PUREMVC Mediator and Notification. On the first load, everything's good as the async method that returns data for DynamicForm C is sending the successful notification AFTER I have already created the VLayout B, and thus the addMember method adds and displays the DynamicForm C.
On the second load, I use the data that has already been gathered from DB, so no async method is being processed and the notification arrives BEFORE I create VLayout B, so basically the DynamicForm C is NOT being drawn.
My solution : I reordered some lines of code to make sure I always call the method that sends data via Notification (whether from DB or stocked) AFTER of the page creation.
NOTE: Updated below...
I have a cocoa desktop application which consists of a series of controls around a custom NSView. I am using displayLink to drive the updates.
When a user clicks on an NSControl (a slider, a button, a checkbox, a radio button) the application appears to freeze until the mouse is released. I can confirm in fact that the displayLink callback (getFrameForTime) is NOT firing during the time. If I create a timer, that also does not fire, both remain paused until the user releases the mouse, at which point the application resumes updating.
The control is bound, and if I update that value from another thread (for example, via a callback from a MIDI interface) the slider behaves as expected: it moves, the value updates and the application does not pause.
I feel like this should be a fairly obvious fix, but I'm stumped.
Checking "continuous" in IB does as advertised: sends the values continuously, but still exhibits this behavior (preventing the UI update) until the mouse is released.
This seems to be related specifically to mouseDown on NSControl? Why would this block, and do I really need to subclass all my UI elements to change this behavior (seems extreme)
DisplayLink is in its own thread, so why mouseDown on the main thread block it? If this is the case, given the injunction on updating the Cocoa UI from other than the main thread, how do I deal with it?
Any help much appreciated.
Update
Per #Nikolai's comments below, I can confirm that using an NSTimer and adding it to NSEventTrackingRunLoopMode does NOT block. However, I would really like to use CVDisplayLink which (according to the documentation) runs in it's own thread and should not be blocked in this way. Unlike CADisplayLink, I cannot find a way to explicitly assign a runloop to CVDisplayLink (it seems it doesn't work that way), so perhaps the new question should be:
Why does CVDisplayLink block on NSEventTrackingRunLoopMode?
When clicking on an NSControl the runloop mode goes from NSDefaultRunLoopMode to NSEventTrackingRunLoopMode, as long as the mouse is down. That means that only run loop sources (display link) and timers fire that have been added to this mode.
You can add timers to any mode by using -[NSRunLoop addTimer:forMode:]. For a display link the equivalent method is -[CADisplayLink addToRunLoop:forMode:].
To make your animation continue during event tracking you would do something like:
[myDisplayLink addToRunLoop:[NSRunLoop currentRunLoop]
forMode:NSEventTrackingRunLoopMode];
Your test project shows that you are calling a view's display method from within the display link's callback.
When commenting the display message out, the display link is called continuously even while moving the slider.
So what goes wrong is that when the runloop goes into event tracking mode, the call to display on the display link's thread blocks until the mouse is released and the run loop goes back to default mode. You can easily confirm this by putting a log statement before the call and one after it.
Why exactly that happens is not clear to me. What is clear is that it's illegal to call a view's methods from a background thread. You have to trigger the view's display by dispatching a setNeedsDisplay: on the main thread:
static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext)
{
dispatch_async(dispatch_get_main_queue(), ^{
[(__bridge MyCustomView*)displayLinkContext setNeedsDisplay:YES];
});
return kCVReturnSuccess;
}
I have an NSView in a Mac OS X application that draws itself. When I am ready for this to happen I request it with the call:
[self.imageRenderedView setNeedsDisplay:YES];
My question is, does this call block? That is, is it synchronous, and can I assume the drawing has happened by the time the subsequent statement is executed? This assumption seems to work for me, but I feel a bit insecure about it.
The setNeedsDisplay: call only marks the view as needing display by setting
a flag in the view object.
Therefore it returns very quickly, but the drawing has not yet happened when the method returns.
From the documentation:
Whenever the data or state used for drawing a view object changes, the
view should be sent a setNeedsDisplay: message. NSView objects marked
as needing display are automatically redisplayed on each pass through
the application’s event loop. (View objects that need to redisplay
before the event loop comes around can of course immediately be sent
the appropriate display... method.)
I don't really understand the difference that cocoa makes between a notification and an event.
For instance I could have code like this:
-(void)mouseMoved:(NSEvent*)event { … }
but not
-(void)windowMoved:(NSEvent*)event { … }
For the second one I'd have to use NSNotification – why?
The difference is, that NSEvent is used to encapsulate input events. Mouse down, key down etc.
However, NSNotification is used to notify observers about a change of a state or an object (eg. when the network reachability changed, new data became available or that a window moved).
In your case: A window move isn't some kind of input, but a change of the windows position. Thus you get an NSNotification rather an NSEvent.