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.
I have a paginated tableView with 2 tabs at the top, Latest and Popular, and I can't get it to animate model changes properly with [tableView beginUpdates] and [tableView endUpdates].
There are 3 loading modes for the tableView:
LoadingModeRefresh: when the user refreshes with pull to refresh.
LoadingModeNewPage: when the user reaches the end of the tableView so that a new page needs to be loaded
LoadingModeNewTab: when the user changes the tab at the top
I receive 30 items per page from the backend (for pagination)
Also I have a loading cell at the end of the tableView so that when someone reaches to the end of the table, he/she can see that it's loading, and when new data arrives, that loading cell is pushed to the end of the tableView, out of sight.
The behaviour I want is as follows:
When the user pulls to refresh, 30 new items are fetched and are set as the backing model for the tableView, and the tableView should reload items from 0 to 29, and delete the rest since the user might have paged more than 1 page so there might be more than 30 items before the refresh, hence we need to delete the extra items after the refresh.
When the user reaches the end of the table, 30 new items are fetched and are appended to the end of the backing model for the tableView, and we should insert those 30 rows to the tableView.
When the user switches tabs, I first want the tableView to be cleared of all the cells so that the loading cell I mentioned is the only cell. so I set the backing model of the tableView to an empty array, and remove all the rows from the tableView. after 30 new items are fetched I insert those 30 rows to the tableView.
I have 3 properties on my ViewModel representing changes to indexPaths: indexPathsToDelete, indexPathsToInsert and indexPathsToReload, which are bound to changes in the backing model with -[RACSignal combinePrevious:reduce:]. My view controller observes these properties, zips them together and applies the changes at once. however somehow my fetching code is being called twice, which causes the indexPaths to be calculated and observed more than once, which causes inconsistencies with the tableView and causes crashes. Can anyone help with where the problem is, since I can not seem to understand what's happening?
here is the code for the ViewModel (sorry for the code, haven't refactored it yet since I haven't got it to work yet):
const NSInteger kArticlePerPage = 30;
#interface FeedViewModel ()
#property (nonatomic, readwrite) Source *model;
#property (nonatomic) LoadingMode loadingMode;
#property (nonatomic) NSInteger selectedTabIndex;
#property (nonatomic, readwrite) NSInteger pagesRequested;
#property (nonatomic, readwrite) NSInteger pagesLoaded;
#property (nonatomic, readwrite) NSArray *indexPathsToDelete;
#property (nonatomic, readwrite) NSArray *indexPathsToInsert;
#property (nonatomic, readwrite) NSArray *indexPathsToReload;
- (NSArray *)indexPathsForRange:(NSRange)range inSection:(NSInteger)section;
#end
#implementation FeedViewModel
- (instancetype)initWithModel:(Source *)source
{
self = [super init];
if (!self) {
return nil;
}
_model = source;
_pagesLoaded = 0;
RACSignal *loadingModeSignal = RACObserve(self, loadingMode);
RACSignal *newTabSignal = [loadingModeSignal //
filter:^BOOL(NSNumber *loadingMode) {
return loadingMode.integerValue == LoadingModeNewTab;
}];
RACSignal *newPageSignal = [loadingModeSignal //
filter:^BOOL(NSNumber *loadingMode) {
return loadingMode.integerValue == LoadingModeNewPage;
}];
RACSignal *refreshSignal = [loadingModeSignal //
filter:^BOOL(NSNumber *loadingMode) {
return loadingMode.integerValue == LoadingModeRefresh;
}];
RAC(self, loading) = [loadingModeSignal //
map:^id(NSNumber *loadingMode) {
switch (loadingMode.integerValue) {
case LoadingModeFinished:
return #(NO);
default:
return #(YES);
}
}];
#weakify(self);
RACSignal *newArticlesSignal = [[[[RACSignal
combineLatest:#[ RACObserve(self, pagesRequested), RACObserve(self, selectedTabIndex) ]]
sample:RACObserve(self, loadingMode)] //
map:^id(RACTuple *tuple) {
#strongify(self);
return [self signalForNewArticlesForPage:tuple.first order:[tuple.second integerValue]];
}] //
switchToLatest];
RACSignal *articlesForNewTabSignal = [[newTabSignal //
flattenMap:^RACStream * (id value) { //
return [newArticlesSignal startWith:#[]];
}] //
skip:1];
RACSignal *articlesForNewPageSignal = [newPageSignal //
flattenMap:^RACStream * (id value) {
return [newArticlesSignal //
map:^id(NSArray *newArticles) {
#strongify(self);
Article *article = self.articles[0];
NSLog(#"article name: %#", article.title);
return [self.articles arrayByAddingObjectsFromArray:newArticles];
}];
}];
RACSignal *articlesForRefreshSignal = [refreshSignal //
flattenMap:^RACStream * (id value) { //
return newArticlesSignal;
}];
RAC(self, articles) = [RACSignal merge:#[
articlesForNewTabSignal, //
articlesForNewPageSignal, //
articlesForRefreshSignal
]];
RACSignal *articlesSignal = RACObserve(self, articles);
RAC(self, indexPathsToDelete) = [articlesSignal //
combinePreviousWithStart:#[] //
reduce:^id(NSArray *previous, NSArray *current) {
#strongify(self);
if (previous.count > current.count) {
return [self
indexPathsForRange:NSMakeRange(current.count,
previous.count - current.count)
inSection:0];
}
else {
return #[];
}
}];
RAC(self, indexPathsToInsert) = [articlesSignal //
combinePreviousWithStart:#[] //
reduce:^id(NSArray *previous, NSArray *current) { //
#strongify(self);
if (previous.count < current.count) {
return [self
indexPathsForRange:NSMakeRange(previous.count,
current.count - previous.count)
inSection:0];
}
else {
return #[];
}
}];
RAC(self, indexPathsToReload) = [articlesSignal //
combinePreviousWithStart:#[] //
reduce:^id(NSArray *previous, NSArray *current) {
if (previous.count >= current.count) {
return [self indexPathsForRange:NSMakeRange(0, current.count)
inSection:0];
}
else {
return #[];
}
}];
RAC(self, pagesLoaded) = [[RACObserve(self, articles) //
skip:1] //
map:^id(NSArray *array) { //
NSInteger pages = array.count / kArticlePerPage;
if (array.count % kArticlePerPage != 0) {
pages++;
}
return #(pages);
}];
RAC(self, separatorColorHexString) = [RACObserve(self, model.type) map:^id(NSNumber *type) {
if (type.integerValue == SourceTypeInspiration) {
return #"ffffff";
}
else {
return #"E5E5E5";
}
}];
RAC(self, segmentTitles) = [RACObserve(self, model) //
map:^id(Source *source) {
NSMutableArray *titles = [NSMutableArray array];
if (source.isPopularAvailable) {
[titles addObject:#"Popular"];
}
if (source.isLatestAvailable) {
[titles addObject:#"Latest"];
}
return titles;
}];
return self;
}
- (void)setCurrentSource:(Source *)source
{
self.model = source;
}
- (void)refreshCurrentTab
{
self.pagesRequested = 1;
self.loadingMode = LoadingModeRefresh;
}
- (void)requestNewPage
{
if (self.pagesRequested == self.pagesLoaded + 1) {
return;
}
self.pagesRequested = self.pagesLoaded + 1;
self.loadingMode = LoadingModeNewPage;
}
- (void)selectTabWithIndex:(NSInteger)index
{
self.selectedTabIndex = index;
self.pagesRequested = 1;
self.loadingMode = LoadingModeNewTab;
}
- (void)selectTabWithIndexIfNotSelected:(NSInteger)index
{
if (self.selectedTabIndex == index) {
return;
}
[self selectTabWithIndex:index];
}
- (NSArray *)indexPathsForRange:(NSRange)range inSection:(NSInteger)section
{
NSMutableArray *indexes = [NSMutableArray array];
for (NSUInteger i = range.location; i < range.location + range.length; i++) {
[indexes addObject:[NSIndexPath indexPathForRow:i inSection:section]];
}
return [indexes copy];
}
- (RACSignal *)signalForNewArticlesForPage:(NSNumber *)pageNumber order:(ArticleOrder)order
{
return [[SourceManager sharedManager] articlesForSourceKey:self.model.key
articleOrder:order
pageNumber:pageNumber];
}
- (void)setLoadingMode:(LoadingMode)loadingMode
{
_loadingMode = loadingMode;
}
#end
and the code for observing indexPath arrays in ViewController.m:
RACSignal *toDeleteSignal = [RACObserve(self, viewModel.indexPathsToDelete) //
deliverOn:[RACScheduler mainThreadScheduler]]; //
RACSignal *toInsertSignal = [RACObserve(self, viewModel.indexPathsToInsert) //
deliverOn:[RACScheduler mainThreadScheduler]]; //
RACSignal *toReloadSignal = [RACObserve(self, viewModel.indexPathsToReload) //
deliverOn:[RACScheduler mainThreadScheduler]];
[[RACSignal zip:#[ toDeleteSignal, toInsertSignal, toReloadSignal ]]
subscribeNext:^(RACTuple *tuple) {
#strongify(self);
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:tuple.first
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:tuple.second
withRowAnimation:UITableViewRowAnimationTop];
[self.tableView reloadRowsAtIndexPaths:tuple.third
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
if (self.tableView.pullToRefresh.state == BPRPullToRefreshStateLoading) {
[self.tableView.pullToRefresh dismiss];
}
}];
Other than that, I only call [self.viewModel requestNewPage] when tableView:willDisplayCell: is called for the last cell in the tableView, [self.viewModel selectTabWithIndexIfNotSelected:index] when the segment selection changes. [self.viewModel selectTabWithIndex:0] in viewDidLoad and [self.viewModel refreshCurrentTab] in the refresh handler.
Where am I making a mistake?
basically I'm using firebase to query a user's 'status' property and doing so in a do/while loop.
If the status property is free then i want to break the loop and continue with the rest of the method. If the status property is not free then I want to query firebase again for a new user until a free user is found.
My firebase code works fine outside the loop but doesn't seem to be called inside of it. Here is the loop:
__block uint32_t rando;
self.freedom = #"about to check";
do {
//check if free
[self checkIfFree:^(BOOL finished) {
if (finished) {
if ([self.freedom isEqualToString:#"free"]) {
//should break loop here
}
else if ([self.freedom isEqualToString:#"matched"]){
//get another user
do {
//picking another random user from array
rando = arc4random_uniform(arraycount);
}
while (rando == randomIndex && rando == [self.randString intValue]);
self.randString = [NSString stringWithFormat:#"%u", rando];
[users removeAllObjects];
[users addObject:[usersArray objectAtIndex:rando]];
self.freeUser = [users objectAtIndex:0];
//should repeat check here but doesn't work
}
else{
NSLog(#"error!");
}
}
else{
NSLog(#"not finished the checking yet");
}
}];
} while (![self.freedom isEqual: #"free"]);
And here's my firebase code:
-(void)checkIfFree:(myCompletion) compblock{
self.freeUserFB = [[Firebase alloc] initWithUrl:[NSString stringWithFormat: #"https://skipchat.firebaseio.com/users/%#", self.freeUser.objectId]];
[self.freeUserFB observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot)
{
self.otherStatus = snapshot.value[#"status"];
NSLog(#"snapshot info %#", snapshot.value);
if ([self.otherStatus isEqualToString:#"free"]) {
self.userIsFree = YES;
self.freedom = #"free";
}
else{
self.userIsFree = NO;
self.freedom = #"matched";
}
compblock(YES);
}];
}
Thanks!
I am not sure I understand correctly your question.
If you want to run your completion code again till some condition is matched (in this case [self.freedom isEqualToString:#"free"]) you can do the following (removing the do - while):
void( ^ myResponseBlock)(BOOL finished) = ^ void(BOOL finished) {
if (finished) {
if ([self.freedom isEqualToString: #"free"]) {
return;
} else if ([self.freedom isEqualToString: #"matched"]) {
//get another user
do {
//picking another random user from array
rando = arc4random_uniform(arraycount);
}
while (rando == randomIndex && rando == [self.randString intValue]);
self.randString = [NSString stringWithFormat: #"%u", rando];
[users removeAllObjects];
[users addObject:usersArray[rando]];
self.freeUser = users.firstObject;
// Schedule another async check
[self checkIfFree: myResponseBlock];
} else {
NSLog(#"error!");
}
} else {
NSLog(#"not finished the checking yet");
}
};
[self checkIfFree: myResponseBlock];
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;
}];
I'm trying to use the RACSignal class's interval method of ReactiveCocoa.
The following code works every second after 1 seconds.
But I want it works immediately and every second.
What's the best way?
#weakify(self);
[[[RACSignal interval:1.0] takeUntilBlock:^BOOL(id x) {
return [AClass count] == 0;
}] subscribeNext:^(id x) {
dispatch_async(dispatch_get_main_queue(), ^{
#strongify(self);
NSUInteger count = [AClass count];
self.title = [NSString stringWithFormat:#"%u", count];
});
} completed:^{
dispatch_async(dispatch_get_main_queue(), ^{
#strongify(self);
self.title = #"";
});
}];
I believe you're looking for -startWith:.
[[[RACSignal interval:1] startWith:NSDate.date] takeUntilBlock:^(id _) { // ...