Our app has a tableview that allows users to minimize/maximize its sections. There are also animations tied to hiding/showing rows depending on the user's input, as well as being connected to different devices (via Bluetooth 4.0). Because of the different sources of animations, we set up a scheduler to handle one animation at a time so that we didn't get any crashes tied to the number of rows/sections being out of sync after a [tableview endUpdates]. It's been working really well for our purposes.
However, a new section in our table has been giving me a bit of grief. Depending on the device that is connected to the iPad, the section either has 1 row or it has 8-9 rows. The rest of the rows have a height of 0. The section minimizes/maximizes just fine when it has 8-9 rows. When the section only has one row though, the insertRowsAtIndexPaths animation for the table does not complete until the section's row is off the screen. So unless the user changes tabs or scrolls down far enough for the table to push it off the screen, no other section can be minimized/maximized due to our scheduler checking first if it's already busy. Here is the code that gets called for the animations:
-(void)animateTableUsingAnimationType:(NSNumber*)aniType
{
static int animationID = -1;
if(tableAnimationBusy)
{
[self performSelector:#selector(animateTableUsingAnimationType:) withObject:aniType afterDelay:.05f];
}
else
{
tableAnimationBusy = true;
tat = [aniType intValue];
[CATransaction begin];
[CATransaction setCompletionBlock:^{
NSLog(#"Animation %d is complete!", tat);
tableAnimationBusy = false;
}];
//if-else if blocks checking for the animationID
//if open section 2
[self animateOpenTableSection:2]
else //undefined ID
tableAnimationBusy = false;
[CATransaction commit];
[self setUpLabelText];
}
}
and the animateOpenTableSection is:
-(void)animateOpenTableSection:(NSInteger)section
{
NSLog(#"Calling open on section: %d", section);
if (section == 2)
{
[self.tableView beginUpdates];
openFlagSettingsRowCount = fsr_COUNT; //max row count for section
[[MCUISectionHandler sharedInstance] sectionTouched:YES forSection:MCUISH_SECTION_NAME__FLAG];
NSMutableArray *indexPathsToInsert = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < fsr_COUNT; i++) {
[indexPathsToInsert addObject:[NSIndexPath indexPathForRow:i inSection:section]];
}
[self.tableView insertRowsAtIndexPaths:indexPathsToInsert withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
}
}
As you can see, I have debug statements in the animateOpenTableSection function, as well as the completion block in animateTableUsingAnimationType. The latter never gets called until the single row is off the screen. Any ideas as to why the tableview wouldn't let go of the CATransaction?
We were able to solve this problem and I forgot to post what I found out about our app. The reason the tableview wouldn't let go of the CATransaction was because of UIActivityIndicator subviews in the rows. They were being animated and so the CATransaction never ended because it was waiting for one or more UIActivityIndicators to stop animating.
So for those who may run into this problem, check to see if any subviews are being animated.
Related
I am using the new prefetchDataSource of UITableView (also can be applied to UICollectionView. The problem that I am having is the following:
I am implementing func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) which works pretty fine.
I am using an array which starts with 15 elements. So in numberOfRowsInSection will initially return array.count which is 15.
What I want is for the prefetchRowsAt function to tell me when we're runing out of the rows, so that I fetch new data from my own source, append these data to the array and then reloadData(), which will in turn increase the numberOfRowsInSection.
Problem is that prefetchRowsAt won't go beyond row 15 because it is limited by the numberOfRowsInSection that I specified. So basically, I get stuck. Basically, when I am at row 5, I want it to tell me to start prefetching row 16.
One solution that I tried and worked would be to increase the count of numberOfRowsInSection to say 100, and then put placeholder cells while I am fetching the new data. But the scrolling bar will look too small as it is expecting that there is 100 items, so the UX won't be that nice.
Maybe the pattern that I use in step 3 is wrong (append to array and reloadData)? Or Can you think of another cleaner solution to the problem?
Thanks in advance!
I think I has understood your meaning: Your class have an mutableArray to store your Cell's data model to shown. Everytime, the count of resource request back is 15.
at first time: the count of data got is 15, you want to pre-making the request of next resources from server when user scroll to 5th indexpath cell .
After get reqeust, your mutableArray's count is 30
at second time: you want to pre-get next resources from server when user scroll to 25th indexpath cell .
After get reqeust, your mutableArray's count is 45
forever making request for next time resouces ,as long as user slides the screen
OK, It's not necessary that using prefetchDataSource(it is only usable iOS 10), rather than you can using dataSource. .
Of course,if you only run your app in iOS 10 , you can making net-request for meta data(like pre-caching images) in Cell by implement prefetch method
. If not, you can use cellForRowAtIndexPath(tableView) or cellForItemAtIndexPath(collectionView)
For UITableView
tableView:cellForRowAtIndexPath:
For UICollectionView
collectionView:cellForItemAtIndexPath:
Futher, I'm not suggest you using scrollViewDidScroll,
Becuase the height of the cell usually varies with the change of data
,if it will be more expensive.
your code can like this code below:(UICollectionView)
#interface YourCollectionView()
#property (nonatomic, strong) SDWebImagePrefetcher *imagePrefetcher;
#end
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
// Check current indexPath
NSInteger distance = self.array.count - indexPath.row-1;
if (distance <= 10) {
[self loadingMoreData];
}
// Preload the ten image into the cache, if you need your app to be compatible with iOS10 below.
NSMutableArray <NSURL*> *URLsArray = [[NSMutableArray alloc] init];
for (NSInteger i = 1; i <= 10; i++) {
NSInteger y = currentLastIndexRow + i;
if (y > self.array.count-1) {
break;
}
YourModel *model = self.array[indexPath.item];
ImageModel *image = model.image;
[URLsArray addObject:[NSURL URLWithString:image.httpsURL]];
}
[self.imagePrefetcher prefetchURLs:URLsArray];
YourCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
// setting your Cell
[cell setNeedsLayout];
return cell;
}
If you do not need your app to be compatible with iOS10 below,you can put prelaod code to prefetch method.
BTW: If you think my answer working, please do not hesitate to adopt it : )
#Updates: About how to reload: You can monitor array by using KVO,
like:
[self addObserver:self forKeyPath:#"array" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
and implement this method
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualToString:#"array"])
{
if(!change) return;
id oldC = [change objectForKey:NSKeyValueChangeOldKey];
id newC = [change objectForKey:NSKeyValueChangeNewKey];
UICollectionView *cv = selfWeak.collectionView;
// changes is non-nil, so we just need to update
[cv performBatchUpdates:^{
// delete reload insert..
} completion:^(BOOL finished) {
// do somethind , like scrollToItemAtIndexPath
}];
}
}
I think you should start loading your new data asynchronously and just insert the new rows: func insertRows(at:with:) instead of reloadData (you are inserting data, not reloading it).
You can start loading the new data as soon as the prefetch method hits the last row (or before). Once it's loaded then you can call beginUpdates(), update your array, insert the new rows and finally endUpdates().
See more in the docs: Batch Insertion, Deletion, and Reloading of Rows and Sections
Well, working of PrefetchDelegate is bit different.
You need to tell the tableview that total how many rows are there in your system.
so basically you need to pass the total count of object table view is going to display in cellForRowAtIndexPath.
Once this is done, prefetchRowsAt will start calling and let you know which row it's going to display and there you should apply your logic of API call.
Let me know if you need more clarification.
I have the expandable UITableView subclass. When a user clicks on a header then the table view removes all rows and only shows the section headers. If the user clicks again - all rows will be shown. The problem is: when the rows and sections counts become too large - table view is freezing and crash the user experience. I've tried to add each section in a different thread but still failed. The next code I've made is crashing when inserting rows with
he number of rows contained in an existing section after the update (23) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
But I never reload my table view don't change the counts in data source methods.
The code for expanding
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int sec = 0; sec < [self.myCustomTableViewDataSource numberOfSectionsInTableView:self]; sec++) {
// NSString *threadNumber = [NSString stringWithFormat:#"inserting_thread_%tu", sec];
// dispatch_queue_t customQueue = dispatch_queue_create([threadNumber UTF8String], 0);
//
// dispatch_sync(customQueue, ^{
//
// });
[self.contractedSections removeObject:[NSNumber numberWithInteger:sec]];
NSMutableArray *indexPaths = [NSMutableArray new];
for (int i = 0; i < [self.myCustomTableViewDataSource resizableTableView:self numberOfRowsInSection:sec]; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:sec]];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
});
}
});
I can't seem to find any solution to following bug. I'm using collection view in usual UIViewController, and using new ios9 feature for interactive reordering. Also I define where i can move picked cell using targetIndexPathForMoveFromItemAtIndexPath:.
The bug is when the targetIndex is not visible in collectionView, moving cell leaves duplicate of itself in place, where touch ended.
Please see bug scrren shot here. The gray cell are not allowed to be placed between pink cells and it should be placed after last gray cell which is not visible cause it's in the beginning of view.
I also use wiggle animation for reordering taken from this article.
https://littlebitesofcocoa.com/104-interactive-collection-view-re-ordering
If you need code where i define my gesture recognizer, here it is
if (gesture.state==UIGestureRecognizerStateBegan){
NSIndexPath *selectedIndexPath = [self.collectionView indexPathForItemAtPoint:[gesture locationInView:self.collectionView]];
if(!selectedIndexPath)
return;
movingIndexPath=selectedIndexPath;
[self setEditing:YES];
[self.collectionView beginInteractiveMovementForItemAtIndexPath:selectedIndexPath];
TLTimelineCollectionCell *pickedCell=[self pickedUpCell];
movingCollectionCell=pickedCell;
[pickedCell stopWiggling];
[self animatePickingUpCell:pickedCell];
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:selectedIndexPath];
CGRect cellRect = attributes.frame;
CGRect rect=[self.collectionView convertRect:cellRect toView:self.collectionView];
} else if (gesture.state==UIGestureRecognizerStateChanged) {
[self.collectionView updateInteractiveMovementTargetPosition:CGPointMake([gesture locationInView:gesture.view].x,39)];
} else {
if (gesture.state==UIGestureRecognizerStateEnded) {
[self.collectionView endInteractiveMovement];
} else {
[self.collectionView cancelInteractiveMovement];
}
[self animatePuttingDownCell:movingCollectionCell];
movingIndexPath=nil;
movingCollectionCell=nil;
[self setEditing:NO];
}
Appreciate any help, thank you!
UPDATE
Found out that this bug also happens without any "wiggle animations" in clear project with just one collection view controller
Turned out that it's iOS 9 bug. On iOS 10 works fine.
In my application,I am performing reordering of cells but I am concatenating selected cells with each other
I have some cells and I am doing some cell animation i.e. user can select any cell and drag to any cell present in my table and after dragging these two cells are replaced by one cell that is created with these two cells.
But my problem is when I am trying to replace the last cell, it is not allowing me to do and the cell which I am trying to drag is added at the bottom without replacing the last cell and it is only happening for last cell
Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (50) must be equal to the number of rows contained in that section before the update (51), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
2013-06-03 15:16:33.636 QuestionPro[6565:1d003] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (50) must be equal to the number of rows contained in that section before the update (51), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
Here is my code
- (void)endedDragGestureWithTranslationPoint:(CGPoint)translation {
[self updateFrameOfDraggedCellForTranlationPoint:translation];
self.draggedCell.layer.shouldRasterize = NO;
[(UITableViewCell *)self.draggedCell setHighlighted:NO animated:YES];
UITableViewCell *oldDraggedCell = [self.draggedCell retain];
NSIndexPath *blankIndexPath = [self.indexPathBelowDraggedCell retain];
CGRect blankCellFrame = oldDraggedCell.frame;
CGPoint blankCellCenter = {
.x = CGRectGetMidX(blankCellFrame),
.y = CGRectGetMidY(blankCellFrame)
};
NSIndexPath *rowToMoveTo = [tableView indexPathForRowAtPoint:blankCellCenter];
if ( rowToMoveTo ) {
CGRect rectForIndexPath = [tableView rectForRowAtIndexPath:rowToMoveTo];
if (dragGesture.state == UIGestureRecognizerStateFailed ){
rectForIndexPath = [tableView rectForRowAtIndexPath:self.indexPathBelowDraggedCell];
}
NSIndexPath * secIndex = [blankIndexPath copy];
[UIView animateWithDuration:0.25 delay:0 options:(UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState) animations:^{
oldDraggedCell.frame = rectForIndexPath;
[self dragTableViewController:self hideDraggableIndicatorsOfCell:oldDraggedCell];
} completion:^(BOOL finished) {
[self dragTableViewController:self removeDraggableIndicatorsFromCell:oldDraggedCell];
[oldDraggedCell removeFromSuperview];
[oldDraggedCell release];
}];
if (dragGesture.state == UIGestureRecognizerStateEnded && secIndex.row != rowToMoveTo.row ){
[tableView beginUpdates];
[self joinTableViewCellsAtFirstRow:rowToMoveTo.row andSecondRow:secIndex.row];
NSArray * delArray = [[NSArray alloc] initWithObjects:secIndex , nil];
NSArray * rfrArray = [[NSArray alloc] initWithObjects:secIndex , nil];
[tableView deleteRowsAtIndexPaths:delArray withRowAnimation:UITableViewRowAnimationNone];
SALTableCellView * toggledCell = (SALTableCellView *)[tableView cellForRowAtIndexPath:rowToMoveTo];
if ( toggledCell.selectedImage && activateDelegate ){
[activateDelegate spotMarkedWithTitle:toggledCell.title andValue:toggledCell.shownValue andColor:[UIColor clearColor]];
}
[delArray release];
[rfrArray release];
[tableView endUpdates];
[self recalculateTapSumm];
[self refreshData];
}
else
hiddenCell.hidden = NO;
[secIndex release];
[tableView scrollRectToVisible:rectForIndexPath animated:YES];
}
[blankIndexPath release];
[oldDraggedCell release];
}
Well, there is a short answer and a long answer.
Short answer: You can't do what you are doing and i am surprised it works for the other rows.
Long answer: what you are trying to do goes against what a user would "normally" expect to happen. ie. if you "move" a row from one place to another, it would "move" and not combine. As the others have pointed out, the error message says as much. That the number of rows are changing from 51 to 50, as you are deleting the rows. the numbers must be the same when moving rows around.
solution: the solution could be quite simple, although i havent tried it myself. try firing all this delete and update tasks with a slight delay or something. that way, the moving rows would do its thingy and by which time the update task would be ready to remove the row and update the data so that you can show the consolidated row in the destination.
Check number of rows before and after you replace the cell.
The error showing you itself says that number of rows before replacing are not equal to the number of rows after replacement.
I'm having problems adding subviews to a scrollview sequentially.
I've got a JSON response coming back from the server which I parse into an array of Business objects, and I send off to the function updateCarousel, which looks like this:
-(void) updateCarousel: (NSArray *)response{
if(response && response.count>0){
int i=0;
self.scrollView.hidden=NO;
[self.scrollView setNeedsDisplay];
self.pageControl.hidden=NO;
[self.scrollView setContentOffset:CGPointMake(0, 0) animated:NO];
for (Business *business in response){
if (i >= MAX_INITAL_SEARCH_RESULTS)
break;
CGRect frame;
frame.origin.x = self.scrollView.frame.size.width * i;
frame.origin.y = 0;
frame.size = scrollView.frame.size;
CardView *cardView = [[CardView alloc] initWithBusinessData:business andFrame:frame];
//I've tried the following code with and without wrapping it in a GCD queue
dispatch_queue_t addingQueue = dispatch_queue_create("adding subview queue", NULL);
dispatch_async(addingQueue, ^{
[self.scrollView addSubview:cardView];
});
dispatch_release(addingQueue);
cardView.backgroundColor = [UIColor colorWithWhite:1 alpha:0];
i++;
self.scrollView.contentSize = CGSizeMake(i*(self.scrollView.frame.size.width), self.scrollView.frame.size.height);
self.pageControl.numberOfPages=i;
}
}else{
self.scrollView.hidden=YES;
self.pageControl.hidden=YES;
NSLog(#"call to api returned a result set of size 0");
}
The result - despite the many things I've tried - is always the same: the scrollView adds the subviews all at once, not as they are processed through the loop. I don't understand how this is possible. If I add a sleep() at the end of the loop, it somehow waits for the whole loop to be over before it shows the subviews as added. How does it even know how long the results array is? I'm at my wits' end, please help.
I assume you are not using any extra threads to process the data.
What you experience is the application is stuck executing your method. Even though you add your subviews one by one (with a sleep between them), no other piece of code is executed to handle your adding.
1. You can use another thread to load the data and add subviews but this needs to be synchronized to the main thread (more complicated).
2 You can break your method in multiple calls. Between 2 calls of your load method, other pieces of code are allowed to execute and this means the scrollview will be able to process/display your subviews one by one.
You will need to change your loading method to something like this:
- (void)updateCarouselStep:(NSNumber*)loadIndex
{
if (response && response.count > 0)
{
// Here add only a subview corresponding to loadIndex
// Here we schedule another call of this function if there is anything
if (loadIndex < response.count - 1)
{
[self performSelector:#selector(updateCarouselStep:) withObject:[NSNumber numberWithInt:(loadIndex+1) afterDelay:0.5f];
}
}
}
This is just one basic solution to the problem. For example, you need to take into consideration what happens if you update response data before you finish loading the previous one.