UITableView correcting scroll position after device rotation - iphone

I'm building something like a reader for a book. When the user rotates the phone I want to increase the font size. I'm using a UITableView to display chunks of text.
Problem is that, increasing the font size increases height of rows in my table view and if I was reading paragraph 320 in portrait mode I get 280 or something similar in landscape mode.
I have set up a rotation notification listener using this code:
UIDevice *device = [UIDevice currentDevice];
[device beginGeneratingDeviceOrientationNotifications];
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:device];
and tried to save the last paragraph index before rotation and then scroll down to it after the rotation but I can't seem to achieve desired effect.
What's the best way to handle this kind of situation and where do I actually implement "before" and "after" states of rotation?
I'd like it to work on iOS 4+.

Swift 3 version of Said Ali Samed's answer (willRotateToInterfaceOrientation and didRotateFromInterfaceOrientation are deprecated):
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: { context in
// Save the visible row position
self.visibleRows = self.tableView.indexPathsForVisibleRows!
context.viewController(forKey: UITransitionContextViewControllerKey.from)
}, completion: { context in
// Scroll to the saved position prior to screen rotate
self.tableView.scrollToRow(at: self.visibleRows[0], at: .top, animated: false)
})
}

Use delegate method willRotateToInterfaceOrientation: to store the visible cell in an array then using the other delegate method of UITableView didRotateFromInterfaceOrientation: scroll to the visible index path that you stored earlier in the array. This is recommended and you don't have to rely on the inconsistent 0.2 seconds wait in a different thread to handle post rotate event.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
// Save the visible row position
visibleRows = [tableview indexPathsForVisibleRows];
}
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
// Scroll to the saved position prior to screen rotate
[tableView scrollToRowAtIndexPath:[visibleRows objectAtIndex:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}

Since I couldn't get a good answer I'll answer myself. I've looked everywhere but couldn't find a way to do what I wanted so I just used the shouldAutorotateToInterfaceOrientation method to increase the size of font and then start a little thread that sleeps for 0.2 seconds and after that scrolls to the desired row. Thanks for your help.
Edit: Use delegate method willRotateToInterfaceOrientation: to store the visible cell in an array then use the delegate method didRotateFromInterfaceOrientation: to scroll to the visible index path that you recorded in the array.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
// Save the visible row position
visibleRows = [tableview indexPathsForVisibleRows];
}
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
// Scroll to the saved position prior to screen rotate
[tableView scrollToRowAtIndexPath:[visibleRows objectAtIndex:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}

A simple way to do it would be to store the index path of the top visible cell, change the font size then restore the top cell:
NSIndexPath* topCellIndexPath = [[_tableView indexPathsForVisibleRows] objectAtIndex:0];
//Insert code to change font size here
[_tableView scrollToRowAtIndexPath:topCellIndexPath atScrollPosition:UITableViewScrollPositionTop animated:NO];
This code can be put in any method that's run when orientation changes, such as the orientationChanged: method you put in the question.
This will not take into account having scrolled halfway down a cell so if the height of your cells is large it will not work well and a more complicated method using content offsets would be needed. Let me know if this is the case.

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)
fromInterfaceOrientation
{
NSLog(#"didRotateFromInterfaceOrientation:%d",fromInterfaceOrientation);
[tableView reloadRowsAtIndexPaths:[tableView indexPathsForVisibleRows] withRowAnimation:UITableViewRowAnimationNone];
}
Then I would recommend adding either the interfaceOrientation number or simply the table width to the dequeue cell name that way the tableView knows that cells in one rotation are different from those in another. Like so:
- (UITableViewCell *)tableView:(UITableView *)tv
cellForRowAtIndexPath:(NSIndexPath *)indexPath
withType:(NSString *)s_type
{
UITableViewCell *cell = nil;
// add width of table to the name so that rotations will change the cell dequeue names
s_cell = [s_cell stringByAppendingString:
[NSString stringWithFormat:#"%#%d",#"Width",(int)tv.bounds.size.width]
];
NSLog(#"%#",s_cell);
cell = [tv dequeueReusableCellWithIdentifier:s_cell];
if( cell == nil ) {
cell = [[[UITableViewCell alloc]
initWithFrame:CGRectZero reuseIdentifier:s_cell] autorelease];
}
}

Firstly, to reload all of your table cells use [self.tableView reloadData]
Secondly, add the line of code that is responsible for the shrinking inside the (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method.
Example:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//Some identifier and recycling stuff
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) {
//Make labels smaller
}
else {
//Make them bigger
}
}
Or you can just call your updateCellForRotate:forRow: method when making them. But I'm not sure how that function works, so I can't be too specific.

Related

How dynamically change UITableViewCell size for the last cell

I have a UITableView with 16-20 cells inside with dynamic cell size. When a cell expands it self it should also move itself to the top of the screen. I did that using "UITableView setContentOffset" method. It works well except for the last cell in table, it's not able to move itself to the top.
I tried altering the frame & content size of UITableView but none of those were working for me!
Any Idea?
[UPDATE]
Here is a part of code: (it's inside the UITableViewCell, so self is pointing to current cell)
HomeViewController *tempViewController = (HomeViewController *) delegate;
UIView *commentField;
/*Skipping lines of codes manipulating commentField */
//Adding a subview to current cell which needs more space
[self addSubview:commentField];
//Expanding cellSize to EXPANDED_CELL_HEIGHT
//ViewController has access to cell size property and using that to determine each cell size.
[self setCellSize:(EXPANDED_CELL_HEIGHT)];
//Reloading UITableView to reflect the cell size change with animation
[[tempViewController tableView] beginUpdates];
[[tempViewController tableView] endUpdates];
[[tempViewController tableView] setContentOffset:CGPointMake(0, self.frame.origin.y) animated:YES];
and in my view controller (as I said earlier) I'm getting cellSize form cell itself
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [(BaseTableViewCell *)[cellContainer objectAtIndex:indexPath.row] cellSize];
}
You can change the height of the cell using the tableview delegate method heightForRowAtIndexPath: When indexPath.row equals your last row, return the height you would like.
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == i)//i being whatever row index you want to change
{
return 60.0;//or float size you want
}
else
{
return 30.0;
}
}
Or you can just modify the bounds of the cell inside willDisplayCell:. If your using a custom tableview cell, just shrink the subviews to whatever frame you'd like and make the cell background clear etc.

