I am still trying to wrap my head around ReactiveCocoa, and one thing in particular is causing headaches. I would like to have a declarative interface to the CoreBluetooth API that lets me do something like this:
#property (nonatomic) NSNumber *registerValue;
#property (nonatomic) NSNumber *serialNumber;
RACSignal *characteristics = [[RACObserve(self, connectedPeripheral) flattenMap:^RACStream *(CBPeripheral *peripheral) {
return [peripheral rac_discoverServices:#[[CBUUID UUIDWithString:kBLEServiceUUIDString]]];
}] flattenMap:^RACStream *(CBService *service) {
return [service rac_discoverCharacteristics:nil];
}];
RACSignal *devInfoCharacteristics = [[RACObserve(self, connectedPeripheral) flattenMap:^RACStream *(CBPeripheral *peripheral) {
return [peripheral rac_discoverServices:#[[CBUUID UUIDWithString:kDevInfoServiceUUIDString]]];
}] flattenMap:^RACStream *(CBService *service) {
return [service rac_discoverCharacteristics:nil];
}];
RACSignal *registerUpdates = [[[characteristics filter:^BOOL(CBCharacteristic *characteristic) {
return ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kRegisterCharacteristicUUIDString]]);
}] flattenMap:^RACStream *(CBCharacteristic *characteristic) {
return [[characteristic rac_updateNotificationState:YES] mapReplace:characteristic];
}] flattenMap:^RACStream *(CBCharacteristic *characteristic) {
return [characteristic rac_didUpdateValue];
}];
RACSignal *serialNumberSignal = [[[characteristics filter:^BOOL(CBCharacteristic *characteristic) {
return ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kSerialNumberCharacteristicUUIDString]]);
}] flattenMap:^RACStream *(CBCharacteristic *characteristic) {
return [characteristic rac_didUpdateValue];
}];
RAC(self, serialNumber) = serialNumberSignal;
RAC(self, registerValue) = registerUpdates;
Now, the implementation of rac_discoverServices looks something like this (a category on CBPeripheral):
- (RACSignal *)rac_discoverServices:(NSArray *)serviceUUIDs
{
#weakify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
#strongify(self);
RACDisposable *disposable = [[[[self rac_didDiscoverServices] take:1] flattenMap:^RACStream *(id value) {
return [[[self.services rac_sequence] signal] filter:^BOOL(CBService *service) {
if (!serviceUUIDs) {
return YES;
} else {
for (CBUUID *uuid in serviceUUIDs) {
if ([uuid isEqual:service.UUID]) {
return YES;
}
}
return NO;
}
}];
}] subscribe:subscriber];
[self discoverServices:serviceUUIDs];
return disposable;
}];
}
The problem is that I don't think this can work, because I don't believe the callback ordering can be guaranteed. So what I really want is to be able to ensure that the subscription to the second signal cannot issue the call to discover new services until the first signal has completed.
I understand that RACCommand will prevent concurrent operation, but I don't see a way to use it to ensure that the operations are queued.
Is there some obvious solution I am missing? The only thing I can think of is to somehow maintain a queue of calls and call discover again when the prior signal is disposed of (and I'm not certain that would work either).
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.
[[[[[self.textfield.rac_textSignal throttle:0.5] flattenMap:^RACStream *(id value) {
//call api
return [API signal];
}] flattenMap:^RACStream *(NSArray *result) {
result = [result.rac_sequence take:150].array;
//result is json array
return result;
}] map:^id (NSArray *result) {
return [result.rac_sequence map:^id (JSON *r) {
//handle json item
return item;
}].array;
}] subscribeNext:^(NSArray *result) {
//reload ui
} error:^(NSError *error) {
} completed:^{
}];
How to avoid the nested map in the signal sequence?
It is a better way to handle this?
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.
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.