Scroll UITableView Cell beyond table bounds - ios

I have a UITableView with changing height.
I wanted in every cell selection to kind of get the 'focus' on it by moving it to the middle of the screen. The problem is to move the upper and lower cells in the table since they cannot scroll beyond the table upper and lower bounds. I'll also prefer to do this with animations..
Any suggestions?
Thanks!

I created small example of how to achieve this:
https://github.com/vkozlovskyi/CenterCellExample
You just need to calculate scroll offset of UITableView, and and animate the change:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// Get offset for cell
CGFloat centerOffset = self.tableView.frame.size.height / 2 - self.tableView.rowHeight / 2;
// Content offset
CGFloat total = indexPath.row * self.tableView.rowHeight;
[UIView animateWithDuration:0.3 animations:^{
self.tableView.contentOffset = CGPointMake(0, total - centerOffset);
}];
}
It works for all cells, upper and lower too.

You can add extra space to the content of your table view using the contentInset property. Add extra space above and below your table view will allow your cells to scroll all the way to the middle of your table view.
UITableView * tableView = self.tableView ;
CGRect bounds = self.tableView.bounds ;
CGFloat extraSpaceNeeded = 0.5 * ( bounds.size.height - tableView.rowHeight ) ;
tableView.contentInset = (UIEdgeInsets){ .top = -extraSpaceNeeded, .bottom = -extraSpaceNeeded } ;
Now you can just use
[tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionMiddle
animated:animated];

Related

UICollectionView doesn't scroll after setting contentInset

