Reactive Cocoa - Convert certain signal values into error or completed - objective-c

Perhaps I'm still struggling on the reactive learning curve but I am having a hard time figuring out how to bridge a non reactive class with the rest of my reactive code. I am using a category to extend the non-reactive class.
The property is just an Enum representing the current state of a network action, states like New, Submitted, Processing and Completed. Right now I have written the following method in my category:
#implementation JRequestBase (RACExtensions)
- (RACSignal*) rac_RequestStateSignal
{
return RACAble(self, state);
}
#end
However, when state transitions from Processing -> Completed or from any state to Errored I want this signal to send Completed or Error instead of Next Value. How can I accomplish this in a category? I want to do something like:
#implementation JRequestBase (RACExtensions)
- (RACSignal*) rac_RequestStateSignal
{
return [RACAble(self, state) map:^(NSNumber *state){
if ([state intValue] == iRequestStateComplete)
{
# SEND COMPLETE
}
else if ([state intValue] == iRequestStateErrored)
{
# SEND ERROR
}
else
{
return state;
}
}];
}
#end
edit: I took a look at the GHAPIDemo and have come up with the following:
- (RACSignal*) rac_RequestSignal
{
RACSubject *subject = [[RACReplaySubject alloc] init];
[[RACAble(self, state) subscribeNext:^(NSNumber* s){
if ( [s intValue] == JRequestStateCompleted)
{
[subject sendNext:self];
[subject sendCompleted];
}
else if ([s intValue] == JRequestStateErrored)
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
// .. Set up dict with necessary values.
NSError *error = [NSError errorWithDomain:#"blah" code:1 userInfo:dict];
[subject sendError:error];
}
}];
return subject;
}
I'm not 100% sure this is the right way but it seems to be working.

Whenever you want to map values → signal events, instead of values → values, you should use -flattenMap: to return a signal corresponding to each input value. Then, as the "flatten" in the name implies, they'll be combined into one resulting signal.
However, this case is a little different, because you want to terminate the signal as soon as you get the Complete value. We'll use -takeUntilBlock: to represent that part.
The resulting code looks something like this:
- (RACSignal*) rac_RequestStateSignal
{
return [[RACObserve(self, state)
takeUntilBlock:^ BOOL (NSNumber *state){
return [state intValue] == iRequestStateComplete;
}]
flattenMap:^(NSNumber *state){
if ([state intValue] == iRequestStateErrored)
{
// Create a meaningful NSError here if you can.
return [RACSignal error:nil];
}
else
{
return [RACSignal return:state];
}
}];
}
(I used RACObserve because ReactiveCocoa 2.0 is now the only supported version, but you can use RACAble until you're ready to upgrade.)
As a general rule, you should avoid using subjects when possible, since they make code more stateful and reduce laziness.

Related

ReactiveCocoa zip is not working as expected

I want to process data stream with ReactiveCocoa, what I want to do is, I want to calculate average value of the data stream for a period, then use the value to subtract with the average value. The flow char would look like this
Data source -> extract value ----> average---> zip
\ /
\------------/
So you can the extract value signal generates average signal, it also sends to zip to combine with average signal's result.
The code to demonstrate is here
__block id<RACSubscriber> sourceSubscriber;
RACSignal *sourceSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
sourceSubscriber = subscriber;
return nil;
}];
RACSignal *extractValueSignal = [sourceSignal map:^id(id value) {
NSLog(#"extractValueSignal %#", value);
return value;
}];
RACSignal *avgSignal = [extractValueSignal scanWithStart:[NSMutableArray array] reduce:^NSMutableArray *(NSMutableArray *array, id next) {
NSLog(#"avgSignal %#", next);
[array addObject:next];
if (array.count > 5) {
[array removeObjectsInRange:NSMakeRange(0, array.count - 5)];
}
return array;
}];
[[RACSignal zip:#[extractValueSignal, avgSignal] reduce:^id(NSNumber *value, NSNumber *avg) {
NSLog(#"zip %#, %#", value, avg);
return value;
}] subscribeNext:^(id x) {
NSLog(#"output %#", x);
}];
[sourceSubscriber sendNext:#1];
[sourceSubscriber sendNext:#2];
[sourceSubscriber sendNext:#3];
[sourceSubscriber sendNext:#4];
[sourceSubscriber sendNext:#5];
The output is
extractValueSignal 1
avgSignal 1
extractValueSignal 2
avgSignal 2
extractValueSignal 3
avgSignal 3
extractValueSignal 4
avgSignal 4
extractValueSignal 5
avgSignal 5
The problem I have here is the zip block never get called, nor the subscribeNext block. I wonder why it doesn't work? Shouldn't I to have one signal in the flow for twice or what? Should I use something like tee or what for extract value signal?
Okay, answering my own question, I noticed that, if you add a NSLog in createSignal block
RACSignal *sourceSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(#"SUB %#", subscriber);
sourceSubscriber = subscriber;
return nil;
}];
You will notice that it get called twice, as down stream subscribes to it twice, so the subscriber was replaced, so only one side of the signal get sent with next event.
To solve that problem, it's fairly easy, just create a multicast connection
RACMulticastConnection *conn = extractValueSignal.publish;
[conn connect];
then you can subscribe to
conn.signal
as much as you like, it will only call the block once.

Restart network RACSignal chain on search text change

I have a semi-working approach to this problem. I have a search field that sets off a series of dependent cold RACSignals - in my case network requests (similar to Chaining dependent signals in ReactiveCocoa). If at any point during those network requests, the text changes again, I would like the current chain of signals to halt, and restart with the new search text. I am aware of functions like switchToLatest, and this thread: https://github.com/ReactiveCocoa/ReactiveCocoa/issues/468, but not how they apply to my situation. Here is an example in code of what I am trying to achieve:
RACSignal * (^longNetworkSignalWithValue)(NSString *) = ^(NSString * value){
return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
usleep(2000000);
[subscriber sendNext:value];
}];
};
[[self.searchField.rac_textSignal throttle:.2] subscribeNext:^(id value) {
__block BOOL cancelled;
[[[self.searchField.rac_textSignal skip:1] take:1] subscribeNext:^(id x) {
NSLog(#"CANCELLED FOR : %#", value);
cancelled = YES;
}];
[[[longNetworkSignalWithValue(value) flattenMap:^RACStream *(id value) {
if(cancelled) return nil;
NSLog(#"Network signal 1 completed for: %#", value);
return longNetworkSignalWithValue(value);
}] flattenMap:^RACStream *(id value) {
if(cancelled) return nil;
NSLog(#"Network signal 2 completed for: %#", value);
return longNetworkSignalWithValue(value);
}] subscribeNext:^(id x) {
if(!cancelled)
NSLog(#"Completed with value: %#", x);
}];
}];
Is my cancelled BOOL the best way to accomplish what I am trying to do?

Better way to observe in ReactiveCocoa

I'm still new to ReactiveCocoa. I wanted to simply add an observer to a field, so did it like this:
[_countryPicker rac_observeKeyPath:#"value" options:nil observer:self block:^(VBCountry* value, NSDictionary* change)
{
if ([_mobileField.textField.text length] == 0)
{
[_mobileField.textField setText:[NSString stringWithFormat:#"+%i", value.dialCode]];
}
}];
With block callback, and no need to explicitly detach the observer, this is already better than old-style KVO.
However, is this a low-level method to which there's a higher level of abstraction? If so, is it OK to call this method directly? And what's the better/higher way to do it?
I'd advise against depending on the direct KVO methods. They're really an implementation detail.
Let's progressively re-write that with idiomatic RAC operators.
First we'll just replace the direct KVO code with RACObserve:
[RACObserve(_countryPicker, value) subscribeNext:^(VBCountry *value) {
if ([_mobileField.textField.text length] == 0)
{
[_mobileField.textField setText:[NSString stringWithFormat:#"+%i", value.dialCode]];
}
}];
Then we'll replace the if and string formatting with -filter: and -map::
[[[RACObserve(_countryPicker, value) filter:^(id _) {
return [_mobileField.textField.text length] > 0;
}] map:^(VBCountry *value) {
return [NSString stringWithFormat:#"+%i", value.dialCode];
}] subscribeNext:^(NSString *text) {
[_mobileField.textField setText:text];
}];
Finally we'll use the RAC macro to make the assignment over time explicit:
RAC(_mobileField.textField, text) = [[RACObserve(_countryPicker, value) filter:^(id _) {
return [_mobileField.textField.text length] > 0;
}] map:^(VBCountry *value) {
return [NSString stringWithFormat:#"+%i", value.dialCode];
}];

RestKit: connecting up a relationship which isn't in the response

Suppose that in my data model, I have Pages, which have-many Comments.
I want to connect up a relationship in the model which goes from the Comment back to the Page it belongs to, but the Page object isn't nested in the response, nor is any primary key which could identify the parent Page present in the response.
At the time that I call loadObjectsAtResourcePath, all the Comments which are loaded should belong to a fixed, known Page object. One way I could hook up the relationship would be to do:
loader.onDidLoadObjects = ^(NSArray* objs) {
for (Comment* comment in objs) comment.page = self.page;
...
}
but I'm hoping there's a better way. Note that I can't use the connectRelationship family of methods, because there's no primary key in the response which could let me hook each Comment up to a Page.
You can use the delegate method - (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout id *)mappableData to inject extra parameters before the mapping step. It ensures the objects and relationships will be correctly saved by RestKit if you are using core data (note the solution you gave above does not save the relationship).
Alternatively, look at this answer where I showed how to override RKObjectLoader to retrieve the page information from the URL itself.
EDIT: Here is the category I mentioned in the comment:
.h
#import <RestKit/RestKit.h>
typedef void(^RKObjectLoaderWillMapDataBlock)(id* mappableData);
#interface RKObjectLoader (Extended)
#property (nonatomic, copy) RKObjectLoaderWillMapDataBlock onWillMapData;
#end
and the .m:
#import <objc/runtime.h>
NSString* kOnWillMapDataKey = #"onWillMapData";
#implementation RKObjectLoader (Extended)
- (RKObjectLoaderWillMapDataBlock) onWillMapData {
return objc_getAssociatedObject(self, &kOnWillMapDataKey);
}
- (void) setOnWillMapData:(RKObjectLoaderWillMapDataBlock) block {
objc_setAssociatedObject(self, &kOnWillMapDataKey, block, OBJC_ASSOCIATION_COPY);
}
- (RKObjectMappingResult*)mapResponseWithMappingProvider:(RKObjectMappingProvider*)mappingProvider toObject:(id)targetObject inContext:(RKObjectMappingProviderContext)context error:(NSError**)error {
id<RKParser> parser = [[RKParserRegistry sharedRegistry] parserForMIMEType:self.response.MIMEType];
NSAssert1(parser, #"Cannot perform object load without a parser for MIME Type '%#'", self.response.MIMEType);
// Check that there is actually content in the response body for mapping. It is possible to get back a 200 response
// with the appropriate MIME Type with no content (such as for a successful PUT or DELETE). Make sure we don't generate an error
// in these cases
id bodyAsString = [self.response bodyAsString];
RKLogTrace(#"bodyAsString: %#", bodyAsString);
if (bodyAsString == nil || [[bodyAsString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] == 0) {
RKLogDebug(#"Mapping attempted on empty response body...");
if (self.targetObject) {
return [RKObjectMappingResult mappingResultWithDictionary:[NSDictionary dictionaryWithObject:self.targetObject forKey:#""]];
}
return [RKObjectMappingResult mappingResultWithDictionary:[NSDictionary dictionary]];
}
id parsedData = [parser objectFromString:bodyAsString error:error];
if (parsedData == nil && error) {
return nil;
}
// Allow the delegate to manipulate the data
if ([self.delegate respondsToSelector:#selector(objectLoader:willMapData:)]) {
parsedData = [parsedData mutableCopy];
[(NSObject<RKObjectLoaderDelegate>*)self.delegate objectLoader:self willMapData:&parsedData];
}
if( self.onWillMapData ) {
parsedData = [parsedData mutableCopy];
self.onWillMapData(&parsedData);
}
RKObjectMapper* mapper = [RKObjectMapper mapperWithObject:parsedData mappingProvider:mappingProvider];
mapper.targetObject = targetObject;
mapper.delegate = (id<RKObjectMapperDelegate>)self;
mapper.context = context;
RKObjectMappingResult* result = [mapper performMapping];
// Log any mapping errors
if (mapper.errorCount > 0) {
RKLogError(#"Encountered errors during mapping: %#", [[mapper.errors valueForKey:#"localizedDescription"] componentsJoinedByString:#", "]);
}
// The object mapper will return a nil result if mapping failed
if (nil == result) {
// TODO: Construct a composite error that wraps up all the other errors. Should probably make it performMapping:&error when we have this?
if (error) *error = [mapper.errors lastObject];
return nil;
}
return result;
}
#end

Can Objective-C switch on NSString?

Is there a more intelligent way to rewrite this?
if ([cardName isEqualToString:#"Six"]) {
[self setValue:6];
} else if ([cardName isEqualToString:#"Seven"]) {
[self setValue:7];
} else if ([cardName isEqualToString:#"Eight"]) {
[self setValue:8];
} else if ([cardName isEqualToString:#"Nine"]) {
[self setValue:9];
}
Unfortunately they cannot. This is one of the best and most sought after utilizations of switch statements, so hopefully they hop on the (now) Java (and others) bandwagon!
If you are doing card names, perhaps assign each card object an integer value and switch on that. Or perhaps an enum, which is considered as a number and can therefore be switched upon.
e.g.
typedef enum{
Ace, Two, Three, Four, Five ... Jack, Queen, King
} CardType;
Done this way, Ace would be be equal to case 0, Two as case 1, etc.
You could set up a dictionary of blocks, like this:
NSString *lookup = #"Hearts"; // The value you want to switch on
typedef void (^CaseBlock)();
// Squint and this looks like a proper switch!
NSDictionary *d = #{
#"Diamonds":
^{
NSLog(#"Riches!");
},
#"Hearts":
^{
self.hearts++;
NSLog(#"Hearts!");
},
#"Clubs":
^{
NSLog(#"Late night coding > late night dancing");
},
#"Spades":
^{
NSLog(#"I'm digging it");
}
};
((CaseBlock)d[lookup])(); // invoke the correct block of code
To have a 'default' section, replace the last line with:
CaseBlock c = d[lookup];
if (c) c(); else { NSLog(#"Joker"); }
Hopefully Apple will teach 'switch' a few new tricks.
For me, a nice easy way:
NSString *theString = #"item3"; // The one we want to switch on
NSArray *items = #[#"item1", #"item2", #"item3"];
int item = [items indexOfObject:theString];
switch (item) {
case 0:
// Item 1
break;
case 1:
// Item 2
break;
case 2:
// Item 3
break;
default:
break;
}
Unfortunately, switch statements can only be used on primitive types. You do have a few options using collections, though.
Probably the best option would be to store each value as an entry in an NSDictionary.
NSDictionary *stringToNumber = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:6],#"Six",
[NSNumber numberWithInt:7],#"Seven",
[NSNumber numberWithInt:8],#"Eight",
[NSNumber numberWithInt:9],#"Nine",
nil];
NSNumber *number = [stringToNumber objectForKey:cardName];
if(number) [self setValue:[number intValue]];
A bit late but for anyone in the future I was able to get this to work for me
#define CASE(str) if ([__s__ isEqualToString:(str)])
#define SWITCH(s) for (NSString *__s__ = (s); ; )
#define DEFAULT
Here is the more intelligent way to write that. It's to use an NSNumberFormatter in the "spell-out style":
NSString *cardName = ...;
NSNumberFormatter *nf = [[NSNumberFormatter alloc] init];
[nf setNumberStyle:NSNumberFormatterSpellOutStyle];
NSNumber *n = [nf numberFromString:[cardName lowercaseString]];
[self setValue:[n intValue]];
[nf release];
Note that the number formatter wants the string to be lowercased, so we have to do that ourselves before passing it in to the formatter.
There are other ways to do that, but switch isn't one of them.
If you only have a few strings, as in your example, the code you have is fine. If you have many cases, you could store the strings as keys in a dictionary and look up the corresponding value:
NSDictionary *cases = #{#"Six" : #6,
#"Seven" : #7,
//...
};
NSNumber *value = [cases objectForKey:cardName];
if (value != nil) {
[self setValue:[value intValue]];
}
BY FAR.. my FAVORITE "ObjC Add-On" is ObjectMatcher
objswitch(someObject)
objcase(#"one") { // Nesting works.
objswitch(#"b")
objcase(#"a") printf("one/a");
objcase(#"b") printf("one/b");
endswitch // Any code can go here, including break/continue/return.
}
objcase(#"two") printf("It's TWO."); // Can omit braces.
objcase(#"three", // Can have multiple values in one case.
nil, // nil can be a "case" value.
[self self], // "Case" values don't have to be constants.
#"tres", #"trois") { printf("It's a THREE."); }
defaultcase printf("None of the above."); // Optional default must be at end.
endswitch
AND it works with non-strings, TOO... in loops, even!
for (id ifNumericWhatIsIt in #[#99, #0, #"shnitzel"])
objswitch(ifNumericWhatIsIt)
objkind(NSNumber) printf("It's a NUMBER.... ");
objswitch([ifNumericWhatIsIt stringValue])
objcase(#"3") printf("It's THREE.\n");
objcase(#"99") printf("It's NINETY-NINE.\n");
defaultcase printf("some other Number.\n");
endswitch
defaultcase printf("It's something else entirely.\n");
endswitch
It's a NUMBER.... It's NINETY-NINE.
It's a NUMBER.... some other Number.
It's something else entirely.
Best of all, there are SO few {...}'s, :'s, and ()'s
Objective-c is no different from c in this aspect, it can only switch on what c can (and the preproc def's like NSInteger, NSUInteger, since they ultimately are just typedef'd to an integral type).
Wikipedia:
c syntax:
The switch statement causes control to be transferred to one of several statements depending on the value of an expression, which must have integral type.
Integral Types:
In computer science, an integer is a datum of integral data type, a
data type which represents some finite subset of the mathematical
integers. Integral data types may be of different sizes and may or may
not be allowed to contain negative values.
I'm kind of late to the party, but to answer the question as stated, there's a more intelligent way:
NSInteger index = [#[#"Six", #"Seven", #"Eight", #"Nine"] indexOfObject:cardName];
if (index != NSNotFound) [self setValue: index + 6];
Note that indexOfObject will look for the match using isEqual:, exactly as in the question.
Building on #Graham Perks idea posted earlier, designed a simple class to make switching on strings fairly simple and clean.
#interface Switcher : NSObject
+ (void)switchOnString:(NSString *)tString
using:(NSDictionary<NSString *, CaseBlock> *)tCases
withDefault:(CaseBlock)tDefaultBlock;
#end
#implementation Switcher
+ (void)switchOnString:(NSString *)tString
using:(NSDictionary<NSString *, CaseBlock> *)tCases
withDefault:(CaseBlock)tDefaultBlock
{
CaseBlock blockToExecute = tCases[tString];
if (blockToExecute) {
blockToExecute();
} else {
tDefaultBlock();
}
}
#end
You would use it like this:
[Switcher switchOnString:someString
using:#{
#"Spades":
^{
NSLog(#"Spades block");
},
#"Hearts":
^{
NSLog(#"Hearts block");
},
#"Clubs":
^{
NSLog(#"Clubs block");
},
#"Diamonds":
^{
NSLog(#"Diamonds block");
}
} withDefault:
^{
NSLog(#"Default block");
}
];
The correct block will execute according to the string.
Gist for this solution
You can use macros approach to achieve it:
#define CASE(str) if ([__s__ isEqualToString:(str)])
#define SWITCH(s) for (NSString *__s__ = (s); ; )
#define DEFAULT
SWITCH (string) {
CASE (#"TestString") {
break;
}
CASE (#"YetAnotherString") {
break;
}
CASE (#"Test") {
break;
}
DEFAULT {
break;
}
}
I can't Comment on cris's answer on #Cris answer but i would like to say that:
There is an LIMITATION for #cris's method:
typedef enum will not take alphanumeric values
typedef enum
{
12Ace, 23Two, 23Three, 23Four, F22ive ... Jack, Queen, King
} CardType;
So here is another One:
Link Stack over flow Go to this user answer "user1717750"
typedef enum
{
Six,
Seven,
Eight
} cardName;
- (void) switchcardName:(NSString *) param {
switch([[cases objectForKey:param] intValue]) {
case Six:
NSLog(#"Six");
break;
case Seven:
NSLog(#"Seven");
break;
case Eight:
NSLog(#"Eight");
break;
default:
NSLog(#"Default");
break;
}
}
Enjoy Coding.....