I'm completely new to the Reactive Cocoa Framework and i'm just doing some simple tests but i'm encountering a problem that i would like to understand.
Basically i'm just doing an API call to fetch a JSON object from my server, i want to do this with RAC. So my steps are the following:
First i build the RACCommand this way:
RACCommand *getLatestVersionCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[[API sharedInstance] getLatestAppVersion]
materialize];
}];
Now i create the signal and subscribe to it this way:
RACSignal *versionCodeSignal = [[getLatestVersionCommand.executionSignals flatten] deliverOn:[RACScheduler mainThreadScheduler]];
[[versionCodeSignal
map:^id(NSDictionary *responseObject) {
return responseObject;
}]
subscribeNext:^(NSDictionary *responseObject) {
NSArray *allVersions = [[NSArray alloc] initWithArray:[responseObject objectForKey:KEY_VERSIONS]];
for(NSDictionary *version in allVersions) {
NSString *device = [version objectForKey:KEY_DEVICE];
if([device isEqualToString:KEY_IOS]) {
NSString *latestVersionName = [[version objectForKey:KEY_VERSION] objectForKey:KEY_NAME];
if([APP_VERSION compare:latestVersionName options:NSNumericSearch] == NSOrderedAscending) {
//There is a new version!
NSLog(#"There is a new version!!!");
}
}
}
}];
Finally I execute the command this way
[getLatestVersionCommand execute:self];
The problem i'm facing is that in the subscribeNext block, the object i receive is an RACEvent object and not the dictionary i'm expecting. I know i'm doing something wrong and not understanding the full flow correctly, i tried adding the map function that i thought i didn't need just to test and nothing.
The only way i found is to convert the response in the map block to a RACEvent and return its value but that doesn't seem to me the right way to go.
Any light will be greatly appreciated.
Remove the call to -[RACSignal materialize] from your command's signal block.
Related
I have the following classes and methods:
Class A
- (RACSignal *)createX
{
NSDictionary *parameters = #{};
return [[[[HTTPClient sharedClient] rac_POST:#"X/" parameters:parameters]
map:^id(OVCResponse *response) {
[self logResponse:response];
return response.result;
}] catch:^RACSignal *(NSError *error) {
return [RACSignal error:[self handleError:error]];
}];
}
Class B
- (void)requestData
{
[[self.myClassA createX]
subscribeNext:^(NSArray *results) {
DDLogDebug(#"response : %#", results);
}
error:^(NSError *error) {
[self.dataManager sendError:error];
}];
}
Class C
- (void)retrieveData
{
[self.myClassB requestData];
}
What is the best way to design requestData in Class B such that the results array can be accessed in Class C.
i.e.
Should I forward the Array some way using [array rac_sequence],
should I create a new signal inside requestData, should requestData return a RACSignal instead of void?
Any help or guidance would be greatly appreciated. Thanks.
I believe you want to use doNext instead of subscribeNext in Class B.
I'm not entirely clear on your use case here but I think you are mixing paradigms. RAC stuff is always asynchronous so in order to access the result of your network request synchronously you will have to store it in some way.
You could bind the result to a property on ClassB or you could use RACCommand, something like:
[[RACCommand alloc] initWithEnabled:RACObserve(self, valid) signalBlock:^RACSignal *(id input) {
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//make network call
//send responseObject to subscriber
[subscriber sendNext:responseObject];
//[subscriber sendError:#NSError#] //send error if something went wrong
[subscriber sendCompleted];
return nil;
}] materialize];
}];
You can then subscribe to the executionSignals of the RACCommand which streams a RACSignal for each execution of the command which you have control over in the block described above.
So I think your options are:
RACCommand pattern, look into it a bit more
Bind result of network call to a property for synchronous access
Return a RACSignal as you describe upon calling the method
Possibly look into replay() or replayLast() here as you could then store a reference to the RACSignal and subscribe to it for access to its last value
I'm building a feature where users of my app can find their Facebook friends and add them in the app. There are three steps which I have to do:
Get the currently connected users
Get the Facebook users
Get the Application Users (this is dependent on step 2)
Once all of these complete I then need to combine/reduce the three resulting arrays into a final array.
I have created three functions that all return RACSignal
getUsersWithFacebookIds, getConnectedUsers, and getFacebookUsers
I'm not sure how to wire all of this using ReactiveCocoa.
The Once All Are Done Do Something With All, can be achieve with:
[[RACSignal combineLatest:#[connectUsersSignal,facebookUsersSignal,applicationUsersSignal]] subscribeNext:^(RACTuple *users) {
NSArray *connectedUsers = [users first];
NSArray *facebookUsers = [users second];
NSArray *applicationUsers = [users third];
}];
The other piece missing is how you make the applicationUsersSignal dependent on the facbookUsersSignal. Which could be done like this:
- (RACSignal *)applicationUsersSignalWithFacebookUsersSignal:(RACSignal *)fbSignal
{
return [fbSignal flattenMap:^RACStream *(NSArray *facebookUsers) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// Do what you have to do with the facebookUsers
return nil;
}];
}];
}
Just to add a bit more to the answer. I am assuming those are cold signals, (signals that haven't started yet aka haven't been subscribed). So the idea of using combineLatest: is that you want to capture the point where each and every single signal as send at least one next, after that you subscribe to it so it can begin. Finally you can get their values from a RACTuple.
I re read your question and you want them to come all in a single Array:
[[[RACSignal combineLatest:#[connectUsersSignal,facebookUsersSignal,applicationUsersSignal]] map:^id(RACTuple *allArrays) {
return [allArrays.rac_sequence foldLeftWithStart:[NSMutableArray array] reduce:^id(id accumulator, id value) {
[accumulator addObjectsFromArray:value];
return accumulator;
}];
}] subscribeNext:^(NSArray *allUsers) {
// Do Something
}];
In my reactive cocoa, I want to block calls to a function if a previous call to itis still progress. I have achieved this as follows, but it seems more like a hack.
__block RACSignal * performSync = [[self performSync:connectionClient] take:1];
[[[self rac_signalForSelector:#selector(forceSync:)]]]
flattenMap:^RACStream *(id value) {
NSLog(kMILogLevelDebug, #"Perform sync requested");
return performSync;
}]
subscribeNext:^(id x) {
NSLog(kMILogLevelDebug,#"Sync is performed", [NSDate date]);
}
error:^(NSError *error) {
[self performSyncCompleted:error];
}
completed:^{
[self performSyncCompleted:nil];
performSync = [[self performSync:connectionClient] take:1];
}];
So, I created a performSync signal, which is executed only once, and once it completes, I recreate the signal. Any better way to accomplish the above?
You should in my opinion use RACCommand :
RACCommand* command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
return [#mysignal];
}];
// then later
[command execute:nil];
// or
[[command execute:nil] take:1];// synchronous but be careful ! here be dragons
You can call execute as many time as you want, it wont subscribe the signal more than once at a time. It makes extensive use of multicasted signals in the default setup.
Moreover you can know if the command is executing by using:
[command executing];
here is a blog article talking about RACCommands
Method waitUntilCompleted from RACSignal could do the trick.
I'm new in reactive words, so, please help to find the best solution for this scenario:
I work with Youtube API. I want to load VideoCategories, then get one top video for each category, then load thumbnail for each video, accumulate it into model, and only then send signal to table view to reload data.
I request categories like this:
[[[TRYoutubeManager manager] rac_GET:#"videoCategories" parameters:parameters] map:^id(id responseObject) {
TRYoutubeListResponseModel *listModel =
[MTLJSONAdapter modelOfClass:[TRYoutubeListResponseModel class] fromJSONDictionary:responseObject error:nil];
listModel.items = [[listModel.items.rac_sequence filter:^BOOL(TRYoutubeVideoCategoryModel *categoryModel) {
return categoryModel.assignable;
}] array];
return listModel;
}];
So, how to send request for each listModel.items and then combine the result, and then signal the table view?
Ok, for everybody who still wonder. Explanation in more abstract way:
// You get your list ob objects here
[[[Manager getList] flattenMap:^RACStream *(NSArray *yourList) {
NSMutableArray *listItemsSignals = [NSMutableArray array];
for (ItemClass *item in yourList) {
//Something that produces signals
RACSignal *itemSignal = [item imageSignal];
[listItemsSignals addObject: itemSignal]
}
return [RACSignal combineLatest:listItemsSignals];
}] subscribeNext:^(RACTuple *values) {
// All your values are here
}];
Im new to unit testing and OCMock so this might be an obvious answer, just didn't find answer on google.
I am trying to test a model object's method.
the method has the following code:
//takes a filepath and a pk, sets the filepath to the
BoxAppDelegate *delegate = [[UIApplication sharedApplication] delegate];
NSNumber *ifExistIndexInJson = [BoxJsonDataHelper pkExistInCurrentJson:[[[self.downloadQueue objectAtIndex:0] objectForKey:#"pk"] integerValue]];
if (ifExistIndexInJson)
{
[[[self.downloadQueue objectAtIndex:0] objectForKey:#"fields"] setObject:path forKey:#"content"];
NSError *error = nil;
[[NSFileManager defaultManager] removeItemAtPath:[[[delegate.currentJsonData objectAtIndex:[ifExistIndexInJson integerValue]] objectForKey:#"fields"] objectForKey:#"content"] error:&error];
[delegate.currentJsonData removeObjectAtIndex:[ifExistIndexInJson integerValue]];
[delegate.currentJsonData addObject:[self.downloadQueue objectAtIndex:0]];
[self.downloadQueue removeObjectAtIndex:0];
if ([self.downloadQueue count] > 0)
{
[BoxServerRequestsObject downloadFileForPK:[[[self.downloadQueue objectAtIndex:0] objectForKey:#"pk"] integerValue]sender:self];
}
else
{
//end the progress or whatever
}
}
else
{
[[[self.downloadQueue objectAtIndex:0] objectForKey:#"fields"] setObject:path forKey:#"content"];
[delegate.currentJsonData addObject:[self.downloadQueue objectAtIndex:0]];
[self.downloadQueue removeObjectAtIndex:0];
if ([self.downloadQueue count] > 0)
{
[BoxServerRequestsObject downloadFileForPK:[[[self.downloadQueue objectAtIndex:0] objectForKey:#"pk"] integerValue]sender:self];
}
else
{
//end the progress or whatever
}
}
I need help with a couple of things:
when I call [BoxJsonDataHelper pkExistInCurrentJson:...]. BoxJsonDataHelper is actually self, only it's a class method not an instance, so I call it by name, How can I fake the results of the return value so theres no dependency?
How to fake a file at a path for the program to remove? than how do I check that it was removed?
how do I mock BoxServerRequestObject to make the method call the mock object instead of the real one? and than how do I check if it has been called(also a class method)
My knowledge in unit testing is limited, and I have just started with OCMock and read some examples so I would appreciate full answers :)
You can mock class methods just like instance methods. They stay mocked until the mock is dealloc'ed.
id boxJsonDataHelperMock = [OCMockObject mockForClass:BoxJsonDataHelper.class];
[[[boxJsonDataHelperMock stub] andReturn:#(1)] pkExistInCurrentJson:OCMOCK_ANY]
Are you just testing whether NSFileManager works at that point? With data objects, I prefer to do the actual writing. Why not just assert that the file doesn't exist after it is removed? If you wanted to mock, you should mock "defaultManager" on NSFileManager and return a mock object that expects removeItemAtPath:error:
Place a mock object in your download queue at index 0.