Animate row additions/reloads/deletions in paginated UITableView with ReactiveCocoa - ios

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?

Related

Tableview is not displaying the NewsFeed using objective-c

News feed are not shown in table viewcontroller.BuyerSocialPage that is linked with Newsfeed Viewcontroller has BuyerSocialPage.h file
#interface BuyerSocialPage : UIViewController <UITableViewDataSource,UITableViewDelegate>
#end
#implementation BuyerSocialPage
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.tableView.delegate=self;
UINib * firstNib = [UINib nibWithNibName:#"BSPFirstCell" bundle:nil];
[self.tableView registerNib:firstNib forCellReuseIdentifier:#"BSPFirstCell"];
UINib * secondNib = [UINib nibWithNibName:#"BSPSecondCell" bundle:nil];
[self.tableView registerNib:secondNib forCellReuseIdentifier:#"BSPSecondCell"];
UINib * thirdNib = [UINib nibWithNibName:#"BSPThirdCell" bundle:nil];
[self.tableView registerNib:thirdNib forCellReuseIdentifier:#"BSPThirdCell"];
UINib * fourthNib = [UINib nibWithNibName:#"BSPFourthCell" bundle:nil];
[self.tableView registerNib:fourthNib forCellReuseIdentifier:#"BSPFourthCell"];
self.view.backgroundColor = [UIColor whiteColor];
[self getBuyerSocialPage];
if (self.revealViewController) {
[_sidebarButton addTarget:self.revealViewController action:#selector(revealToggle:) forControlEvents:UIControlEventTouchUpInside];
[self.view addGestureRecognizer:self.revealViewController.panGestureRecognizer];
}
}
-(void)getBuyerSocialPage {
}
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
int row = (int)indexPath.row;
if (row == 0) {
return 324;
}
else if (row == 1)
{
return 152;
}
else if (row == 2)
{
return 152;
}
else
{
return 152;
}
}
#end
After login you will see home screen. From side menu bar at the top select the "news feed" and it should display the news feed.but it is not displaying newsfeeds.Api is running correctly on postman
How I can get news feed in the table view ?
Page Number is missing in your url &pageno=1.
Your url looks like this:
http://api.shoclef.com/api/NewsFeed?user_id=1164
It should be like this:
http://api.shoclef.com/api/NewsFeed?user_id=1164&pageno=1
try this in you API Manager class. Working fine for me.
NSString * url = [NSString stringWithFormat:#"%#NewsFeed?user_id=%I&pageno=1",API_BASE_URL,userID];
Update For Image
Update this code in your cellForRowAtIndexPath function in NewsFeedNew class
NSString *strUrl = [self.images objectAtIndex:indexPath.row].image;
strUrl = [strUrl stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]];
[cell.image sd_setImageWithURL:[NSURL URLWithString:strUrl]];
You need to set tableview delegate and dataSource.
Also put a break point in numberOfRowsInSection method to verify that tableview is set with datasource and delegate.
You need to reload data of tableview when you get response - [self.tableView reloadData];
-(void)getBuyerSocialPage {
NSLog(#"getBuyerSocialPage");
UserDao * profileID = [[DatabaseManager sharedManager]getLoggedInUser];
ApiManager * manager = [ApiManager sharedManager];
[manager socialPageWithProfileID:profileID.userID withCompletionBlock:^(BOOL error, NSDictionary *socialPage) {
// NSMutableArray * details = [[NSMutableArray alloc]init];
for (NSDictionary * temp in socialPage ) {
[self.socialPageArray addObject:temp];
}
[self.tableView reloadData];
if (!error) {
self.profileImages=self.socialPageArray;
}
}];
}
Few things to debug here,
Your API response might be nil or having error check and log appropriate response.
If the server response is correct then get on the main thread and reload the TableView
Set the UITableViewDataSource and UITableViewDelegate to self if you haven't done in storyboard.
- (void)getBuyerSocialPage {
NSLog(#"getBuyerSocialPage");
UserDao *profileID = [[DatabaseManager sharedManager] getLoggedInUser];
ApiManager *manager = [ApiManager sharedManager];
__weak BuyerSocialPage *weakSelf = self;
[manager socialPageWithProfileID:profileID.userID withCompletionBlock:^(BOOL error, NSDictionary *socialPage) { [weak self]
if (error) {
NSLog("Error fetching data");
return;
}
for (NSDictionary *temp in socialPage ) {
[weakSelf.socialPageArray addObject:temp];
}
if ([weakSelf.socialPageArray count] > 0) {
// update UI on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.tableView.reloadData()
}
}
}]
}

How to set display time for each item using iCarousel when auto-scroll

As my title indicated, I want to know if there's a iCarousel delegate method as well as property that I can use to set display time for each item for my banner view when auto-scroll.
Previously, I used SDCycleScrollView for my banner view and I did it this way below:
- (void)cycleScrollView:(SDCycleScrollView *)cycleScrollView didScrollToIndex:(NSInteger)index {
BannerData *data = self.bannerDatas[index];
NSLog(#"***************cycleScrollView************************");
NSLog(#"index --> %ld", (long)index);
NSLog(#"banner data --> %#", data);
NSLog(#"banner data duration --> %d", data.duration);
cycleScrollView.autoScrollTimeInterval = data.duration;
}
data.duration is duration for each item to stay still.
How can I achieve this with iCarousel? Thanks in advance.
Here below are my iCarousel methods so far:
- (NSInteger)numberOfItemsInCarousel:(__unused iCarousel *)carousel
{
NSLog(#">>>>>>>>>number of items in Carousel --> %lu", (unsigned long)[self.bannerDatas count]);
return (NSInteger)[self.bannerDatas count];
}
- (UIView *)carousel:(__unused iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
{
view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 520, 375)];
NSLog(#"url --> %#", [NSURL URLWithString:[self.netImages objectAtIndex:index]]);
[((UIImageView *)view)sd_setImageWithURL:[NSURL URLWithString:[self.netImages objectAtIndex:index]] placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
view.contentMode = UIViewContentModeCenter;
return view;
}
- (void)scrollToItemAtIndex:(NSInteger)index duration:(NSTimeInterval)duration
{
BannerData *data = self.bannerDatas[index];
NSLog(#"***************cycleScrollView************************");
NSLog(#"index --> %ld", (long)index);
NSLog(#"banner data --> %#", data);
NSLog(#"banner data duration --> %d", data.duration);
}
- (CGFloat)carousel:(__unused iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value
{
//customize carousel display
switch (option)
{
case iCarouselOptionWrap:
{
//normally you would hard-code this to YES or NO
return YES;
}
case iCarouselOptionSpacing:
{
//add a bit of spacing between the item views
//return value * 1.05;
}
case iCarouselOptionFadeMax:
{
// if (self.carousel.type == iCarouselTypeCustom)
// {
// //set opacity based on distance from camera
// return 0.0;
// }
// return value;
}
case iCarouselOptionShowBackfaces:
case iCarouselOptionRadius:
case iCarouselOptionAngle:
case iCarouselOptionArc:
case iCarouselOptionTilt:
case iCarouselOptionCount:
case iCarouselOptionFadeMin:
case iCarouselOptionFadeMinAlpha:
case iCarouselOptionFadeRange:
case iCarouselOptionOffsetMultiplier:
case iCarouselOptionVisibleItems:
{
return value;
}
}
}
#pragma mark -
#pragma mark iCarousel taps
- (void)carousel:(__unused iCarousel *)carousel didSelectItemAtIndex:(NSInteger)index
{
BannerData *data = self.bannerDatas[index];
if (![data.playlistId isEqualToString:#""]) { //Play Playlist
CategoryDetailData *detailData = [[CategoryDetailData alloc]init];
detailData.categoryId = data.playlistId;
RandomSecondVC *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"RandomSecondVC"];
vc.detailData = detailData;
NSLog(#"vc.detailData.categoryId --> %#", vc.detailData.categoryId );
vc.hidesBottomBarWhenPushed = YES; //????sanit
[self.navigationController pushViewController:vc animated:YES];
} else if (![data.url isEqualToString:#""]) { //Show webview
WebViewVC *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"WebViewVC"];
vc.hidesBottomBarWhenPushed = YES;
vc.navTitle = #"";
vc.urlString = data.url;
[self.navigationController pushViewController:vc animated:YES];
}else if (![data.videoId isEqualToString:#""]) { //Play video
VideoDetailVC *detailView = [self.storyboard instantiateViewControllerWithIdentifier:#"VideoDetailVC"];
detailView.videoId = data.videoId;
detailView.hidesBottomBarWhenPushed = YES; //????sanit
[self.navigationController pushViewController:detailView animated:YES];
}
}
Here below is how I place my Carousel view:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *tableCell = nil;
if (indexPath.section == 0) { //for banner
//++++++++++++++++++++??sanit iCarousel ver.1++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
NSLog(#"first section!!!!!!!!");
static NSString *BannerCellIdentifier = #"BannerTableViewCell"; //original
BannerTableViewCell *bannerCell = [tableView dequeueReusableCellWithIdentifier:BannerCellIdentifier]; //original
//BannerTableViewCell *bannerCell = [[BannerTableViewCell alloc]init];
[self setUpNetImages];
bannerCell.carouselView.type = iCarouselTypeCoverFlow2;
//bannerCell.carouselView.type = iCarouselTypeLinear;
//bannerCell.carouselView.autoscroll = 1; //?????sanit set autoscroll
bannerCell.carouselView.delegate = self;
bannerCell.carouselView.dataSource =self;
//??????sanit to fix banner timing issue first banner
//BannerData *data0 = self.bannerDatas[1]; //???santi
//self.cycleScrollView.autoScrollTimeInterval = data0.duration; // use this to fix timing issue of first slide
//[bannerCell.carouselView scrollToItemAtIndex:0 duration:data0.duration];
//bannerCell.carouselView.autoscroll = 0.8;
//[bannerCell.carouselView scrollToItemAtIndex:0 duration:5];
//[bannerCell.carouselView scrollToItemAtIndex:1 animated:YES];
[bannerCell.carouselView reloadData];
tableCell = bannerCell;
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++=//
} else { //for collection cell below
NSLog(#"not first section!!!!!!!! %d",(int)indexPath.section);
static NSString *MainHeaderCellIdentifier = #"MainHeaderTableViewCell";
MainHeaderTableViewCell *mainHeaderCell = (MainHeaderTableViewCell *)[tableView dequeueReusableCellWithIdentifier:MainHeaderCellIdentifier]; //original
mainHeaderCell.collectionView = (UICollectionView *)[mainHeaderCell viewWithTag:100];
mainHeaderCell.collectionView.delegate = self;
mainHeaderCell.collectionView.dataSource = self;
//NSLog(#"mainHeaderCell.index = %d",(int)mainHeaderCell.index);
//original
if (mainHeaderCell.collectionView == nil) {
NSLog(#"CollectionView Nil!!!!!!!");
}
tableCell = mainHeaderCell;
}
return tableCell;
}
Seems that iCarousel does not have that option, but, luckily, you can implement such functionality by yourself:
#property (nonatomic, strong) NSMutableDictionary *operations;
#property (nonatomic) BOOL isDragging;
- (void) carouselDidEndScrollingAnimation:(iCarousel *)carousel
{
if (!self.isDragging){
NSNumber *tag = #(carousel.tag);
[self.operations[tag] cancel];
self.operations[tag] = [NSBlockOperation blockOperationWithBlock:^{
double duration = [self durationForItemAtIndex:carousel.currentItemIndex inCarousel:carousel];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSInteger i = carousel.currentItemIndex + 1;
NSInteger next = i < carousel.numberOfItems ? i : 0;
[carousel scrollToItemAtIndex:next animated:YES];
});
}];
[(NSOperation*)(self.operations[tag]) start];
}
}
- (void) carouselWillBeginDragging:(iCarousel *)carousel
{
[self.operations[#(carousel.tag)] cancel];
self.isDragging = YES;
}
- (void) carouselDidEndDragging:(iCarousel *)carousel willDecelerate: (BOOL)decelerate
{
self.isDragging = NO;
}
- (double) durationForItemAtIndex:(NSInteger)index inCarousel:(iCarousel*)carousel
{
return <appropriate_duration_for_particular_carousel_and_item_index_in_it>;
}
- (void) startCarousel:(iCarousel*)carousel
{
[self carouselDidEndScrollingAnimation:carousel];
}
Update -[tableView:cellForRowAtIndexPath:] method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
<...>
if (indexPath.section == 0) {
<...>
bannerCell.carouselView.tag = indexPath.row;
[self startCarousel:bannerCell.carouselView];
} else {
<...>
}
return tableCell;
}
And don't forget to initialize the operations dictionary somewhere before usage:
self.operations = [NSMutableDictionary new];
However, all this is kind of draft - you may want to tune and upgrade these snippets according to your needs.

How to debug "unrecognized selector sent to instance"

I want to check if the user has an open basket if so send them to the menu if not then send send them to my open basket view but i m getting an error.
So my question is how to get rid of this problem ? and is my logic right ?
'NSInvalidArgumentException', reason: '-[Merchant merchantId]: unrecognized selector sent to instance 0x7fad19529490'.
i m using this method to check for open baskets when the button addToOrderButtonTapped is press in the menuview
[[OrderManager sharedManager]getBasketsForMerchant:merchant success:^(NSArray *baskets) {
for (Basket *basket in baskets){
if ([basket.status isEqualToString:kBasketStatusOpen]) {
[self.openBaskets addObject:basket];
}
}if (self.openBaskets.count > 0){
self.openBaskets = self.openBaskets;
dispatch_async(dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier:kSegueMenu sender:self];
});
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier:KSegueOpenBasket sender:self];
});
}
} failure:^(NSError *error, NSHTTPURLResponse *response) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *localizedString = NSLocalizedString(#"Order.ErrorLoadingOpenOrders", "Get error message");
[self showMessage:localizedString withTitle:MessageTypeError];
[MBProgressHUD hideAllHUDsForView:self.view animated:YES];
});
}];
The menuview
//
// MenuItemViewController.m
// BaseApp
//
#import "MenuItemViewController.h"
#import "RoundedButton.h"
#import "MenuOptionCell.h"
#import "MenuItem.h"
#import "MenuOptionButton.h"
#import "MenuChoice.h"
#import "OrderManager.h"
#import "TypeOfChoiceCell.h"
#import "MenuOption.h"
#import "OrderPricingTableViewCell.h"
#import "RKManagedObjectStore.h"
#import "NSManagedObjectContext+RKAdditions.h"
#import "MerchantManager.h"
#import "Merchant.h"
#import "OrdersViewController.h"
#import "MenuItem.h"
#import "MenuViewController.h"
#import "OrderConfirmationViewController.h"
#import "OrderManager.h"
#import "APIClient.h"
#import "MenuManager.h"
#import "DiscoverViewController.h"
#import "DiscoverCollectionViewCell.h"
#import "MerchantManager.h"
#import "MenuViewController.h"
#import "MenuManager.h"
#import "MerchantDetailsViewController.h"
#import "OpenOrdersViewController.h"
typedef NS_ENUM(NSUInteger, MenuOptionsSection) {
MenuOptionsSectionRequired = 0,
MenuOptionsSectionOptional = 1
};
static NSString *const OpenBasketsSegue = #"OpenBaskets";
static NSString *const kSegueMenu = #"ShowMenu";
static NSString *const KSegueOpenBasket = #"OpenBaskets";
#interface MenuItemViewController () <UITableViewDelegate, UITableViewDataSource>
#property (nonatomic, weak) IBOutlet UILabel *menuItemName;
#property (nonatomic, weak) IBOutlet UILabel *quantityLabel;
#property (nonatomic, weak) IBOutlet UILabel *menuItemPrice;
#property (nonatomic, weak) IBOutlet UILabel *subTotalLabel;
#property (nonatomic, weak) IBOutlet UITextView *menuItemDescription;
#property (nonatomic, weak) IBOutlet RoundedButton *addToOrderButton;
#property (nonatomic, weak) IBOutlet UITableView *tableView;
#property (nonatomic, weak) IBOutlet UIView *cartView;
#property (nonatomic, weak) IBOutlet UIButton *subtractButton;
#property (nonatomic) NSDecimalNumber *temporarySubtotal;
#property (nonatomic, strong) NSMutableArray<MenuOption *> *requiredChoices;
#property (nonatomic, strong) NSMutableArray<MenuOption *> *optionalChoices;
#property (nonatomic, strong) NSMutableArray<NSMutableArray *> *optionChoicesSection;
#property (nonatomic, strong) NSMutableArray<NSDictionary *> *options;
#property (nonatomic, strong) NSMutableArray <Basket *>*openBaskets;
#property (nonatomic) BOOL launchViewFirstTime;
#end
#implementation MenuItemViewController
#pragma - Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = #"Menu Item";
self.menuItemName.text = self.menuCategory.selectedBasketLine.menuItem.name;
self.menuItemPrice.text = [NSString stringWithFormat:#"%#", [Basket formattedCurrencyStringForAmount:self.menuCategory.selectedBasketLine.menuItem.price]];
self.quantityLabel.text = self.menuCategory.selectedBasketLine.quantity.stringValue;
self.temporarySubtotal = [self calculateTemporarySubtotal:[OrderManager sharedManager].currentBasket.subtotal menuItemPrice:self.menuCategory.selectedBasketLine.menuItem.price];
[self setSubtotalText:self.temporarySubtotal];
self.menuItemDescription.text = self.menuCategory.selectedBasketLine.menuItem.menuItemDescription;
self.subtractButton.alpha = 0.65;
self.requiredChoices = [[NSMutableArray alloc] init];
self.optionalChoices = [[NSMutableArray alloc] init];
self.optionChoicesSection = [[NSMutableArray alloc] init];
self.options = [[NSMutableArray alloc] init];
[self initializeChoiceArrays];
self.tableView.delegate = self;
self.tableView.dataSource = self;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.launchViewFirstTime = YES;
if (self.optionChoicesSection.count > 0) {
[self.tableView reloadData];
} else {
self.tableView.hidden = YES;
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.launchViewFirstTime = NO;
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
for (MenuOption *menuOption in self.requiredChoices) {
[menuOption resetNumberOfChoicesSelected];
}
for (MenuOption *menuOption in self.optionalChoices) {
[menuOption resetNumberOfChoicesSelected];
}
self.menuCategory = nil;
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self.menuItemDescription setContentOffset:CGPointZero animated:YES];
}
#pragma - IBActions
- (IBAction)addButtonTapped:(id)sender {
NSInteger count = self.quantityLabel.text.integerValue;
count++;
if (count > 1) {
self.subtractButton.alpha = 1;
}
NSDecimalNumber *newSubTotal = [self.temporarySubtotal decimalNumberByAdding:self.menuCategory.selectedBasketLine.menuItem.price];
[self modifyCurrentBasketSubtotal:newSubTotal quantity:count];
}
- (IBAction)subtractButtonTapped:(id)sender {
NSInteger count = self.quantityLabel.text.integerValue;
if (count > 1) {
count--;
NSDecimalNumber *newSubTotal = [self.temporarySubtotal decimalNumberBySubtracting:self.menuCategory.selectedBasketLine.menuItem.price];
[self modifyCurrentBasketSubtotal:newSubTotal quantity:count];
if (count == 1) {
self.subtractButton.alpha = 0.65;
}
}
}
- (IBAction)addToOrderButtonTapped:(id)sender {
MenuOption *menuOption;
Merchant *merchant = [[Merchant alloc]init];
// First check if there are any missing required options that have to be selected
if ((menuOption = [self checkMissingRequiredOptionsHaveBeenSelected])) {
NSString *localizedString = NSLocalizedString(#"AddMenuItem.RequiredChoicesNotSelected", #"Get string for error");
NSString *formattedString = [NSString stringWithFormat:localizedString, menuOption.name];
[self showMessage:formattedString withTitle:MessageTypeError];
return;
}
// Now check if the minimum and maximum choice seletion have been met for the menu options
if ((menuOption = [self validateMinMaxForMenuOptionsHaveBeenMet])) {
NSString *localizedString = NSLocalizedString(#"AddMenuItem.MinMaxNotFulfilled", #"Get string for error");
NSString *formattedString = [NSString stringWithFormat:localizedString, menuOption.name];
[self showMessage:formattedString withTitle:MessageTypeError];
return;
}
// Add the menu item to the basket
if (self.menuItemAddedBlock) {
self.menuItemAddedBlock(self, self.menuCategory);
// [self dismissViewControllerAnimated:YES completion:nil];
//checking for open basket here
[[OrderManager sharedManager]getBasketsForMerchant:merchant success:^(NSArray *baskets) {
for (Basket *basket in baskets){
if ([basket.status isEqualToString:kBasketStatusOpen]) {
[self.openBaskets addObject:basket];
}
}if (self.openBaskets.count > 0){
self.openBaskets = self.openBaskets;
dispatch_async(dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier:kSegueMenu sender:self];
});
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier:KSegueOpenBasket sender:self];
});
}
} failure:^(NSError *error, NSHTTPURLResponse *response) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *localizedString = NSLocalizedString(#"Order.ErrorLoadingOpenOrders", "Get error message");
[self showMessage:localizedString withTitle:MessageTypeError];
[MBProgressHUD hideAllHUDsForView:self.view animated:YES];
});
}];
}
}
- (IBAction)cancelButtonTapped:(UIBarButtonItem *)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma - UITableViewDelegate, UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.optionChoicesSection.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.optionChoicesSection[section].count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MenuOptionCell *cell = (MenuOptionCell *)[tableView dequeueReusableCellWithIdentifier:[MenuOptionCell reuseIdentifier]];
cell.menuOption = self.optionChoicesSection[indexPath.section][indexPath.row];
cell.menuOptionCellButtonPressedBlock = ^(MenuOptionButton *button, MenuOption *option, MenuChoice *choice) {
[self adjustSelectedOptions:option choice:choice indexPath:indexPath];
};
cell.menuOptionCellDefaultOptionDetectedBlock = ^(MenuOptionButton *button, MenuOption *option, MenuChoice *choice) {
[self adjustSelectedOptions:option choice:choice indexPath:indexPath];
};
cell.menuOptionCellDeselectedBlock = ^(MenuOptionButton *button, MenuOption *option, MenuChoice *choice) {
[self adjustSelectedOptions:option choice:choice indexPath:indexPath];
};
[cell configureCell:self.launchViewFirstTime];
return cell;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
TypeOfChoiceCell *cell = (TypeOfChoiceCell *)[tableView dequeueReusableCellWithIdentifier:[TypeOfChoiceCell reuseIdentifier]];
switch ((MenuOptionsSection)section) {
case MenuOptionsSectionRequired:
if (self.requiredChoices.count > 0) {
cell.title = NSLocalizedString(#"AddMenuItem.RequiredChoiceText", #"Get string for title");
return cell;
} else if (self.optionalChoices.count > 0) {
cell.title = NSLocalizedString(#"AddMenuItem.OptionalChoiceText", #"get string for title");
return cell;
}
case MenuOptionsSectionOptional:
if (self.optionalChoices.count > 0) {
cell.title = NSLocalizedString(#"AddMenuItem.OptionalChoiceText", #"Get string for title");
return cell;
}
}
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
MenuOption *option = self.optionChoicesSection[indexPath.section][indexPath.row];
return [MenuOptionCell heightForMenuOption:option];
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return [TypeOfChoiceCell height];
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
return 0.01f;
}
#pragma - Helpers
- (void)setSubtotalText:(NSDecimalNumber *)subtotal {
if (!subtotal) {
self.subTotalLabel.text = NSLocalizedString(#"AddMenuItem.SubtotalDefault", #"Get string for subtotal");
} else {
NSString *localizedString = NSLocalizedString(#"AddMenuItem.SubtotalFormatted", #"Get string for subtotal");
self.subTotalLabel.text = [NSString stringWithFormat:localizedString, [Basket formattedCurrencyStringForAmount:subtotal]];
}
}
- (void)modifyCurrentBasketSubtotal:(NSDecimalNumber *)subtotal quantity:(NSInteger)quantity {
self.menuCategory.selectedBasketLine.quantity = [NSNumber numberWithInteger:quantity];
self.menuCategory.selectedBasketLine.subtotal = subtotal;
self.temporarySubtotal = subtotal;
[self setSubtotalText:subtotal];
self.quantityLabel.text = self.menuCategory.selectedBasketLine.quantity.stringValue;
}
- (NSDecimalNumber *)calculateTemporarySubtotal:(NSDecimalNumber *)orderManagerSubTotal menuItemPrice:(NSDecimalNumber *)menuItemPrice {
if (orderManagerSubTotal == 0) {
return menuItemPrice;
} else {
return [orderManagerSubTotal decimalNumberByAdding:menuItemPrice];
}
}
- (MenuOption *)checkMissingRequiredOptionsHaveBeenSelected {
for (MenuOption *menuOption in self.requiredChoices) {
if (!menuOption.selected) {
return menuOption;
}
}
return nil;
}
- (MenuOption *)validateMinMaxForMenuOptionsHaveBeenMet {
for (MenuOption *menuOption in self.requiredChoices) {
if ([menuOption validateIndividualMinMaxForMenuOption]) {
return menuOption;
}
}
for (MenuOption *menuOption in self.optionalChoices) {
if (menuOption.selected) {
if ([menuOption validateIndividualMinMaxForMenuOption]) {
return menuOption;
}
}
}
return nil;
}
- (void)initializeChoiceArrays {
NSArray<MenuOption *> *menuOptions = [self.menuCategory.selectedBasketLine.menuItem.menuOptions allObjects];
for (MenuOption *menuOption in menuOptions) {
if (menuOption.minimumChoices == nil) {
menuOption.minimumChoices = [NSNumber numberWithInt:0];
}
if (menuOption.maximumChoices == nil) {
menuOption.maximumChoices = [NSNumber numberWithInt:0];
}
// For now make an optional choice required if minimumChoices > 0
if (menuOption.isRequired || [menuOption.minimumChoices intValue] > 0) {
menuOption.isRequired = YES;
[self.requiredChoices addObject:menuOption];
} else {
[self.optionalChoices addObject:menuOption];
}
}
if (self.requiredChoices.count > 0) {
[self.optionChoicesSection addObject:self.requiredChoices];
}
if (self.optionalChoices.count > 0) {
[self.optionChoicesSection addObject:self.optionalChoices];
}
}
- (void)adjustSelectedOptions:(MenuOption *)option choice:(MenuChoice *)choice indexPath:(NSIndexPath *)indexPath {
self.menuCategory.selectedBasketLine.subtotal = self.menuCategory.selectedBasketLine.menuItem.price;
if (option.selected && option.menuChoices.count == 0) {
[self.options addObject:#{kOption: option.menuOptionId ?: #"", kChoice: #""}];
if (option.price) {
self.temporarySubtotal = [self.temporarySubtotal decimalNumberByAdding:option.price];
}
if (option.isRequired) {
self.requiredChoices[indexPath.row].selected = option.selected;
self.requiredChoices[indexPath.row].numberOfChoicesSelected = option.numberOfChoicesSelected;
} else {
self.optionalChoices[indexPath.row].selected = option.selected;
self.optionalChoices[indexPath.row].numberOfChoicesSelected = option.numberOfChoicesSelected;
}
} else {
if (option.menuChoices.count == 0) {
[self.options removeObject:#{kOption: option.menuOptionId ?: #"", kChoice: #""}];
if (option.price) {
self.temporarySubtotal = [self.temporarySubtotal decimalNumberBySubtracting:option.price];
}
if (option.isRequired) {
self.requiredChoices[indexPath.row].selected = option.selected;
self.requiredChoices[indexPath.row].numberOfChoicesSelected = option.numberOfChoicesSelected;
} else {
self.optionalChoices[indexPath.row].selected = option.selected;
self.optionalChoices[indexPath.row].numberOfChoicesSelected = option.numberOfChoicesSelected;
}
}
}
if (choice.selected && option.menuChoices.count > 0) {
[self.options addObject:#{kOption: choice.menuOption.menuOptionId ?: #"", kChoice: choice.menuChoiceId ?: #""}];
if (choice.price) {
self.temporarySubtotal = [self.temporarySubtotal decimalNumberByAdding:choice.price];
}
if (option.isRequired) {
self.requiredChoices[indexPath.row].selected = choice.selected;
self.requiredChoices[indexPath.row].numberOfChoicesSelected = option.numberOfChoicesSelected;
} else {
self.optionalChoices[indexPath.row].selected = choice.selected;
self.optionalChoices[indexPath.row].numberOfChoicesSelected = option.numberOfChoicesSelected;
}
} else {
if (option.menuChoices.count > 0) {
[self.options removeObject:#{kOption: choice.menuOption.menuOptionId ?: #"", kChoice: choice.menuChoiceId ?: #""}];
if (choice.price) {
self.temporarySubtotal = [self.temporarySubtotal decimalNumberBySubtracting:choice.price];
}
if (option.isRequired) {
if ([option.numberOfChoicesSelected intValue] == 0) {
self.requiredChoices[indexPath.row].selected = option.selected;
} else {
self.requiredChoices[indexPath.row].selected = !choice.selected;
}
self.requiredChoices[indexPath.row].numberOfChoicesSelected = option.numberOfChoicesSelected;
} else {
self.optionalChoices[indexPath.row].selected = choice.selected;
self.optionalChoices[indexPath.row].numberOfChoicesSelected = option.numberOfChoicesSelected;
}
}
}
[self setSubtotalText:self.temporarySubtotal];
self.menuCategory.selectedBasketLine.attributes = self.options;
}
#end
This is the line of code thats giving me the error
+ (void)getBasketsForMerchant:(Merchant *)merchant success:(void (^)(NSArray *basket))success failure:(void (^)(NSError *, NSHTTPURLResponse *))failure {
NSMutableDictionary* params = #{#"expand": #"merchant"}.mutableCopy;
if (merchant) {
params[#"merchant"] = merchant.merchantId;
}
[[RKObjectManager sharedManager] getObjectsAtPath:kOrdersEndpoint parameters:params success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult){
NSArray* items = mappingResult.array;
if (success) {
success(items);
}
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
if (failure) {
failure(error, operation.HTTPRequestOperation.response);
} else {
_defaultFailureBlock(operation, error);
}
}];
}
This error:
'NSInvalidArgumentException', reason: '-[Merchant merchantId]: unrecognized selector sent to instance 0x7fad19529490'.
Means that you have an object at location 0x7fad19529490 and you tried to call "merchantId" on it. That object does not respond to merchantId.
So, look very carefully at the definition of Merchant. Did you get the spelling of merchantId right in fieldMappings? Is it merchantID with a capital D?
If you are sure that merchant has a property named merchantId exactly, then, the next likely thing is that the object at 0x7fad19529490 is not a Merchant.
The easiest thing to do is to add an exception breakpoint (use the +) at the bottom of the breakpoint navigator. When this exception happens, grab the pointer address from the error (it might be different each time) and see what it is in the debugger. To do that, at the (lldb) prompt, type:
po (NSObject*)(0x7fad19529490)
but use the address from the error. If it's a Merchant, I expect it to say something like:
<Merchant: 0x7fad19529490>
Or, if you have overridden description, it will be the output of that.

ReactiveCocoa takeUntil 2 possible signals?

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.

Calling UITableView datasource methods beforehand when the view is not present

I have dropped my code into a situation where I need to call UITableView data source methods written in some UIViewController class before a particular view is presented so that the cells get prepopulated and I can set a BOOL that the data in the not present viewController class is valid or not. I may explain it in more detail if required, but I wanted to know if its possible to do that. If yes, then how to do it? .. as a particular set of my code written after [tableView reloadData] is dependent on running the dataSource methods of UITableView. Please throw some light on this, if needs to be handled in a specific thread?
Following is the case where I call reloadData. Note: This is happening in another class when basicFactsViewController's viewWillAppear method has not been called yet:
- (BOOL) isComplete {
dispatch_async(dispatch_get_main_queue(), ^{
[basicFactsViewController.tableView reloadData];
});
return basicFactsViewController.isComplete && selectedVehicleId && selectedMakeId && selectedModelId && selectedYearId && selectedTrimId;
}
Now basicFactsViewController.isComplete is checked in this method:
- (BOOL) isComplete {
[self collectKeyHighlights];
return _isComplete;
}
Now the dictionary "tableCells" in the method below uses the cells population to check whether all features have been completed or not:
- (NSDictionary *) collectKeyHighlights {
NSMutableDictionary *key_highlights_update = [NSMutableDictionary new];
NSMutableDictionary *cell_highlight_update = [NSMutableDictionary new];
if(visible_key_highlights.count == 0) _isComplete = YES;
_isComplete = YES;
__block NSMutableArray *reloadCellAtIndexPathSet = [[NSMutableArray alloc] init];
[visible_key_highlights enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSDictionary *feature = (NSDictionary *)obj;
UITableViewCell *cell = [self.tableCells objectForKey:[NSIndexPath indexPathForRow:idx inSection:0]];
if(cell) {
if([cell isKindOfClass:[DRColorSelectionTableViewCell class]]) {
NSInteger selectedIndex = ((DRColorSelectionTableViewCell *)cell).selectedIndex;
NSInteger numberOfSegments = ((DRColorSelectionTableViewCell *)cell).numberOfSegments;
if(selectedIndex > -1 ) {
NSArray *dataValues = [[visible_key_highlights objectAtIndex:idx] objectForKey:#"data_values"];
NSDictionary *colorData;
BOOL reloadCellForIndexPath = NO;
if (numberOfSegments == selectedIndex) {
colorData = #{ #"normalized" : #"user_defined", #"isother" : #YES, #"hexcode":#"#FFFFFF", #"actual":((DRColorSelectionTableViewCell *)cell).otherColorTextField.text};
reloadCellForIndexPath = YES;
}
else{
colorData = [dataValues objectAtIndex:selectedIndex];
}
[key_highlights_update setObject:colorData forKey:[feature objectForKey:#"name"]];
[cell_highlight_update setObject:colorData forKey:[feature objectForKey:#"name"]];
if (![colorData isEqual:[prevSelections objectForKey:[feature objectForKey:#"name"]]]) {
[reloadCellAtIndexPathSet addObject:((DRColorSelectionTableViewCell *)cell).indexPath];
}
//if (reloadCellForIndexPath) {
//}
} else {
_isComplete = NO;
}
} else if([cell isKindOfClass:[DRInputTableViewCell class]]) {
NSString *textInput = ((DRInputTableViewCell *)cell).inputTextField.text;
if([textInput length]) {
[key_highlights_update setObject:[NSString toSnakeCase:textInput] forKey:[feature objectForKey:#"name"]];
[cell_highlight_update setObject:textInput forKey:[feature objectForKey:#"name"]];
}else {
_isComplete = NO;
}
} else if([cell isKindOfClass:[DRPickerTableViewCell class]]) {
NSString *textInput = ((DRPickerTableViewCell *)cell).inputField.text;
if([textInput length]) {
[key_highlights_update setObject:[NSString toSnakeCase:textInput] forKey:[feature objectForKey:#"name"]];
[cell_highlight_update setObject:textInput forKey:[feature objectForKey:#"name"]];
} else {
_isComplete = NO;
}
} else if([cell isKindOfClass:[DRSwitchTableViewCell class]]) {
// send this everytime for now
BOOL isSelected = ((DRSwitchTableViewCell *)cell).toggleButton.selected;
[key_highlights_update setObject:[NSNumber numberWithBool:isSelected] forKey:[feature objectForKey:#"name"]];
[cell_highlight_update setObject:[NSNumber numberWithBool:isSelected] forKey:[feature objectForKey:#"name"]];
}
}
else{
_isComplete = NO;
}
}];
prevSelections = cell_highlight_update;
if ([reloadCellAtIndexPathSet count]) {
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:reloadCellAtIndexPathSet withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
}
return key_highlights_update;
}
Now here since
[tableView reloadData]
is not calling cellForRowAtIndePath:, hence, tableCells is not getting populated, hence, I am always getting _isComplete = NO.
If I understand correctly, there is processing being done when the tableview loads (calls it's dataSource methods) and you want to trigger that early to use its results. Calling [basicFactsViewController.tableView reloadData]; early won't work if the basicFactsViewController hasn't been displayed yet. If basicFactsViewController is a UIViewController and has the default view and the tableView property is a subview of that standard view, then (if I remember correctly) the tableView property will be nil until the basicFactsViewController has been displayed. A shortcut around that is to access the viewController's view property and cause it to initialize (viewDidLoad and all that). You can do that by simply messaging the viewController: [basicFactsViewController view].
If I've been right so far I'm fairly confident that will initialize the tableView property. But I'm not sure if it will cause the table view to load its data. And even if it does work, it's definitely not the best solution to the piece of code you're trying to architect. Apple's design for UIKit has been focused on the model/view/controller pattern and it's easier to go with the flow and do the same. I imagine that you could move the processing that is in the data source methods for the tableView out into another class (or maybe even the same class), and call that method to get everything ready for both the tableView and any other checks that you have, storing the data in dictionaries and arrays in such a way that you can easily load them by index into the tableView when cellForIndex is called.

Resources