I've implemented Cocoa with Love's example for Multi-row selection which involves creating a custom UITableViewCell that initiates an animation in layoutSubviews to display checkboxes to the left of each row, like so:
- (void)layoutSubviews
{
[UIView beginAnimations:nil context:nil];
[UIView setAnimationBeginsFromCurrentState:YES];
[super layoutSubviews];
if (((UITableView *)self.superview).isEditing)
{
CGRect contentFrame = self.contentView.frame;
contentFrame.origin.x = EDITING_HORIZONTAL_OFFSET;
self.contentView.frame = contentFrame;
}
else
{
CGRect contentFrame = self.contentView.frame;
contentFrame.origin.x = 0;
self.contentView.frame = contentFrame;
}
[UIView commitAnimations];
}
This works fine and for all intents and purposes my UITableView acts as it should. However I'm running into a small aesthetic issue: when scrolling my UITableView rows which have not previously been displayed will initiate their sliding animation, meaning the animation is staggered for certain rows as they come into view.
This is understandable, given that setAnimationBeginsFromCurrentState has been set to YES and rows further down in the UITableView have yet to have their frame position updated. To solve the issue, I attempted to use willDisplayCell to override the animation for cells which become visible while the UITableView is in edit mode. Essentially bypassing the animation and updating the rows frame immediately, so as to make it appear as if the cell has already animated into place, like so:
/*
Since we animate the editing transitions, we need to ensure that all animations are cancelled
when a cell is scheduled to appear, so that things happen instantly.
*/
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
[cell.contentView.layer removeAllAnimations];
if(tableView.isEditing) {
CGRect contentFrame = cell.contentView.frame;
contentFrame.origin.x = EDITING_HORIZONTAL_OFFSET;
cell.contentView.frame = contentFrame;
} else {
CGRect contentFrame = cell.contentView.frame;
contentFrame.origin.x = 0;
cell.contentView.frame = contentFrame;
}
}
Unfortunately this doesn't seem to have any effect. Does anyone have any idea as to how I can solve this issue?
Not sure if you still need an answer to this question but I just ran into the exact same issue so I thought that I would share my solution. I implemented Multi-Selection the same way its described in the Cocoa with Love blog post that you mentioned.
In the cellAtIndexPath DataSource method when I create a new cell (not if the cell is already in the Queue of reusable cells) I check if the tableView is in editing mode and if it is I set a property on the cell (I created my own custom cell with an EnableAnimation property) to false so when it gets the SetEditing callback it will not animate the cell, instead it will just set the frame. In the constructor of the Cell class I set EnableAnimation to true, when the SetEditing callback is called I set EnableAnimation to the animate argument that is passed in. I hope this helps.
Related
I'm trying to mimic the iMessage bubble text behaviour with an UITableView. In order to always scroll to the bottom I'm using scrollToRowAtIndexPath when viewDidLoad and viewDidAppear. This is because when the viewDidLoad method is called, the table has not been completely loaded, so I need that extra scroll in viewDidAppear. This code makes the trick. However, what I want is not an animated scroll (setting animated to NO does not solve this), I want the table to be displayed always from the bottom, not load the table and then go to the last row.
Is this possible? I can't find any solution that fits completely with the desired behaviour.
This is the best solution!
Just reverse everything!
tableView.transform = CGAffineTransformMakeRotation(-M_PI);
cell.transform = CGAffineTransformMakeRotation(M_PI);
Swift 4.0:
tableView.transform = CGAffineTransform(rotationAngle: -CGFloat.pi)
cell.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
Be careful though, because now the headerView and footerView positions are reversed as well.
You can avoid the call from viewDidLoad because scrolling from within viewDidAppear makes that first call redundant. viewDidAppear is called every time you navigate back to the view but viewDidLoad is only called once when the view is initialized.
I would agree with earlier suggestions of hiding the scroll from the user instead of changing the way a UITableView is loading data. My suggestion would be to use the scrollToRowAtIndexPath method in the viewWillAppear method with animation set to NO. After that if you have to add a new row while the table is visible to the user, use insertRowsAtIndexPaths:withRowAnimation: to add a row at the bottom of the table view. Be sure to take care of adding the data at the end of your data model so that when the user navigates away and comes back, s/he comes back to the same layout.
Hope this helps.
edit:
Just saw your reason for not accepting the previous answers and thought I'd elaborate a little more. The solution I propose would require minimum effort, avoid calling reloadData time and again and thus avoid calling the scrollToRowAtIndexPath method again and again. You only need to make one call to scrollToRowAtIndexPath in viewWillAppear to scroll to the bottom of the table view (hiding the transition from the user when doing so) and you wouldn't need to do that again.
I do something similar in an RPN calculator I've built. I have a table view with all the numbers in it and when a number is added to the stack, everything pops up one cell. When I load the view I call:
[self.myTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:NumOfStackItems - 1 inSection:0]
atScrollPosition:UITableViewScrollPositionTop animated:NO];
In my viewWillAppear. This way my table view starts shown at the bottom of the stack and no animation is seen. By putting this in the viewWillAppear, every time I navigate to the view, it shows up at the bottom of the table.
When I add numbers to the stack, I just add it in an array that holds all the numbers and then put the text in the proper row like this:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Cell initialization here...
NSUInteger row_num = [indexPath row];
cell.rowNumber.text = [NSString stringWithFormat:#"%g", [DataArray objectAtIndex:NumberOfStackItems-row_num-1];// subtract the row number off to get the correct array index
return cell
}
I also make sure that whenever I update the tableview with a new value i first call the reloadData function, and then call the scrollToRowAtIndexPath function I cited above, this way I stay at the bottom of the table.
You can have your UITableView hidden on viewDidLoad, and then change it to visible on viewDidAppear right after you scroll the table to the bottom. This way the user won't see the scrolling animation.
The solution is to override viewWillAppear and let it scroll (non-animated) to the bottom:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self goToBottom];
}
-(void)goToBottom
{
NSIndexPath *lastIndexPath = [self lastIndexPath];
[self.tableView scrollToRowAtIndexPath:lastIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];
}
-(NSIndexPath *)lastIndexPath
{
NSInteger lastSectionIndex = MAX(0, [self.tableView numberOfSections] - 1);
NSInteger lastRowIndex = MAX(0, [self.tableView numberOfRowsInSection:lastSectionIndex] - 1);
return [NSIndexPath indexPathForRow:lastRowIndex inSection:lastSectionIndex];
}
By performing this at viewWillAppear it will be done before the user sees the table.
You can fix it by making an invisible footer and do the calculations in there. When the footer is loaded the contentSize is updated. To make it scroll I check set the contentOffset of the tableview.
I have commented out the animation part, since you wanted it without, but it also works.
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
return 1;
}
-(UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
if( tableView.contentOffset.y != tableView.contentSize.height - tableView.frame.size.height && automaticScroll ){
//[UIView animateWithDuration:0.0 animations:^{
self.contentTableView.contentOffset = CGPointMake(0, tableView.contentSize.height - self.contentTableView.frame.size.height);
//} completion:^(BOOL finished) {
[tableView reloadData];
//}];
automaticScroll = NO;
}
UIView *emptyFooter = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 1)];
emptyFooter.backgroundColor = [UIColor clearColor];
return emptyFooter;
}
I created a BOOL automaticScroll to trigger the scroll to the bottom. This should be set in the viewWillAppear method, or whenever you load the data and reload the tableView.
If you want to add rows, you also need to set the BOOL, like:
-(void)addItemButtonClicked:(id)sender
{
automaticScroll = YES;
//Add object to data
[self.contentTableView reloadData];
}
If you need more help, please let me know.
scrollToRowAtIndexPath
use to scroll the row in tableview to particular position
just change content inset after load data to move content view of table view if height is less than parent view.
[self.tableView reloadData];
[self.tableView setContentInset:UIEdgeInsetsMake(self.view.frame.size.height - self.tableView.contentSize.height < 0 ? 0 : self.view.frame.size.height - self.tableView.contentSize.height, 0, 0, 0)];
Swift 3.1
tableView.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
cell.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
Credits: #Christos Hadjikyriacou
As a little background, im building a rotated UITableView as a sort of sideways "picker" view. To do this, im taking a UITableView, appling a rotation transform to it, and then rotating again the UITableViewCells inside the tableview.
The problem im having is that some of the table cells become "misaligned" - their frame gets drawn at a certain distant offset (in both the x and y dimension) from the other table cells.
I've narrowed down that this bug occurs on the first table cell completely out of the visible tableview rect after a [tableView reloadData] call is made. (i.e. if I have 4 table cells, A which is completely visible and drawn, B which is half on/half off the view, and C and D which are completely off the screen and not yet rendered, when i scroll to C it is bugged, but when i scroll to D, it is not).
Now for some code -
the containing view's init
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
...
self.tableView = [[UITableView alloc] init];
[self addSubview:_tableView];
[_tableView setDelegate:self];
[_tableView setShowsVerticalScrollIndicator:NO];
[_tableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];
CGAffineTransform transform = CGAffineTransformMakeRotation(-1.5707963);
_tableView.transform = transform;
_tableView.frame = self.bounds;
...
}
return self;
}
the relevant delegate methods
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [tableView.dataSource tableView:tableView cellForRowAtIndexPath:indexPath].frame.size.height;
}
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
CGAffineTransform transform = CGAffineTransformMakeRotation(1.5707963);
cell.transform = transform;
}
the table cell's layoutSubviews
edit: I manually set the size of the cell (mainly the width) based on the length on the content of the cell
- (void)layoutSubviews {
[super layoutSubviews];
// some calculations and sizing of subviews
// height and width are swapped here, because the table cell will be rotated.
self.frame = (CGRect){self.frame.origin.x,self.frame.origin.y,calculatedHeight,calculatedWidth};
}
It would seem that the bugged tablecell's frame.origin is set incorrectly when it reaches layout subviews. Setting the frame's origin.x value to 0 fixes the x dimension offset problem, but obviously I can't do the same for the y dimension because this value determines the cell's position in the tableview.
Please let me know if there's some crucial info I might be leaving out. Thanks!
Have you tried to set the anchor point of the cells layer, which is the point the layer is rotated (transformed) about. It defaults to .5, .5 which is the centre of the layer, it may need to be set to 0, 0 (or 1, 1 - i can't remember if the layer coordinates are inverted off the top of my head)
or try setting the frame in willDisplayCell immediately after applying the transform instead of doing it in layout subviews
Good news - after spending many many previous hours trying to figure this out, I just stumbled upon the solution.
This code was getting called multiple times for the 'bugged' cell, and apparently due to some intricacies of CALayer and CGAffineTranform's, assigning the tranform had an additive affect.
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
CGAffineTransform transform = CGAffineTransformMakeRotation(1.5707963);
cell.transform = transform;
}
The solution was to move the transform into the cell's init method, so that it is guaranteed to be set only once per cell.
// ... init stuff
self.transform = CGAffineTranformMakeRotation(1.5707963);
// ... more init stuff
Make a subclass of UITableViewCell and it its layoutSubviews override make sure to set the transform there:
class InvertedTableViewCell: UITableViewCell {
override func layoutSubviews() {
self.transform = CGAffineTranformMakeRotation(1.5707963)
}
}
I have a grouped UITableView with custom cells (created by subclassing UITableViewCell). I add subviews and insert sublayers just like this:
[self.contentView addSubview:myUILabel];
and
[self.contentView.layer insertSublayer:myCALayer];
When entering editing mode for deleting rows, the cells move right and myUILabel and myCALayer go beyond the borders of the cell, which looks ugly.
I tried this:
Grouped UITableView with custom UILabels in Editing Mode
... but it didn't help.
What "kind of" worked is to override setEditing: in my custom cell
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
CGRect oldBounds = self.myCALayer.bounds;
CGRect newBounds = oldBounds;
CGPoint oldPosition = self.myCALayer.position;
CGPoint newPosition = oldPosition;
//move right
if (editing && !self.showingDeleteConfirmation) {
newBounds.size.width -=32;
newPosition.x -= 15;
}
//move back left
else if (self.editing) {
newBounds.size.width += 32;
newPosition.x += 15;
}
self.myCALayer.bounds = newBounds;
self.myCALayer.position = newPosition;
[super setEditing:editing animated:animated];
}
... however, the animations are not performed synchronously. What happens is while the cell moves right when entering editing mode, first the width of the layer shrinks, and then the position changes. Although the layer finally fits the cell, the animation looks bad.
Thanks for your advice!
Not sure about your layers, but you should be able to set a smart autoresizingMask on your subviews so they re-layout appropriately when the size of contentView is altered by the tableview (on entry to edit mode).
More here.
Edit: with a little looking, I think CALayer's anchorPoint property may help with your sublayers. (more here)
Alternatively (and this is a bit of a hack, and could hurt performance somewhat), you could set your custom layers on a UIView w/ an appropriately set autoresizingMask.
Is there any way to animate the removal of a UITableView cell accessory?
I currently am showing a UITableViewCellAccessoryDisclosureIndicator, but I would like to animate swapping the disclosure indicator with a UISwitch on all visible table cells.
I've tried something like this:
[UIView animateWithDuration:0.3
animations:^{
for (SwitchTableViewCell *cell in self.tableView.visibleCells)
{
cell.accessoryType = UITableViewCellAccessoryNone;
}
}];
... but unfortunately that has no affect. The disclosure indicator abruptly disappears and the contentView width jumps in one step, rather than a smooth transition.
accessoryType is not an animatable property. There are two ways you can do this, depending on your situation. The easiest only applies if you are changing the accessory to a UISwitch because of entering the editing state. In this case, just usecell.editingAccessoryType = theSwitch; in your tableView:cellForRowAtIndexPath: method. The table view will then do a fade in/out automatically when entering editing mode.
If you are doing this outside of editing mode, then the following code will do what you want:
[UIView animateWithDuration:0.3 animations:^{
for(SwitchTableViewCell *cell in self.tableView.visibleCells) {
[[cell valueForKey:#"_accessoryView"] setAlpha:0.0];
}
} completion:^(BOOL done) {
for(SwitchTableViewCell *cell in self.tableView.visibleCells) {
cell.accessoryView = theSwitch;
}
}];
However, I do not know if this code will make it into the app store since it uses the hidden property _accessoryView.
I have implemented a custom UITableViewCell which includes a UITextView that auto-resizes as the user types, similar to the "Notes" field in the Contacts app. It is working properly on my iPhone, but when I am testing it in the iPad, I am getting some very strange behavior: When you get to the end of a line, the keyboard hides for a millisecond and then shows itself again immediately. I would write it off as just a quirky bug, but it actually causes some data loss since if you are typing, it loses a character or two. Here's my code:
The Code
// returns the proper height/size for the UITextView based on the string it contains.
// If no string, it assumes a space so that it will always have one line.
- (CGSize)textViewSize:(UITextView*)textView {
float fudgeFactor = 16.0;
CGSize tallerSize = CGSizeMake(textView.frame.size.width-fudgeFactor, kMaxFieldHeight);
NSString *testString = #" ";
if ([textView.text length] > 0) {
testString = textView.text;
}
CGSize stringSize = [testString sizeWithFont:textView.font constrainedToSize:tallerSize lineBreakMode:UILineBreakModeWordWrap];
return stringSize;
}
// based on the proper text view size, sets the UITextView's frame
- (void) setTextViewSize:(UITextView*)textView {
CGSize stringSize = [self textViewSize:textView];
if (stringSize.height != textView.frame.size.height) {
[textView setFrame:CGRectMake(textView.frame.origin.x,
textView.frame.origin.y,
textView.frame.size.width,
stringSize.height+10)]; // +10 to allow for the space above the text itself
}
}
// as per: https://stackoverflow.com/questions/3749746/uitextview-in-a-uitableviewcell-smooth-auto-resize
- (void)textViewDidChange:(UITextView *)textView {
[self setTextViewSize:textView]; // set proper text view size
UIView *contentView = textView.superview;
// (1) the padding above and below the UITextView should each be 6px, so UITextView's
// height + 12 should equal the height of the UITableViewCell
// (2) if they are not equal, then update the height of the UITableViewCell
if ((textView.frame.size.height + 12.0f) != contentView.frame.size.height) {
[myTableView beginUpdates];
[myTableView endUpdates];
[contentView setFrame:CGRectMake(0,
0,
contentView.frame.size.width,
(textView.frame.size.height+12.0f))];
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
int height;
UITextView *textView = myTextView;
[self setTextViewSize:textView];
height = textView.frame.size.height + 12;
if (height < 44) { // minimum height of 44
height = 44;
[textView setFrame:CGRectMake(textView.frame.origin.x,
textView.frame.origin.y,
textView.frame.size.width,
44-12)];
}
return (CGFloat)height;
}
The Problems
So, here's what's happening
This code is working 100% properly on my iPhone and in the iPhone simulator. As I type the text, the UITextView grows smoothly, and the UITableViewCell along with it.
On the iPad simulator, however, it gets screwy. It works fine while you are typing on the first line, but when you get to the end of a line, the keyboard disappears and then reappears immediately, so that if the user continues typing the app misses a character or two.
Here are some additional notes on the weird behaviors that I have noticed which may help explain it:
Also, I have found that removing the lines [myTableView beginUpdates]; [myTableView endUpdates]; in the function textViewDidChange:(UITextView *)textView makes the UITextView grow properly and also doesn't show and hide the keyboard, but unfortunately, then the UITableViewCell doesn't grow to the proper height.
UPDATE: Following these instructions, I am now able to stop the strange movement of the text; but the keyboard is still hiding and showing, which is very strange.
Does anyone have any ideas as to how to get the keyboard to continually show, rather than hide and show when you get to the end of the line on the iPad?
P.S.: I am not interested in using ThreeTwenty.
you should return NO in:
-(BOOL) textViewShouldEndEditing:(UITextView *)textView
if you would like to show keyboard at all times. You should handle cases, which keyboard should be hidden, by returning YES to this delegate function.
edit:
I dug a little more, when [tableView endUpdates] called, it basically does 3 things :
Disables user interaction on the tableView
Updates cell changes
Enables user interaction on the tableView
The difference between SDKs(platforms) is at [UIView setUserInteractionEnabled] method. As UITableView does not overrite setUserInteractionEnabled method, it is called from super (UIView).
iPhone when setUserInteractionEnabled called, looks for a private field _shouldResignFirstResponderWithInteractionDisabled which returns NO as default, so does not resign the first responder (UITextView)
But on iPad there is no such check AFAIK, so it resignes UITextView on step 1, and sets focus and makes it first responder on step 3
Basically, textViewShouldEndEditing, which allows you to keep focus, according to SDK docs, is your only option ATM.
This method is called when the text
view is asked to resign the first
responder status. This might occur
when the user tries to change the
editing focus to another control.
Before the focus actually changes,
however, the text view calls this
method to give your delegate a chance
to decide whether it should.
I had the same issue for an iPad app and came up with another solution without having calculating the height of the text itself.
First create a custom UITableViewCell in IB with an UITextField placed in the cell's contentView. It's important to set the text view's scrollEnabled to NO and the autoresizingMask to flexibleWidth and flexibleHeight.
In the ViewController implement the text view's delegate method -textViewDidChanged: as followed, where textHeight is a instance variable with type CGFloat and -tableViewNeedsToUpdateHeight is a custom method we will define in the next step.
- (void)textViewDidChange:(UITextView *)textView
{
CGFloat newTextHeight = [textView contentSize].height;
if (newTextHeight != textHeight)
{
textHeight = newTextHeight;
[self tableViewNeedsToUpdateHeight];
}
}
The method -tableViewNeedsToUpdateHeight calls the table view's beginUpdates and endUpdates, so the table view itself will call the -tableView:heightForRowAtIndexPath: delegate method.
- (void)tableViewNeedsToUpdateHeight
{
BOOL animationsEnabled = [UIView areAnimationsEnabled];
[UIView setAnimationsEnabled:NO];
[table beginUpdates];
[table endUpdates];
[UIView setAnimationsEnabled:animationsEnabled];
}
In the table view's -tableView:heightForRowAtIndexPath: delegate method we need to calculate the new height for the text view's cell based on the textHeight.
First we need to resize the text view cells height to the maximum available height (after subtracting the height of all other cells in the table view). Then we check if the textHeight is bigger than the calculated height.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
CGFloat heightForRow = 44.0;
if ([indexPath row] == kRowWithTextViewEmbedded)
{
CGFloat tableViewHeight = [tableView bounds].size.height;
heightForRow = tableViewHeight - ((kYourTableViewsNumberOfRows - 1) * heightForRow);
if (heightForRow < textHeight)
{
heightForRow = textHeight;
}
}
return heightForRow;
}
For a better user experience set the table view's content insets for bottom to e.g. 50.0.
I've tested it on the iPad with iOS 4.2.1 and works as expected.
Florian