I have a signal A that contains integer values. A value of -1 indicates an invalid result, so I'd like to, instead of passing -1 along as a value, send an error. This way anything that subscribes to B will receive valid integers through subscribeNext: and the errors through subscribeError:.
I think I know how to do this with RACSubject:
RACSequence *A = [#[ #(2), #(6), #(5), #(-1), #(4) ] rac_sequence];
RACSubject *B = [RACSubject subject];
[A subscribeNext:^(NSNumber *val) {
if ( [val integerValue] == -1 ) {
[B sendError:[NSError errorWithDomain:#"MyDomain" code:0 userInfo:nil]];
} else {
[B sendNext:val];
}
} error:^(NSError *error) {
[B sendError:error];
} completed:^{
[B sendCompleted];
}];
I'm wondering if there's a more "inline" way to do this along the lines of:
RACSequence *A = [#[ #(2), #(6), #(5), #(-1), #(4) ] rac_sequence];
RACSignal *B = [A filter:^BOOL(id val) {
if ( [val integerValue] == -1 ) {
//FIXME: send an error to B's subscriber(s)
return NO;
} else {
return YES;
}
}
The primary method to do this is by using -flattenMap:, similar to how you've written the -filter: above. Using your example:
RACSignal *B = [A flattenMap:^(NSNumber *number) {
if (number.intValue == -1) {
return [RACSignal error:[NSError errorWithDomain:#"MyDomain" code:0 userInfo:nil]];
} else {
return [RACSignal return:number];
}
}];
Update
Alternatively, using the newer -try: operator:
RACSignal *B = [A try:^(NSNumber *number, NSError **error) {
if (number.intValue == -1) {
*error = [NSError errorWithDomain:#"MyDomain" code:0 userInfo:nil];
return NO;
}
return YES;
}];
Related
I don't have any idea how to merge a lot of signals and get results from a RACTuple, its seems to be like easy answer but I can't found that.
What we have for exmaple:
NSArray *a = #[#{#"k1":#"v1"},
#{#"k2":#"v2"},
#{#"k3":#"v3"},
#{#"k4":#"v4"},
#{#"k5":#"v5"},
#{#"k6":#"v6"},
#{#"k7":#"v7"}];
NSArray *b = #[#{#"kk1":#"vv1"},
#{#"kk2":#"vv2"},
#{#"kk3":#"vv3"},
#{#"kk4":#"vv4"},
#{#"kk5":#"vv5"},
#{#"kk6":#"vv6"},
#{#"kk7":#"vv7"}];
and
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
RACSignal *s1 = [self adaptObjects:a];
RACSignal *s2 = [self adaptObjects:b];
return [[RACSignal merge:#[s1,s2]] map:^id(id value) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:value];
return nil;
}];
}];
}];
[[command execute:nil] subscribeNext:^(RACTuple *x) {
NSLog(#"%#",x);
}];
this operator map is wrong I know that, but this is for example
- (RACSignal *)adaptObjects:(NSArray *)objects {
return [objects.rac_sequence.signal flattenMap:^RACStream *(id x) {
return [self adaptObject:x];
}];
}
- (RACSignal*)adaptObject:(NSDictionary*) x {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// some operations with data here
[subscriber sendNext:x];
return nil;
}];
}
In NSLog I want to see tuple result with two arrays first - s1, second - s2
Thx
I've written a small example, hope it helps you.
NSArray *a = #[#{#"k1":#"v1"},
#{#"k2":#"v2"},
#{#"k3":#"v3"},
#{#"k4":#"v4"},
#{#"k5":#"v5"},
#{#"k6":#"v6"},
#{#"k7":#"v7"}];
NSArray *b = #[#{#"kk1":#"vv1"},
#{#"kk2":#"vv2"},
#{#"kk3":#"vv3"},
#{#"kk4":#"vv4"},
#{#"kk5":#"vv5"},
#{#"kk6":#"vv6"},
#{#"kk7":#"vv7"}];
- (NSArray<RACSignal *> *)rac_signalsFromArray:(NSArray *)array {
NSMutableArray<RACSignal *> *signals = [NSMutableArray array];
for (NSDictionary *dict in array) {
[signals addObject:[RACSignal return:dict]];
}
return signals;
}
NSArray *Asignals = [self rac_signalsFromArray:a];
NSArray *Bsignals = [self rac_signalsFromArray:b];
NSArray *signals = [[NSArray arrayWithArray:Asignals] arrayByAddingObjectsFromArray:Bsignals];
[[RACSignal zip:signals] subscribeNext:^(RACTuple *tuple) {
// tuple here
}];
I am playing with code from some book regarding Reactive Cocoa and I am stuck with this one:
Here is my photo importer code :
+ (RACSignal *)importPhotos {
NSURLRequest *request = [self popularURLRequest];
return [[[[[[NSURLConnection rac_sendAsynchronousRequest:request] map:^id(RACTuple *value) {
return [value second];
}] deliverOn:[RACScheduler mainThreadScheduler]]
map:^id(NSData *value) {
id result = [NSJSONSerialization JSONObjectWithData:value
options:0
error:nil];
return [[[result[#"photos"] rac_sequence] map:^id(NSDictionary *photoDictionary) {
FRPPhotoModel *photoModel = [FRPPhotoModel new];
[self configurePhotoModel:photoModel withDict:photoDictionary];
[self downloadThumbnailForPhotoModel:photoModel];
return photoModel;
}] array];
}] publish] autoconnect];
}
+ (void)configurePhotoModel:(FRPPhotoModel *)photoModel withDict:(NSDictionary *)dict {
photoModel.photoName = dict[#"name"];
photoModel.identifier = dict[#"id"];
photoModel.photographerName = dict[#"user"][#"username"];
photoModel.rating = dict[#"rating"];
[[self urlForImageSize:3 inArray:dict[#"images"]] subscribeNext:^(id x) {
photoModel.thumbnailURL = x;
}];
}
+ (RACSignal *)urlForImageSize:(NSInteger)size inArray:(NSArray *)array {
return [[[[array rac_sequence] filter:^BOOL(NSDictionary *value) {
return [value[#"size"] integerValue] == size;
}] map:^id(NSDictionary *value) {
return value[#"url"];
}] signal];
}
+ (void)downloadThumbnailForPhotoModel:(FRPPhotoModel *)photoModel {
[[RACObserve(photoModel, thumbnailURL) flattenMap:^RACStream *(id value) {
return [self download:value];
}] subscribeNext:^(id x) {
photoModel.thumbnailData = x;
}];
}
+ (RACSignal *)download:(NSString *)urlString {
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
return [[NSURLConnection rac_sendAsynchronousRequest:request] map:^id(RACTuple *value) {
return [value second];
}];
}
and UI is updated like this:
RAC(self.imageView, image) = [[RACObserve(self, photoModel.thumbnailData) filter:^BOOL(id value) {
return value != nil;
}] map:^id(id value) {
return [UIImage imageWithData:value];
}];
Can you please explain why my UI is not updated or updated wrongly with new UIImages which I get from that NSData objects.
So the first problem was that flattenMap delivers on some background RACScheduler. Changed to this:
[[[[RACObserve(photoModel, thumbnailURL) ignore:nil] flattenMap:^RACStream *(id value) {
return [self download:value];
}] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(id x) {
photoModel.thumbnailData = x;
}];
Also another problem was that download:nil was throwing an error, which was not caught by subscriber and thus terminated signal which provided values of observing. Adding ignore:nil fixed issue.
So I have successfully turned a button into an off and on switch that changes the label.
I was also able to have it start a timed processed set off when that is to occur, and it have the ability to shut off the timed process.
Anyways I need to way to shut down the timed process I was wondering if there was a way to stop it without using the disposable. With a second takeUntil signal.
Edit I think what I was trying to do was slightly misleading let me show my current solution that works.
-(RACSignal*) startTimer {
return [[RACSignal interval:1.0
onScheduler:[RACScheduler mainThreadScheduler]]
startWith:[NSDate date]];
}
-(void) viewWillAppear:(BOOL)animated {}
-(void) viewDidLoad {
self.tableView.delegate = self;
self.tableView.dataSource = self;
RACSignal* pressedStart = [self.start rac_signalForControlEvents:UIControlEventTouchUpInside];
#weakify(self);
RACSignal* textChangeSignal = [pressedStart map:^id(id value) {
#strongify(self);
return [self.start.titleLabel.text isEqualToString:#"Start"] ? #"Stop" : #"Start";
}];
// Changes the title
[textChangeSignal subscribeNext:^(NSString* text) {
#strongify(self);
[self.start setTitle:text forState:UIControlStateNormal];
}];
RACSignal* switchSignal = [[textChangeSignal map:^id(NSString* string) {
return [string isEqualToString:#"Stop"] ? #0 : #1;
}] filter:^BOOL(id value) {
NSLog(#"Switch %#",value);
return [value boolValue];
}];
[[self rac_signalForSelector:#selector(viewWillAppear:)]
subscribeNext:^(id x) {
}];
static NSInteger t = 0;
// Remake's it self once it is on finished.
self.disposable = [[[textChangeSignal filter:^BOOL(NSString* text) {
return [text isEqualToString:#"Stop"] ? [#1 boolValue] : [#0 boolValue];
}]
flattenMap:^RACStream *(id value) {
NSLog(#"Made new Sheduler");
#strongify(self);
return [[self startTimer] takeUntil:switchSignal];
}] subscribeNext:^(id x) {
NSLog(#"%#",x);
#strongify(self);
t = t + 1;
NSLog(#"%zd",t);
[self updateTable];
}];
[[self rac_signalForSelector:#selector(viewWillDisappear:)] subscribeNext:^(id x) {
NSLog(#"viewWillAppear Dispose");
[self.disposable dispose];
}];
}
-(BOOL) isGroupedExcercisesLeft {
BOOL isGroupedLeft = NO;
for (int i =0;i < [self.excercises count]; i++) {
Excercise* ex = [self.excercises objectAtIndex:i];
if(ex.complete == NO && ex.grouped == YES) {
isGroupedLeft = YES;
break;
}
}
return isGroupedLeft;
}
-(void) updateTable {
// Find the
NSInteger nextRow;
if (([self.excercises count] > 0 || self.excercises !=nil) && [self isGroupedExcercisesLeft]) {
for (int i =0;i < [self.excercises count]; i++) {
Excercise* ex = [self.excercises objectAtIndex:i];
if(ex.complete == NO && ex.grouped == YES) {
nextRow = i;
break;
}
}
NSIndexPath* path = [NSIndexPath indexPathForItem:nextRow inSection:0];
NSArray* indexPath = #[path];
// update //
Excercise* ex = [self.excercises objectAtIndex:nextRow];
[self.tableView scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionTop animated:YES];
if (ex.seconds <= 0) {
RLMRealm* db = [RLMRealm defaultRealm];
[db beginWriteTransaction];
ex.complete = YES;
[db commitWriteTransaction];
}
else {
// Update Seconds
RLMRealm* db = [RLMRealm defaultRealm];
[db beginWriteTransaction];
ex.seconds = ex.seconds - 1000;
NSLog(#"Seconds: %zd",ex.seconds);
[db commitWriteTransaction];
// Update table
[self.tableView reloadRowsAtIndexPaths:indexPath withRowAnimation:UITableViewRowAnimationNone];
}
} else {
NSLog(#"Done");
SIAlertView *alertView = [[SIAlertView alloc] initWithTitle:#"Deskercise" andMessage:#"Excercises Complete"];
[alertView addButtonWithTitle:#"Ok"
type:SIAlertViewButtonTypeDefault
handler:^(SIAlertView *alert) {
}];
alertView.transitionStyle = SIAlertViewTransitionStyleBounce;
[alertView show];
NSLog(#"Dispose");
[self.disposable dispose];
}
}
The issue with using takeUntil:self.completeSignal is that when you change completeSignal to another value, it isn't passed to any function that was already waiting for the variable that completeSignal was previously holding.
- (RACSignal*) startTimer {
#weakify(self)
return [[[RACSignal interval:1.0
onScheduler:[RACScheduler mainThreadScheduler]]
startWith:[NSDate date]]
takeUntil:[[self.start rac_signalForControlEvents:UIControlEventTouchUpInside]
merge:[[RACObserve(self, completeSignal) skip:1] flattenMap:
^RACStream *(RACSignal * signal) {
#strongify(self)
return self.completeSignal;
}]]
];
}
The signal is now observing and flattening completeSignal, which will give the desired effect. Signals that complete without sending next events are ignored by takeUntil:, so use self.completedSignal = [RACSignal return:nil], which sends a single next event and then completes.
However, this code is anything but ideal, let's look at a better solution.
#property (nonatomic, readwrite) RACSubject * completeSignal;
- (RACSignal*) startTimer {
return [[[RACSignal interval:1.0
onScheduler:[RACScheduler mainThreadScheduler]]
startWith:[NSDate date]]
takeUntil:[[self.start rac_signalForControlEvents:UIControlEventTouchUpInside]
merge:self.completeSignal]
];
}
- (void) viewDidLoad {
[super viewDidLoad];
self.completeSignal = [RACSubject subject];
self.tableView.delegate = self;
self.tableView.dataSource = self;
RACSignal * pressedStart = [self.start rac_signalForControlEvents:UIControlEventTouchUpInside];
#weakify(self);
RACSignal* textChangeSignal = [[pressedStart startWith:nil] scanWithStart:#"Stop" reduce:^id(id running, id next) {
return #{#"Start":#"Stop", #"Stop":#"Start"}[running];
}];
[self.start
rac_liftSelector:#selector(setTitle:forState:)
withSignals:textChangeSignal, [RACSignal return:#(UIControlStateNormal)], nil];
[[[pressedStart flattenMap:^RACStream *(id value) { //Using take:1 so that it doesn't get into a feedback loop
#strongify(self);
return [self startTimer];
}] scanWithStart:#0 reduce:^id(NSNumber * running, NSNumber * next) {
return #(running.unsignedIntegerValue + 1);
}] subscribeNext:^(id x) {
#strongify(self);
[self updateTable];
NSLog(#"%#", x);
}];
}
- (void) updateTable {
//If you uncomment these then it'll cause a feedback loop for the signal that calls updateTable
//[self.start sendActionsForControlEvents:UIControlEventTouchUpInside];
//[self.completeSignal sendNext:nil];
if ([self.excercises count] > 0 || self.excercises !=nil) {
} else {
}
}
Let's run through the list of changes:
completeSignal is now a RACSubject (a manually controlled RACSignal).
For purity and to get rid of the #weakify directive, textChangeSignal now uses the handy scanWithStart:reduce: method, which lets you access an accumulator (this works well for methods that work with an incrementing or decrementing number).
start's text is now being changed by the rac_liftSelector function, which takes RACSignals and unwraps them when all have fired.
Your flattenMap: to replace pressedStart with [self startTimer] now uses scanWithStart:reduce, which is a much more functional way to keep count.
I'm not sure if you were testing by having updateTable contain completion signals but it definitely causes a logic issue with your flattenMap: of pressedButton, the resulting feedback loop eventually crashes the program when the stack overflows.
Here's the source code for the method that appears to be causing the leak.
- (void)search:(CDVInvokedUrlCommand*)command
{
NSString* callbackId = command.callbackId;
NSArray* fields = [command argumentAtIndex:0];
NSDictionary* findOptions = [command argumentAtIndex:1 withDefault:[NSNull null]];
[self.commandDelegate runInBackground:^{
// from Apple: Important You must ensure that an instance of ABAddressBookRef is used by only one thread.
// which is why address book is created within the dispatch queue.
// more details here: http: //blog.byadrian.net/2012/05/05/ios-addressbook-framework-and-gcd/
CDVAddressBookHelper* abHelper = [[CDVAddressBookHelper alloc] init];
CDVContacts* __weak weakSelf = self; // play it safe to avoid retain cycles
// it gets uglier, block within block.....
[abHelper createAddressBook: ^(ABAddressBookRef addrBook, CDVAddressBookAccessError* errCode) {
if (addrBook == NULL) {
// permission was denied or other error - return error
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:errCode ? (int)errCode.errorCode:UNKNOWN_ERROR];
[weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId];
return;
}
NSArray* foundRecords = nil;
// get the findOptions values
BOOL multiple = NO; // default is false
NSString* filter = nil;
NSArray* desiredFields = nil;
if (![findOptions isKindOfClass:[NSNull class]]) {
id value = nil;
filter = (NSString*)[findOptions objectForKey:#"filter"];
value = [findOptions objectForKey:#"multiple"];
if ([value isKindOfClass:[NSNumber class]]) {
// multiple is a boolean that will come through as an NSNumber
multiple = [(NSNumber*)value boolValue];
// NSLog(#"multiple is: %d", multiple);
}
desiredFields = [findOptions objectForKey:#"desiredFields"];
// return all fields if desired fields are not explicitly defined
if (desiredFields == nil || desiredFields.count == 0) {
desiredFields = [NSArray arrayWithObjects:#"*", nil];
}
}
NSDictionary* searchFields = [[CDVContact class] calcReturnFields:fields];
NSDictionary* returnFields = [[CDVContact class] calcReturnFields:desiredFields];
NSMutableArray* matches = nil;
if (!filter || [filter isEqualToString:#""]) {
// get all records
foundRecords = (__bridge_transfer NSArray*)ABAddressBookCopyArrayOfAllPeople(addrBook);
if (foundRecords && ([foundRecords count] > 0)) {
// create Contacts and put into matches array
// doesn't make sense to ask for all records when multiple == NO but better check
int xferCount = multiple == YES ? (int)[foundRecords count] : 1;
matches = [NSMutableArray arrayWithCapacity:xferCount];
for (int k = 0; k < xferCount; k++) {
CDVContact* xferContact = [[CDVContact alloc] initFromABRecord:(__bridge ABRecordRef)[foundRecords objectAtIndex:k]];
[matches addObject:xferContact];
xferContact = nil;
}
}
} else {
foundRecords = (__bridge_transfer NSArray*)ABAddressBookCopyArrayOfAllPeople(addrBook);
matches = [NSMutableArray arrayWithCapacity:1];
BOOL bFound = NO;
int testCount = (int)[foundRecords count];
for (int j = 0; j < testCount; j++) {
CDVContact* testContact = [[CDVContact alloc] initFromABRecord:(__bridge ABRecordRef)[foundRecords objectAtIndex:j]];
if (testContact) {
bFound = [testContact foundValue:filter inFields:searchFields];
if (bFound) {
[matches addObject:testContact];
}
testContact = nil;
}
}
}
NSMutableArray* returnContacts = [NSMutableArray arrayWithCapacity:1];
if ((matches != nil) && ([matches count] > 0)) {
// convert to JS Contacts format and return in callback
// - returnFields determines what properties to return
#autoreleasepool {
int count = multiple == YES ? (int)[matches count] : 1;
for (int i = 0; i < count; i++) {
CDVContact* newContact = [matches objectAtIndex:i];
NSDictionary* aContact = [newContact toDictionary:returnFields];
[returnContacts addObject:aContact];
}
}
}
// return found contacts (array is empty if no contacts found)
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:returnContacts];
[weakSelf.commandDelegate sendPluginResult:result callbackId:callbackId];
// NSLog(#"findCallback string: %#", jsString);
if (addrBook) {
CFRelease(addrBook);
}
}];
}]; // end of workQueue block
return;
}
The specific line that is doing most of the leaking is foundRecords = (__bridge_transfer NSArray*)ABAddressBookCopyArrayOfAllPeople(addrBook);, but this is confusing, given that the correct __bridge_transfer call is used. What's going on here?
I have two data sources that are pulling in different arrays of both Contacts and Users and an aggregate Invitee datasource which is created to combine and keep references to the results of the Contacts and Users:
AddressBookDataSource:
- (RACSignal *)getContacts {
return [[[[self getContactsSignal] flattenMap:^RACStream *(NSArray *contacts) {
return contacts.rac_sequence.signal;
}]
map:^id(APContact *contact) {
return [[Contact alloc] initWithAPContact:contact];
}] collect];;
}
- (RACSignal*)getContactsSignal {
APAddressBook *addressBook = [[APAddressBook alloc] init];
addressBook.fieldsMask = APContactFieldFirstName | APContactFieldCompositeName | APContactFieldPhoto;
RACSignal *addressBookSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[addressBook loadContacts:^(NSArray *contacts, NSError *error) {
if (error) {
[subscriber sendError:error];
} else {
[subscriber sendNext:contacts];
}
}];
return nil;
}];
return addressBookSignal;
}
ParseDataSource:
- (RACSignal *)getUsers {
return [[[[[self getUsersSignal] flattenMap:^RACStream *(NSArray *users) {
return users.rac_sequence.signal;
}] filter:^BOOL(User *user) {
return ![user.username isEqualToString:[User currentUser].username];
}] map:^id(User *user) {
return user;
}] collect];
}
- (RACSignal*)getUsersSignal {
RACSignal *getUsersSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
PFQuery *userQuery = [User query];
[userQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (error) {
[subscriber sendError:error];
} else {
[subscriber sendNext:objects];
}
}];
return nil;
}];
return getUsersSignal;
}
InviteeDataSource:
- (RACSignal*)getPotentialInvitees {
ParseDataSource *parseDataSource = [[ParseDataSource alloc] init];
AddressBookDataSource *addressBookDataSource = [[AddressBookDataSource alloc] init];
return [[RACSignal concat:#[
[parseDataSource getUsers],
[addressBookDataSource getContacts]
]]
flattenMap:^RACSignal *(RACTuple *tuple) {
RACTupleUnpack(NSArray *users, NSArray *contacts) = tuple;
_contactSection.contacts = contacts;
_userSection.users = users;
return [RACSignal empty];
}];
}
The problem is that the flattenMap block never gets called, meaning the subsequent subscribers never have their subscribeNext blocks called.
Help?
Thanks to a twitter reply from #jspahrsummers, there were a couple things that were making this not behave as I'd like, but the root of the problem seemed to be that I was not calling -sendCompleted on the subscriber in the -getUsersSignal and -getContactsSignal.