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.
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?
I succeed to add friend with Cloud Code and Parse.com.
Now I would like to delete friend relation with Cloud Code in didSelectRowAtIndexPath
My error is "attempt to insert nil object from objects[0]'"
But I don't know what parameters I need to configure, I found the cloud code for main.js :
Parse.Cloud.define("removeFriend", function(request, response)
{
// here's how to get the client's user making the request
var user = request.user;
// consider checking that user && request.params.friend are valid
// if not, return response.error("missing user or friend id")
getUser(request.params.friend).then(function(friend) {
// your code prematurely called response.success() here, thereby canceling any further steps
friend.relation("friendsRelation").remove(user);
// return the promise returned by save() so we can chain the promises
return friend.save();
}).then(function(result) {
// only now that the save is finished, we can claim victory
response.success(result);
}, function (error) {
response.error(result);
});
});
// EDIT - the OP once referred to a getUser function that we assume to be something like this:
// return a promise to get a user with userId
function getUser(userId) {
var userQuery = new Parse.Query(Parse.User);
return userQuery.get(userId);
}
Here is my code EditFriends.m :
- (void)viewDidLoad
{
[super viewDidLoad];
PFQuery *query = [PFUser query];
[query orderByAscending:#"name"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (error) {
NSLog(#"Error: %# %#", error, [error userInfo]);
}
else {
self.allUsers = objects;
[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
}
}];
self.currentUser = [PFUser currentUser];
[self loadFriends];
}
-(void) loadFriends{
self.friendsRelation = [[PFUser currentUser] objectForKey:#"friends"];
PFQuery *query = [self.friendsRelation query];
[query orderByAscending:#"username"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error)
{
if (error) {
NSLog(#"Error %# %#", error, [error userInfo]);
}
else {
self.friends = objects;
[self.tableView reloadData];
}
}];
}
- (BOOL)isFriend:(PFUser *)user {
for(PFUser *friend in self.friends) {
if ([friend.objectId isEqualToString:user.objectId]) {
return YES;
}
}
return NO;
}
CellForRowAtIndexPath :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
PFUser *user = [self.allUsers objectAtIndex:indexPath.row];
NSString *name = [[self.allUsers objectAtIndex:indexPath.row] valueForKey:#"username"];
cell.textLabel.text = name;
if ([self isFriend:user]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
didSelectRowAtIndexPath :
PFUser *selected = [self.allUsers objectAtIndex:indexPath.row];
if ([self isFriend:selected]) {
NSLog(#"déjà amis");
// PFObject *friendRequest = [self.friendRequests objectAtIndex:indexPath.row];
[PFCloud callFunctionInBackground:#"removeFriend" withParameters:#{#"friendRequest" : selected.objectId} block:^(id object, NSError *error) {
if (!error) {
//add the fromuser to the currentUsers friends
//save the current user
[self.currentUser saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
} else {
}
}];
}
else {
}
}];
}
else{
PFUser *selectedUser = [self.allUsers objectAtIndex:indexPath.row];
//request them
PFObject *friendRequest = [PFObject objectWithClassName:#"FriendRequest"];
friendRequest[#"from"] = self.currentUser;
friendRequest[#"fromUsername"] = [[PFUser currentUser] objectForKey:#"username"];
//selected user is the user at the cell that was selected
friendRequest[#"to"] = selectedUser;
// set the initial status to pending
friendRequest[#"status"] = #"pending";
[friendRequest saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Yay" message:#"Friend request sent" delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[alert show];
} else {
// error occurred
}
}];
}
The iOS code looks okay, and only needs to be sure it sends the correct user objectId.
The cloud code is close, but must be improved a little:
Parse.Cloud.define("removeFriend", function(request, response)
{
// here's how to get the client's user making the request
var user = request.user;
// consider checking that user && request.params.friend are valid
// if not, return response.error("missing user or friend id")
getUser(request.params.friendRequest).then(function(friend) {
// your code prematurely called response.success() here, thereby canceling any further steps
console.log("relation is:" + JSON.stringify(friend.relation("friends")));
friend.relation("friends").remove(user);
// return the promise returned by save() so we can chain the promises
return friend.save();
}).then(function(friend) {
// friendship goes both ways, so remove the friend from user's friends
user.relation("friends").remove(friend);
return user.save();
}).then(function(result) {
// only now that the save is finished, we can claim victory
console.log("relation is:" + JSON.stringify(result.relation("friends")));
response.success(result);
}, function (error) {
response.error(error);
});
});
// EDIT - the OP once referred to a getUser function that we assume to be something like this:
// return a promise to get a user with userId
function getUser(userId) {
var userQuery = new Parse.Query(Parse.User);
return userQuery.get(userId);
}
EDIT - to call:
PFUser *selected = [self.allUsers objectAtIndex:indexPath.row];
if ([self isFriend:selected]) {
NSLog(#"déjà amis");
[PFCloud callFunctionInBackground:#"removeFriend" withParameters:#{#"friendRequest" : selected.objectId} block:^(id object, NSError *error) {
// etc.
I have an IBAction that when called, registers a user information with Parse.com... I have only implemented the email and username methods, since i need to make sure that prior to submission to the server i added a counter int, if the counter == 2 it will execute the registration... well to my surprise, when I run the code, it gets ran backwards, so my conditional statement at the bottom is not even reviewed, why is this the case?
This is my code:
- (IBAction)signMeUpButton:(id)sender {
[self.view endEditing:YES];
counter = 0;
user = [PFUser user];
NSString *emailFromTextField = self.emailTF.text;
if ([self isValidEmailAddress:emailFromTextField]) {
[self emailHasBeenTaken:emailFromTextField completion:^(BOOL emailIsTaken, NSError *error) {
if (error) {
// TODO: handle any errors here
return;
}
if (!emailIsTaken) {
emailString = emailFromTextField;
user.email = emailString;
counter++;
NSLog(#"The email is %# the counter is %i", emailString, counter);
}
else {
[self duplicateEmail];
}
}];
}
NSString *usernameFromTextField = self.usernameTF.text;
if (usernameFromTextField.length >= 1) {
[self usernameHasBeenTaken:usernameFromTextField completion:^(BOOL usernameIsTaken, NSError *error) {
if (error) {
return;
}
if (!usernameIsTaken) {
usernameString = usernameFromTextField;
user.username = usernameString;
counter++;
NSLog(#"The username is %# and the counter is %i", usernameString, counter);
}
else {
//
}
}];
}
if (counter == 2) {
NSLog(#"Its working");
}
}
And this is what I get on my console when i run it, as you can see the usernameHasBeenTaken is being called FIRST, even though I have it written second... why is that?
2014-07-17 23:18:12.169 app[28210:60b] in the usernameHasBeenTaken. USERNAME IS NOT EXISTENT
2014-07-17 23:18:12.170 app[28210:60b] The username is sample and the counter is 1
2014-07-17 23:18:15.328 app[28210:60b] in the emailHasBeenTaken, EMAIL IS NOT EXISTENT
2014-07-17 23:18:15.328 app[28210:60b] The email is sample#email.com the counter is 2
These are my 2 methods: usernameHasBeenTaken and emailHasBeenTaken
- (void)emailHasBeenTaken:(NSString *)email completion:(void(^)(BOOL emailIsTaken, NSError *error))completionBlock
{
void (^completionCopy)(BOOL, NSError *) = [completionBlock copy];
PFQuery *query = [PFUser query];
[query whereKey:#"email" equalTo:email];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (error) {
NSLog(#"in the emailHasbeenTaken ERROR HAS OCCURRED");
if (completionCopy) {
completionCopy(NO, error);
}
return;
}
if (objects.count > 0) {
NSLog(#"in the emailHasbeenTaken EMAIL IS DUPLICATE");
if (completionCopy) {
completionCopy(YES, nil);
}
}
else {
NSLog(#"in the emailHasBeenTaken, EMAIL IS NOT EXISTENT");
if (completionCopy) {
completionCopy(NO, nil);
}
}
}];
}
- (void) usernameHasBeenTaken:(NSString *)username completion:(void(^)(BOOL usernameIsTaken, NSError *error))completionBlock
{
void (^completionCopy)(BOOL, NSError *) = [completionBlock copy];
PFQuery *query = [PFUser query];
[query whereKey:#"username" equalTo:username];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (error) {
NSLog(#"in the usernameHasBeenTaken ERROR HAS OCCURED");
if (completionCopy) {
completionCopy(NO, error);
}
return;
}
if (objects.count > 0) {
NSLog(#"in the usernameHasBeenTaken USERNAME IS DUPLICATE");
if (completionCopy) {
completionCopy(NO, nil);
}
}
else {
NSLog(#"in the usernameHasBeenTaken. USERNAME IS NOT EXISTENT");
if (completionCopy) {
completionCopy(NO, nil);
}
}
}];
}
Your use of a completion block indicates that you are running them asynchronously. If that is the case then your password condition gets hit immediately after you invoke emailHasBeenTaken:completion:
If you must run async, which you should if you are hitting a web service, you need to nest your calls in the completion blocks.
Edit
It appears that this is the case. The Parse API runs async, to avoid locking the UI thread. You will need to nest your callbacks like I show below.
- (IBAction)signMeUpButton:(id)sender {
[self.view endEditing:YES];
counter = 0;
user = [PFUser user];
NSString *emailFromTextField = self.emailTF.text;
if ([self isValidEmailAddress:emailFromTextField]) {
[self emailHasBeenTaken:emailFromTextField completion:^(BOOL emailIsTaken, NSError *error) {
if (error) {
// TODO: handle any errors here
return;
}
if (!emailIsTaken) {
emailString = emailFromTextField;
user.email = emailString;
counter++;
NSLog(#"The email is %# the counter is %i", emailString, counter);
NSString *usernameFromTextField = self.usernameTF.text;
if (usernameFromTextField.length >= 1) {
[self usernameHasBeenTaken:usernameFromTextField completion:^(BOOL usernameIsTaken, NSError *error) {
if (error) {
return;
}
if (!usernameIsTaken) {
usernameString = usernameFromTextField;
user.username = usernameString;
counter++;
NSLog(#"The username is %# and the counter is %i", usernameString, counter);
}
else {
//
}
if (counter == 2) {
NSLog(#"Its working");
}
}];
}
}
else {
[self duplicateEmail];
}
}];
}
}