UITextView inside UITableView

I know this question has been asked before, though I can't seem to find what I want. I have a section in my app where I have a tableview with a textview inside of it. I DO NOT want to have a seperate .xib, .h, and .m files for the tableview cell. The tableview does not need to shrink or grow depending on the amount of text inside the textview. I don't want the textview to be editable either. I hope this isn't too much to ask for, though I'm really stuck at the moment.
To do this, you will need to embed one in your UITableViewCell. But there's no need to create a custom cell. Here is the basic idea of what you will want to do:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
UITextView *comment = [[UITextView alloc] initWithFrame:CGRectMake(cell.frame.origin.x, cell.frame.origin.y, cell.frame.size.width, tableView.rowHeight)];
comment.editable = NO;
comment.delegate = self;
[cell.contentView addSubview:comment];
[comment release];
}
return cell;
}
You will, of course, need to set your rowHeight if you don't want the standard 44pt height that comes with the cell. And if you want actual cells, you'll need to add your own logic so that only the cell you want is a textView, but this is the basic idea. The rest is yours to customize to your fitting. Hope this helps
EDIT: to bypass the textView to get to your cell, there are two ways to go about this.
1) you can make a custom textView class and overwrite touchesBegan to send the message to super:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
}
this will send the touch events to its superview, which would be your tableView. Considering you didn't want to make custom UITableViewCells, I imagine you probably don't want to make a custom textView class either. Which leads me to option two.
2) when creating the textView, remove comment.editable = NO;. We need to keep it editable, but will fix that in a delegate method.
In your code, you will want to insert a textView delegate method and we'll do all our work from there:
EDIT: changing this code to use with a UITableViewController
- (BOOL)textViewShouldBeginEditing:(UITextView *)textView {
// this method is called every time you touch in the textView, provided it's editable;
NSIndexPath *indexPath = [self.tableView indexPathForCell:textView.superview.superview];
// i know that looks a bit obscure, but calling superview the first time finds the contentView of your cell;
// calling it the second time returns the cell it's held in, which we can retrieve an index path from;
// this is the edited part;
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
// this programmatically selects the cell you've called behind the textView;
[self tableView:self.tableView didSelectRowAtIndexPath:indexPath];
// this selects the cell under the textView;
return NO; // specifies you don't want to edit the textView;
}
If that's not what you wanted, just let me know and we'll get you sorted out