I have a collection view that scrolls horizontally and spans its parent view's full width. My cheap way to achieve paging on it is to set the cell widths to be equal to 1/3 of the collection view width, and to set that same amount of width as left and right content insets.
I disable scrolling in IB and replace with left and right swipe recognizers. My code almost works without setting contentInset, but setting the contentInset seems prevent any scrolling from happening
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGFloat itemWidth = self.collectionView.bounds.size.width/3.0;
NSInteger count = [self collectionView:self.collectionView numberOfItemsInSection:0];
self.collectionView.contentSize = (CGSize){ .width=itemWidth*count, .height=self.collectionView.bounds.size.height };
// uncomment this line, and the scroll code in the swipes below fails to work
//self.collectionView.contentInset = UIEdgeInsetsMake(0, itemWidth, 0, itemWidth);
self.collectionView.contentOffset = (CGPoint){ .x=self.collectionView.contentSize.width/2.0, .y=0 };
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGFloat width = self.view.bounds.size.width/3.0;
return (CGSize){ .width=width, .height=collectionView.bounds.size.height };
}
This code handles the swipes...
- (NSIndexPath *)centerIndexPath {
CGRect visibleRect = (CGRect){.origin = self.collectionView.contentOffset, .size = self.collectionView.bounds.size};
CGPoint visiblePoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect));
return [self.collectionView indexPathForItemAtPoint:visiblePoint];
}
- (void)swipeLeft:(UISwipeGestureRecognizer *)gr {
NSIndexPath *centerIndexPath = [self centerIndexPath];
NSLog(#"at %#", centerIndexPath);
if (centerIndexPath.row < [self collectionView:self.collectionView numberOfItemsInSection:0]-1) {
[self.collectionView scrollToItemAtIndexPath:centerIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}
}
- (void)swipeRight:(UISwipeGestureRecognizer *)gr {
NSIndexPath *centerIndexPath = [self centerIndexPath];
NSLog(#"at %#", centerIndexPath);
if (centerIndexPath.row > 0) {
[self.collectionView scrollToItemAtIndexPath:centerIndexPath atScrollPosition:UICollectionViewScrollPositionRight animated:YES];
}
}
All of this works, except when I set the contentInsets in the setup above. Then, even though I reach the scrollToItemAtIndexPath: code in the debugger, no scrolling occurs.
It's important to have those insets, because I want user to understand that center item is the selected item.
Can somebody explain why contentInset spoils scrolling and how to fix?
It looks like UICollectionView has its own built-in way to handle insets:
https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html#//apple_ref/doc/uid/TP40012334-CH3-SW1
Using Section Insets to Tweak the Margins of Your Content
Section insets are a way to adjust the space available for laying out cells. You can use insets to insert space after a section’s header view and before its footer view. You can also use insets to insert space around the sides of the content. Figure 3-5 demonstrates how insets affect some content in a vertically scrolling flow layout.
Figure 3-5 Section insets change the available space for laying out cells

Implementing iPad General Setting Page

So far I have customized the tableview and implemented the iPad General Setting Page. Below is the code for tableview which will change frame accordingly. But the issue is when I insert/delete rows or section in the tableview. My tableviewcell backgroundview (not cell) width get shrinks. Any idea what wrong am I doing here?
- (void)setFrame:(CGRect)frame
{
if (UI_USER_INTERFACE_IDIOM()==UIUserInterfaceIdiomPad)
{
CGFloat inset =10;
frame.origin.x += inset;
frame.size.width -= 2 * inset;//The issue is in this line
}
[super setFrame:frame];
}
I found the simple solution to achieve this. No need of customizing UITableView subclass. Just take the outlet of tableview and set the frame and change the color of backgroundview. like that below:-
CGFloat tableBorderLeft = 10;
CGFloat tableBorderRight = 10;
CGRect tableRect = self.view.frame;
tableRect.origin.x += tableBorderLeft; // make the table begin a few pixels right from its origin
tableRect.size.width -= tableBorderLeft + tableBorderRight; // reduce the width of the table
yourTableView.frame = tableRect;
self.view.backgroundColor=[UIColor colorWithRed:(239/255.0f) green:(239/255.0f) blue:(244/255.0f) alpha:1.0];

Dynamically size UITableViewCell's height

I have a UITableViewCell with multiple items inside. (Not just a textView so I cant follow this option.) I'm trying to dynamically size it's height based on the content it has inside.
The heights I will be changing, are a UITextView and a UIView. The textView will constantly be changing (at another method, if you'd like, I can post it). And the UIView will change if the user clicks a button:
Here is my code:
- (void)viewDidLoad
{
self.tableView.rowHeight = UITableViewAutomaticDimension;
}
- (IBAction)thisButton:(id)sender
{
CGRect frame = self.myView.frame;
frame.size.height = 50;
frame.size.width = self.myView.frame.size.width;
self.myView.frame = frame;
// update 'myView's constraint
self.viewHeight.constant = self.myView.frame.size.height;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[myTableView reloadData];
}
Problem:
What happens is, when I press the button, the UIView's height gets
updated, but then everything else in the cell gets moved up, and the cell stays the same size.
When the UITextView's height changes, it doesn't pull everything else
down, and the cells height stays the same. Though the textView's
height does change and it just goes over everything else.
Constraints:
On the UITextView I have 3 constraints - 2 on each side, and 1 on top. The UIView has 3 constraints - 2 on each side, and 1 on the bottom.
I then have a constraint connecting the UIView to the textView.
You can dynamically manage UITableViewCell size by calculating a max height of your internal views:
NSArray *array = [[NSArray alloc] initWithArray:#[view1, view2, view3]];
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
__block CGFloat maxHeight;
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
UIView *view = obj;
if (view.bounds.size.height > maxHeight) {
maxHeight = view.bounds.size.height;
}
}];
return maxHeight;
}
Then in the end of your method that changes view's dimensions you must call [self.tableView reloadData]

Get UICollectionView cell position after scrollToItemAtIndexPath

I would like to get a UICollectionViewCell's position after programattically scrolling to a cell. I'm setting the scroll position like this.
[self.collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:NO];
I would then like to get the frame of the cell I have scrolled to as I need to position another UIView near it.
This code returns the rect of the cell with the scrollview, but I need the position within the visible rect.
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:indexPath];
CGRect cellRect = attributes.frame;
The frame of a cell doesn't change, it's the content offset of the enclosing scroll view. Subtract the contentOffset from the frame's origin to get the on-screen position, or use one of the convertRect: methods to get the coordinates in a related view.
Here is the cell X and Center values
UICollectionViewLayoutAttributes *attributes = [collectionView layoutAttributesForItemAtIndexPath:indexPath];
CGPoint cellCenterPoint = attributes.center;
CGPoint contentOffset = [collectionView contentOffset];
NSLog(#"%f , %f",-1*contentOffset.x , cellCenterPoint.x);
int relativeCellCenter = (-1*contentOffset.x) + cellCenterPoint.x +collectionView.frame.origin.x;
int relativeX = (-1*contentOffset.x) +collectionView.frame.origin.x;

UICollectionView cell borders in a flow layout

I'm using UICollectionView to lay out a bunch of cells that are sectioned by first letter of their title. Each cell should have a very thin border around it, and the section headers should have borders above and below. Here's my current prototype:
I achieve the current appearance with the following rules:
Stroke the right and bottom edge of each cell.
Stroke the bottom edge of each section heading.
This is very close to what I want, but there are two defects:
If the line before a section heading isn't full, then the border along the top of the heading stops short of the right edge of the screen.
It's not visible in this screenshot, but if a line is full, the right border of the last cell in the line is still drawn, which looks a little odd against the edge of the screen.
My best idea to fix this is to somehow tell each cell if it's in the last row of a section or the last cell in a row; then the cell would turn off the offending borders, section headings would draw a top border as well as a bottom, and everything would be hunky-dory. I don't know how to achieve that, though.
Any thoughts on how to manage that, or another way to get the look I'm going for? I'm currently using a UICollectionViewFlowLayout.
I ended up subclassing UICollectionViewFlowLayout and applying several heuristics after the flow layout had calculated the attributes for each cell:
If center.y is equal to center.y of the last item in the section, the cell is in the last row of the section.
If CGRectGetMaxY(frame) is equal to CGRectGetMaxY(self.collectionView.bounds), then the cell is agains the right edge of the collection view.
I then stored the results of these calculations in a subclass of UICollectionViewLayoutAttributes, and wrote a UICollectionViewCell subclass whose -applyLayoutAttributes: method would adjust the borders its background view draws based on the additional properties.
I've put the whole mess into a fairly enormous gist so you can see exactly what I did. Happy hacking.
My best idea to fix this is to somehow tell each cell if it's in the last row of a section or the last cell in a row; then the cell would turn off the offending borders, section headings would draw a top border as well as a bottom, and everything would be hunky-dory. I don't know how to achieve that, though.
What you describe is more or less what I did in a similar scenario. I added a border property to my cell:
typedef NS_OPTIONS(NSInteger, TLGridBorder) {
TLGridBorderNone = 0,
TLGridBorderTop = 1 << 0,
TLGridBorderRight = 1 << 1,
TLGridBorderBottom = 1 << 2,
TLGridBorderLeft = 1 << 3,
TLGridBorderAll = TLGridBorderTop | TLGridBorderRight | TLGridBorderBottom | TLGridBorderLeft,
};
#interface TLGridCellView : UIView
#property (nonatomic) TLGridBorder border;
#end
Then I set the border in my view controller's cell configuration:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
TLGridCellView *cell = ...;
if (indexPath.item == self collectionView:collectionView numberOfItemsInSection:indexPath.section - 1) {
cell.border = TLGridBorderLeft;
} else {
cell.border = TLGridBorderLeft | TLGridBorderRight;
}
return cell;
}
I solve this problem in a simple way. I didn't add boarder to cell, instead I add a label with boarder into the cell. For the first column, the frame of the label is the same with the cell. For the other label, I set the x coordinate -0.5 to make their boarder overlap. Hope it helps.
Here is the code:
- (UICollectionViewCell *) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
// Then use it
UILabel *label = nil;
if (cell.contentView.subviews.count > 0) {
label = cell.contentView.subviews[0];
} else {
label = [[UILabel alloc] init];
}
label.text = #"北京";
[label setTextAlignment:NSTextAlignmentCenter];
[label setFont:[UIFont systemFontOfSize:14]];
[label setCenter:cell.contentView.center];
CGRect frame = label.frame;
if (indexPath.row%4 == 0) {
frame.origin.x = 0;
} else {
frame.origin.x = -0.5;
}
frame.origin.y = 0;
frame.size.width = self.collectionView.frame.size.width / 4;
frame.size.height = self.collectionView.frame.size.height / 9;
[label setFrame:frame];
if (cell.contentView.subviews.count == 0) {
[[cell contentView] addSubview:label];
}
label.layer.borderWidth = 0.5;
label.layer.borderColor = [[UIColor lightGrayColor] CGColor];
cell.backgroundColor = [UIColor whiteColor];
return cell;
}

Resources