I'm making a UIViewController to manage a messaging screen. I'm doing this using a UITableView and some custom cells.
To make things simpler, each cell contains:
Its chat "bubble" (a UIView subclass)
Its chat text (a UILabel)
A timestamp header label (which might be hidden)
A bottom footer label (for "Sending...", "Delivered", etc.; also might be hidden)
Because of performance concerns, I am not using auto-sizing of cell heights, but caching cell heights into an NSMutableDictionary.
When the user sends or receives a new message, I want the following to occur:
The current last message cell is reloaded, hiding its bottom label, if needed.
The new last message cell is appended at the bottom of the UITableView.
The UITableView is scrolled so that the new last cell is visible.
I can get it to where the end state of the screen is as desired, but the animations in between are really kinda funky. I have tried a whole lot of different approaches to get the animations to behave. Basically, it seems like some major reloading is happening, even though the only cell that could possibly change its height is the last cell (prior to the insertion of the new cell). Plus, I'd like to have the last cell simply "appear" in place without animation. If it does, it should be off-screen, and then I should be able to animate it on-screen.
Here's my current "user sent a new message" method:
- (IBAction)sendButtonPressed {
//Creation of the new message, into 'message' variable
[self.messages addObject:message];
int thisIndex = (int)self.messages.count - 1;
NSIndexPath *this = [NSIndexPath indexPathForRow:thisIndex inSection:0];
int prevIndex = (int)self.messages.count - 2;
NSIndexPath *prev = [NSIndexPath indexPathForRow:prevIndex inSection:0];
[self removeCachedHeightForIndex:prevIndex];
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[prev] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView insertRowsAtIndexPaths:#[this] withRowAnimation:UITableViewRowAnimationTop];
[CATransaction setCompletionBlock:^{
[self scrollToBottomAnimated:YES];
}];
[self.tableView endUpdates];
}
This appears to reload the last several cells in the UITableView. Or, at least, every cell that is visible when this is called seems to be animated in some way. Only the cell at prev is actually changing in any way.
Longterm, I might pull out the header and footer labels into different cells, but is there a way to fix this animation glitch as-is?
Related
Here is my program. I want to create a simple list of items that display a number. When the rows are tapped the number will increment by one.
EDIT: Is it proper to change the UI of a row in the didSelectRowAtIndexPath function?
I created a UIViewController in Xcode 5 through a storyboard and it does everything right except I can't seem to stop the [tableView reloadData] from deselecting my row after being tapped. Specifically, I want the row to turn gray and then fade out normally.
I have tried selecting the row and then deselecting the row programatically after calling [tableView reloadData], but it doesn't work.
I know that if I was using UITableViewController that I could just call [self setClearsSelectionOnViewWillAppear:NO], but I'm not.
Is there a similar property I can set for UIViewController?
Here is the code:
[tableView beginUpdates];
[counts replaceObjectAtIndex: row withObject: [NSNumber numberWithInt:newCount]];
[tableView reloadData];
[tableView endUpdates];
I feel I may not be describing what is going on. I have a row that uses UITableViewCellStyle2, which displays a label to the left and right. On the right aligned text is a number that increments each time the row is tapped. Simply updating the data structure does not solve the problem. I need to update it visually. I don't need or want to replace the row, unless I have too. I just want to update the right-aligned text field AND keep the row from being deselected immediately without animation. I can do one or the other, but not both.
Is there a way to just update the right-aligned text field while still staying true to the MVC model?
Remove the [tableView reloadData]; from the code. It should not be called in the methods that insert or delete rows, especially within an animation block implemented with calls to beginUpdates and endUpdates .
Call reloadData method to reload all the data that is used to construct the table, including cells, section headers and footers, index arrays, and so on. For efficiency, the table view redisplays only those rows that are visible. It adjusts offsets if the table shrinks as a result of the reload. The table view's delegate or data source calls this method when it wants the table view to completely reload its data.
[tableView beginUpdates];
[counts replaceObjectAtIndex: row withObject: [NSNumber numberWithInt:newCount]];
[tableView endUpdates];
See the developer.apple section - reloadData
If you want to keep the selection after reload, the easy way is
NSIndexPath *selectedRowIndexPath = [tableView indexPathForSelectedRow];
[tableView reloadData];
[tableView selectRowAtIndexPath:selectedRowIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
I wish to select all rows in UITableView for which I am using a for loop as per below code:
- (void)selectAllRows
{
for (int row = 0; row < [self.tableView numberOfRowsInSection:1]; row ++)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:1];
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
}
}
I couldn't find any direct delegates on NSIndexPath class or on UITableView controller to select all the rows on UITableView cell. This is of 'n' complexity, but could there be a better way to select all the cells?
This is the only way to select all the cells. The real question is why are you wanting to select all the cells? Are you selecting them to get UITableView delegate callbacks? Are you selecting them for UI purposes?
You can't reference all cells in your tableView as the tableView doesn't "have" all of them as it uses reuse/dequeueing.
If by "UI purposes" you mean solely for the visual effect as opposed to, you could loop
through all visible cells in your tableview and set the alpha to 0.5 or something similar, and if you want to fade them out and in as if they were selected/deselected you could do some [UIView animate....] calls on all of them. If you were to do something like this, you might want to consider disabling scrolling for the short period of time your animation is occurring, because if you are doing animation on visible cells as soon as you scroll any of the new cells wouldn't have that animation. Once you're done animating, in the completion block, you can then re-enable scrolling.
Trying to be ios7-esque, I am inserting a UIPickerView into a UITableView when tapping on a cell in the table. This works fine and animates nicely. However, there is an issue when I retract the cell by calling deleteRowsAtIndexPaths.
I am experiencing a "bleed"/overlap where the picker is hiding one of the cells further down in the table view. See the screenshots.
I'm not doing anything super custom, so I wonder if this is an iOS7 bug. All cells have solid background colors (white).
Any help would be greatly appreciated. Thanks!
Tapping the top row
This is mid animation when retracting. Notice the overlap and the picker bleeding out over the cell at the bottom
I'm not sure why, but it looks to me like the picker cell is covering the cell below "Choose Product". If this is indeed the case, one workaround would be to explicitly set the z-order of your cells, placing the picker cell under all others:
#import <QuartzCore/QuartzCore.h>
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = ...;// your logic for getting a cell
BOOL isPickerCell = ...;// your logic for identifying if this is the picker cell
cell.layer.zPosition = isPickerCell ? 0 : 1;
}
If the picker is near the bottom of the table, it could still show through below the last cell since there's nothing there to cover it. For example, if "Choose Product" were the last cell. You can work around this by inserting blank cell(s) at the bottom. This is a general problem with having cells of varying height.
After struggling with this problem, I realised that Apple's calendar application has the same issue
However, they minimise the side effects by inserting the row with a .fade animation.
tableView.insertRows(at: indexes, with: .fade)
I had similar issues (iOS 8, iPhone 6 simulator)
In my case, I had a custom cell containing a DatePicker being inserted/deleted either between Right Detail style cells or between a Right Detail style cell and the section footer, which worked as expected.
[self.table beginUpdates];
if (isVisible) {
[self.table insertRowsAtIndexPaths:#[index]
withRowAnimation:UITableViewRowAnimationMiddle];
} else {
[self.table deleteRowsAtIndexPaths:#[index]
withRowAnimation:UITableViewRowAnimationMiddle];
}
[self.table endUpdates];
But I also had Right Detail style cell being inserted/deleted between a Right Detail style cell and the end of section, which did not work as expected using the same code. The appearing/disappearing cell was visible on top of/through the cell above, and the cell moved twice as far as it should have. In the image below, People is appearing below Privacy, mid-animation.
However, I noticed that when the beginUpdates/endUpdates were commented out, the cell only moved about half a cell height instead of twice a cell height which meant that it looked much improved.
I also tried setting the zPosition which appeared to lessen the visibility when the cells overlapped.
// [self.table beginUpdates];
if (isVisible) {
[self.table insertRowsAtIndexPaths:#[index]
withRowAnimation:UITableViewRowAnimationMiddle];
} else {
cell = [self.table cellForRowAtIndexPath:peopleIndex];
cell.layer.zPosition = -1;
[self.table deleteRowsAtIndexPaths:#[peopleIndex]
withRowAnimation:UITableViewRowAnimationMiddle];
}
// [self.table endUpdates];
I've got a text field inside of a UICollectionViewCell that can receive first-responder status. The cell currently isn't visible on-screen, and I want to scroll to the cell based off of a button hit from a UISegmentedControl. There's two segments to this control… and a hit to the second segment should scroll to the first cell in the 2nd section of the UICollectionView. After this happens, the cell should get selected programatically, and then the text field inside of that cell is supposed to get first responder status and bring up the keyboard.
What's happening now (inside my action method from a value change from the segmented control) is that a call to -[UICollectionView selectItemAtIndexPath:animated:scrollPosition:] isn't scrolling to it at all (and I'm using UICollectionViewScrollPositionTop; may as well be "…None"). If I thumb down the list manually, the cell is indeed selected (it gets a darker background color in that state), but the text field certainly doesn't have first responder status.
To fix the scroll problem, I've been able to ascertain the position of the cell in the list, and scroll to the cell's content offset (I've also used scrollRectToVisible here). Then I manually select it (as well as telling the delegate to fire its appropriate method as well, where the cell's text field gains first responder status).
- (void)directionSegmentedControlChanged:(UISegmentedControl *)sender {
NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:sender.selectedSegmentIndex];
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:path];
[self.collectionView setContentOffset:attributes.frame.origin animated:YES];
[self.collectionView selectItemAtIndexPath:path animated:NO scrollPosition:UICollectionViewScrollPositionNone];
[self.collectionView.delegate collectionView:self.collectionView didSelectItemAtIndexPath:path];
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
BDKCollectionViewCell *cell = (BDKCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
[cell.textField becomeFirstResponder];
}
The problem here is that the cell as it's seen in -[collectionView:didSelectItemAtIndexPath:] is nil, because it's not in the visible cell set of the collection view when the method gets fired.
What's the best way to solve this? I've tried tossing my scrolling code inside of a [UIView animateWithDuration:animations:completion:] block, and assigned first responder upon completion there, but manually animating the collection view in this manner neglects to load any of the cells that should be scrolled past. Any ideas?
Update: many thanks to #Esker, who suggested I simply perform the "focus selection" action after a delay using Grand Central Dispatch. My solution ended up looking like this.
- (void)directionSegmentedControlChanged:(UISegmentedControl *)sender {
NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:sender.selectedSegmentIndex];
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:path];
[self.collectionView setContentOffset:attributes.frame.origin animated:YES];
dispatch_time_t startAfter = dispatch_time(DISPATCH_TIME_NOW, 0.28 * NSEC_PER_SEC);
dispatch_after(startAfter, dispatch_get_main_queue(), ^{
[self.collectionView selectItemAtIndexPath:path animated:NO scrollPosition:UICollectionViewScrollPositionNone];
[self collectionView:self.collectionView didSelectItemAtIndexPath:path];
});
}
I had a similar challenge with a UITableView: scrolling to a cell that was not yet visible, and assigning first responder to a UITextField within the target cell once it was visible. Here's a simplified description of how I handle this. I imagine this approach could work with a UICollectionView, but I don't have much experience with collection views.
If the desired cell/text field is currently visible, immediately send it becomeFirstResponder, and scroll to the cell if desired.
Otherwise, set a property in your view controller or a similar class that indicates that a text field needs focus, and which one needs focus
Tell the table view/collection view to scroll to the desired index path
In collectionView:cellForItemAtIndexPath:, you could try to check that property to see if a text field at the given indexPath needs to get focus, and if so, send it becomeFirstResponder immediately, but I found this won't work if the cell is scrolling into view, presumably because at this point, when you're configuring the new cell, it's not yet actually in the view hierarchy. So I added a check, if becomeFirstResponder returns NO at this point, I try again after a delay:
dispatch_after(someDelay, dispatch_get_main_queue(), ^(void){
[self getFocus:textField];
});
The getFocus method will both send becomeFirstResponder to the text field and clear that property that tracks which text field needs focus.
My actual implementation is somewhat specialized for the view model associated with my table view, and encapsulated in a couple of classes and using some KVO, but I wanted to avoid that and focus on the minimum required logic in the description above.
I have a tableview with custom cells with dynamic cell heights depending on the cell content.
My problem is the following, when I ask, programmatically, in the viewDidLoad, to scroll to a given position it works, except for the last row. Sometime the row appears but not fully, and sometimes it even does not appear. In both cases I have to scroll manually to see the row.
Here is the code :
[self.tableView reloadData];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:aRow inSection:aSection];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES ];
Is this a bug of iOS ? any workaround ?
As so far i came to know that,
All the operations used to before the view is shown on the screen are initialized in the viewDidLoad all the UI objects, data objects can be allocated and initialized in this method.
All the operations data modifications, UI modifications made to view need to be done in viewDidAppear. Or even some operations can be done in viewWillAppear.
So for your issue, the UITableView scrolling must be done after the table is loaded on & shown on screen i.e., in viewDidAppear.
Also note that viewDidAppear & viewWillAppear will be called each time view is shown to user, so if you want to scroll the table only for the first instance you can have a flag in your header indicating the instance.
[self.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
NSIndexPath *rowIndexPath = [NSIndexPath indexPathForRow:3 inSection:0];
[self.tableView scrollToRowAtIndexPath:rowIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
});
I don't know exactly why, but I guess this approach works because when we add(???) rows and call [tableView reloadData] tableView has no time to update some internal counters (like row counter) and calling [tableView scrollToRowAtIndexPath ...] has no effect since there is no such row at that time (again, probably correct in case you add rows or set tableView's data for the first time). Calling
dispatch_async(dispatch_get_main_queue(), ^{
...
});
after [tableView reloadData] gives tableView enough time to update row counter and perform scroll to existing row.
Vishy's approach works just because it gives enough time but applicable only if you need to scroll exactly one time when screen is loaded. Moreover it requires ugly flag to check every time viewDid/WillAppear.