Problem updating UITableViewCells when rotating UITableView

I have a UILabel in a custom UITableViewCell that gets resized when the device is rotated. The text in this label needs to be recalculated after the rotation because I am cutting it down to size and appending some text at the end.
E.g. the datamodel has: "This is a run-on sentence that needs to stop."
In portrait mode it becomes "This is a run-on sent... more"
In landscape mode it becomes "This is a run-on sentence that... more"
From (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
I am able to access the visible UITableViewCells and update the descriptions.
The problem seems to be that there are UITableViewCells that are cached but I can't get to. When I scroll the UITableView after a rotation, one or two cells that are below the visible area after the rotation don't have the correct text in the label. So they haven't been rendered via (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath - but they weren't returned by [tableView visibleCells] (or via looping through all views returned via [tableView subViews]).
I've tried to access the "extra" cells via this method:
for (int index=max + 1; index < max + 3 && index < [cellTypes count]; index++) {
NSIndexPath *updatedPath = [NSIndexPath indexPathForRow:index inSection:0];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:updatedPath];
if (cell == nil) { continue; }
[self updateCellForRotate:cell forRow:index];
}
(where max is the biggest row returned from visibleCells) but cell is always nil.
Is there anyway to flush the cache of UITableViewCells so that they don't get re-used? Or to access them so I can update them?
Thanks!
Two things.
First. In your didRotateFromInterfaceOrientation method you can simply reload the visible rows like so:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation) fromInterfaceOrientation
{
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
NSLog(#"didRotateFromInterfaceOrientation:%d",fromInterfaceOrientation);
[tableView reloadRowsAtIndexPaths:[tableView indexPathsForVisibleRows] withRowAnimation:UITableViewRowAnimationNone];
}
Then I would recommend you add either the interfaceOrientation number or simply the table width to the dequeue cell name that way the tableView knows that cells in one rotation are different from those in another. Like so:
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath withType:(NSString *)s_type
{
UITableViewCell *cell = nil;
// add width of table to the name so that rotations will change the cell dequeue names
s_cell = [s_cell stringByAppendingString:[NSString stringWithFormat:#"%#%d",#"Width",(int)tv.bounds.size.width]];
NSLog(#"%#",s_cell);
cell = [tv dequeueReusableCellWithIdentifier:s_cell];
if( cell == nil ) {
cell = [[[UITableViewCell alloc];
initWithFrame:CGRectZero reuseIdentifier:s_cell] autorelease];
}
}
Firstly, to reload all of your table cells use [self.tableView reloadData]
Secondly, add the line of code that is responsible for the shrinking inside the (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method.
Example:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//Some identifier and recycling stuff
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) {
//Make labels smaller
}
else {
//Make them bigger
}
}
Or you can just call your updateCellForRotate:forRow: method when making them. But I'm not sure how that function works, so I can't be too specific.
When you create the cell in cellForRowAtIndexPath:, add it to an array. Then, loop through the array, updating the text as necessary.
Hope this helps,
jrtc27
EDIT:
You say they are custom cells - could you not update your text in your UITableViewCell subclass?
So, I was having (what I think was) a very similar problem recently, and none of the posted answers helped me, I'm sorry to say.
My issue was that I deliberately resized and repositioned the UITableView upon rotation, and I did that programatically. The table cells in portrait took up the width of the view, and in Landscape were made somewhat higher but less wide. I then repositioned the elements of the cell depending on the orientation we'd come to.
Upon application start, the first viewing of the table was fine. Then I rotated and found that I appeared to have two instances of some elements, and these appeared to be where the cells had been visible in the first table. Rotating back then corrupted the initial orientation table with elements from the previous table.
I tried all of the applicable answers above, until I looked closer at the cellForRowAtIndexPath code:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
I understand cell re-use is a great idea and all, but I really didn't need to retain (as in preserve) any cells and wanted them all bright, spangly and new after each rotation.
EDIT: In my own app I'll have maybe 20-30 rows maximum, as I personally don't like hugely long tables. If there were going to be lots of rows returned for a particular query I'd have some filters available to the user to help them sort out which rows they wanted. If you're going to have loads of rows displayed, then dequeuing them may cause you a performance impact that you don't want.
All I did was comment out the if and the following bracket, and my table cells renewed exactly as I wanted them to:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
//if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
//}
Apologies for the waffle, and the late answer to an old question.
Ben.
Waffles and cream, or syrup.
You can use this simple line on the shouldAutorotateToInterfaceOrientation method :
self.view.autoresizesSubviews = YES;
For me it works always successfully

First Tap on customcell of uitableview should expand it and second should contract it

In my application I have this requirement that first tap on custom cell of uitableview with a label in it should expand it and second should contract it. I'm able to expand and contract cell and expand label inside cell, but not able to contract the label on second tap.
I'm using this function
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
if( selected == YES ) {
[self expandRow];
}
else {
[self contractRow];
}
height = [lblFeed frame].size.height + 75;
}
expandRow expands the label and contractRow contracts it. I'm perplexed as for how many rows this function gets called. It doesn't get called only for the cell tapped, it gets called more number of times for single tap on single cell may be for other cells but I'm not getting which rows.
This' really urgent.
Can anybody please help?
Tapping a selected row doesn't cause it to be deselected. When a cell gets selected, it stays selected until deselectRowAtIndexPath:animated: gets called on its table. That's why your method isn't getting called for the second tap.
In an MVC architecture like UIKit, it's recommended that you handle user interactions in your controller classes. It would be appropriate to override -[UITableViewCell setSelected:animated:] if all you were doing was customizing the way the view represents a selected cell, but in this case your expand/contract toggle behavior would require a change in the way UITableView selects and deselects its cells.
You could subclass UITableView and implement this toggle behavior yourself, or you can leave UITableView alone and handle it all at the UIViewController level by doing something like this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if ([self.expandedIndexPath isEqual:indexPath]) {
[(YourCustomCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath] contractRow];
self.expandedIndexPath = nil;
}
else {
if (self.expandedIndexPath) {
[(YourCustomCell *)[self tableView:tableView cellForRowAtIndexPath:self.expandedIndexPath] contractRow];
}
[(YourCustomCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath] expandRow];
self.expandedIndexPath = indexPath;
}
[tableView deselectRowAtIndexPath:indexPath animated:NO];
}
I would suggest that you don't add your functionality on top of the selected property of the cell, which has slightly different behaviour than you expect.
Just add your own BOOL expanded property, and see how that works. You should probably call it from the UITableView delegate methods, too.

How to get the size of the "Delete" button in a UITableViewCell when swipe to delete?

I add a datetime label on the right of a table cell. When swip-to-delete shows the "Delete" button, the datetime label need to shift left a little bit. But how to get the "Delete" button's size?
I tried to find it in the cell.subviews but failed.
You don't have to know the button's size. Instead, use the size of the cell's contentView property to calculate the sizes of the subviews. When swiping over a cell, UIKit will adapt the contentView's size and call layoutSubviews on the cell-object. In your subclass of UITableViewCell, overwrite the layoutSubviews method and set the appropriate sizes to the subviews.
Look at RecipeTableViewCell.m of Apple's iPhoneCoreDataRecipes sample code.
Use this code in your custom Cell class
- (void)layoutSubviews {
[super layoutSubviews];
NSMutableArray *subviews = [self.subviews mutableCopy];
while (subviews.count > 0)
{
UIView *subV = subviews[0];
[subviews removeObjectAtIndex:0];
if ([NSStringFromClass([subV class])isEqualToString:#"UITableViewCellDeleteConfirmationView"])
{
UIView *deleteButtonView = (UIView *)[self.subviews objectAtIndex:0];
CGFloat deleteBtnHeight=deleteButtonView.frame.size.height;//here you get the height
}
}
}
The size adjusts to fit the text contained. See the following code:
- (NSString *)tableView:(UITableView *)tableView
titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
return #"Dynamic width!";
}
vs
- (NSString *)tableView:(UITableView *)tableView
titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
return #"⏎";
}
If you don't override layoutSubviews method in your custom table view cell than my approach is:
Create your custom subview, set frame basing on contentView.bounds.
Set autoresizingMask to UIViewAutoresizingFlexibleWidth.
Add your custom subview to ContentView of a cell.
Configure cell for editing
Now when you swipe on cell the delete button appears and your view auto resizes with contentView.
The delete button is 63x33.