How to avoid this infinite loop caused by invalidateIntrinsicContentSize - ios

This code causes an infinite loop when the view is put insida a tableviewcell. How do i use autolayout correctly without causing an infinite loop here?
#implementation SimpleStackView
#synthesize rowSpacing=_rowSpacing;
- (void)layoutSubviews
{
[super layoutSubviews];
[self sizeToFit];
[self invalidateIntrinsicContentSize];
CGFloat nextRowTop = 0;
for (UIView *view in self.subviews)
{
CGSize size = [view sizeThatFits:CGSizeMake(self.bounds.size.width, view.bounds.size.height)];
view.frame = CGRectMake(0, nextRowTop, self.bounds.size.width, size.height);
nextRowTop += view.frame.size.height + self.rowSpacing;
}
}
- (CGSize)sizeThatFits:(CGSize)size
{
CGFloat sumOfHeights = 0;
for (UIView *view in self.subviews) {
sumOfHeights += [view sizeThatFits:CGSizeMake(size.width, view.bounds.size.height)].height;
}
CGFloat sumOfRowSpacings = MAX(0, (int)self.subviews.count - 1) * self.rowSpacing;
return CGSizeMake(size.width, sumOfHeights + sumOfRowSpacings);
}
- (CGSize)intrinsicContentSize
{
CGFloat intrinsicHeight = [self sizeThatFits:self.bounds.size].height;
return CGSizeMake(UIViewNoIntrinsicMetric, intrinsicHeight);
}
#end

Related

UIScrollView - when is contentSize set

I have a UIViewController and it's view hierarchy looks like this:
UIView
UIScrollView
UIImageView
I have code that positions the image view in the middle of the scroll view's frame, like so:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self recenterContent:scrollView];
}
- (void)recenterContent:(UIScrollView *)scrollView {
//this centers the content when it is smaller than the scrollView's bounds
CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);
self.scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0.f, 0.f);
}
This works fine when zooming the content, but when the view controller first loads it does not center. This is because the scrollView.contentSize is always 0. So my question is - when should I call this method after the scrollView.contentSize is set? When does that get set?
I have tried in viewDidLayoutSubviews, and the bounds of the scroll view is set then, but not the content size. Is there some method that I can use where the scroll view will be guaranteed to have the content size set?
Or is there a better way to keep the image centered when it is smaller than the scroll view? What I am trying to accomplish is to have it so the image view is not at the top of the scroll view and what I am using works, except when the scroll view's content size is not set. But if there is a better way of doing this without having to adjust the contentInset, I would be fine with that too.
Update
Here is what I have currently.
It is almost working, but no matter what I try, I cannot get it to look correct when the view loads. The way it works now is that it starts out off-center because when it calls the recenterContent method, before the view is displayed the content size of the scroll view is CGSizeZero, so the calculations are wrong. But if I try to recenter the content after the view has been displayed, then there is a visible delay before it gets centered.
I am just confused as to when the contentSize of the scroll view is set if I am using AutoLayout constraints to specify the size.
Here is my code. Can anyone see anything wrong with it?
#interface MyImageViewController ()
#property (strong, nonatomic) UIScrollView *scrollView;
#property (strong, nonatomic) UIImageView *imageView;
#property (assign, nonatomic) BOOL needsZoomScale;
#end
#implementation MyImageViewController
- (void)loadView {
self.view = [[UIView alloc] init];
[self.view addSubview:self.scrollView];
[self.scrollView addSubview:self.imageView];
self.needsZoomScale = YES;
[NSLayoutConstraint activateConstraints:#[
[self.scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.scrollView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[self.scrollView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[self.imageView.leadingAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.leadingAnchor],
[self.imageView.topAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.topAnchor],
[self.imageView.trailingAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.trailingAnchor],
[self.imageView.bottomAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.bottomAnchor]
]];
}
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(doubleTapZoom:)];
doubleTapGesture.numberOfTapsRequired = 2;
[self.imageView addGestureRecognizer:doubleTapGesture];
}
- (CGRect)zoomRectForScrollView:(UIScrollView *)scrollView withScale:(CGFloat)scale withCenter:(CGPoint)center {
CGRect zoomRect;
//the zoom rect is in the content view's coordinates. At a zoom scale of 1.0, the zoom rect would be the size
//of the scroll view's bounds. As the zoom scale decreases, so more content is visible, the size of the rect
//grows.
zoomRect.size.width = scrollView.frame.size.width / scale;
zoomRect.size.height = scrollView.frame.size.height / scale;
//choose an origin so as to get the right center
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
return zoomRect;
}
- (void)doubleTapZoom:(UITapGestureRecognizer *)sender {
UIView *tappedView = sender.view;
CGPoint tappedPoint = [sender locationInView:tappedView];
if (tappedPoint.x <= 0) {
tappedPoint.x = 1;
}
if (tappedPoint.y <= 0) {
tappedPoint.y = 1;
}
if (tappedPoint.x >= tappedView.bounds.size.width) {
tappedPoint.x = tappedView.bounds.size.width - 1;
}
if (tappedPoint.y >= tappedView.bounds.size.height) {
tappedPoint.y = tappedView.bounds.size.height - 1;
}
CGFloat zoomScale;
if (self.scrollView.zoomScale < 1) {
zoomScale = 1;
} else if (self.scrollView.zoomScale < self.scrollView.maximumZoomScale) {
zoomScale = self.scrollView.maximumZoomScale;
} else {
zoomScale = self.scrollView.minimumZoomScale;
}
CGRect zoomRect = [self zoomRectForScrollView:self.scrollView withScale:zoomScale withCenter:tappedPoint];
[self.scrollView zoomToRect:zoomRect animated:YES];
}
- (UIScrollView *)scrollView {
if (!self->_scrollView) {
self->_scrollView = [[UIScrollView alloc] init];
self->_scrollView.translatesAutoresizingMaskIntoConstraints = NO;
self->_scrollView.minimumZoomScale = 0.1f;
self->_scrollView.maximumZoomScale = 4.0f;
self->_scrollView.bounces = YES;
self->_scrollView.bouncesZoom = YES;
self->_scrollView.delegate = self;
self->_scrollView.backgroundColor = [UIColor blackColor];
}
return self->_scrollView;
}
- (UIImageView *)imageView {
if (!self->_imageView) {
self->_imageView = [[UIImageView alloc] init];
self->_imageView.translatesAutoresizingMaskIntoConstraints = NO;
self->_imageView.userInteractionEnabled = YES;
}
return self->_imageView;
}
- (UIImage *)image {
return self.imageView.image;
}
- (void)setImage:(UIImage *)image {
self.imageView.image = image;
self.needsZoomScale = YES;
[self updateZoomScale];
}
- (void)updateZoomScale {
if (self.needsZoomScale && self.image) {
CGSize size = self.view.bounds.size;
if (size.width == 0.0f || size.height == 0.0f) {
return;
}
UIImage *image = self.image;
CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
if (imageSize.width > 0 && imageSize.height > 0) {
CGFloat widthScale = size.width / imageSize.width;
CGFloat heightScale = size.height / imageSize.height;
CGFloat minScale = MIN(widthScale, heightScale);
self.scrollView.minimumZoomScale = minScale;
self.scrollView.zoomScale = minScale;
self.needsZoomScale = NO;
}
}
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self updateZoomScale];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self recenterContent:self.scrollView];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self recenterContent:self.scrollView];
}
#pragma mark - UIScrollViewDelegate
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self recenterContent:scrollView];
}
- (void)recenterContent:(UIScrollView *)scrollView {
//this centers the content when it is smaller than the scrollView's bounds
CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);
self.scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0.f, 0.f);
}
#end
The problem is that a UIImageView has an intrinsic content size of 0,0 -- so your code is initially putting the a 0x0 image view at the center of the scroll view.
I've made a few changes to the code you posted... see comments (I "wrapped" the changes in
// ---------------------------------
comment lines:
#interface MyImageViewController : UIViewController <UIScrollViewDelegate>
#end
#interface MyImageViewController ()
#property (strong, nonatomic) UIScrollView *scrollView;
#property (strong, nonatomic) UIImageView *imageView;
#property (assign, nonatomic) BOOL needsZoomScale;
#end
#implementation MyImageViewController
- (void)loadView {
self.view = [[UIView alloc] init];
[self.view addSubview:self.scrollView];
[self.scrollView addSubview:self.imageView];
self.needsZoomScale = YES;
// ---------------------------------
// respect safe area
UILayoutGuide *g = [self.view safeAreaLayoutGuide];
// saves on a little typing
UILayoutGuide *sg = [self.scrollView contentLayoutGuide];
// ---------------------------------
[NSLayoutConstraint activateConstraints:#[
[self.scrollView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor],
[self.scrollView.topAnchor constraintEqualToAnchor:g.topAnchor],
[self.scrollView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor],
[self.scrollView.bottomAnchor constraintEqualToAnchor:g.bottomAnchor],
[self.imageView.leadingAnchor constraintEqualToAnchor:sg.leadingAnchor],
[self.imageView.topAnchor constraintEqualToAnchor:sg.topAnchor],
[self.imageView.trailingAnchor constraintEqualToAnchor:sg.trailingAnchor],
[self.imageView.bottomAnchor constraintEqualToAnchor:sg.bottomAnchor]
]];
}
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(doubleTapZoom:)];
doubleTapGesture.numberOfTapsRequired = 2;
[self.imageView addGestureRecognizer:doubleTapGesture];
}
- (CGRect)zoomRectForScrollView:(UIScrollView *)scrollView withScale:(CGFloat)scale withCenter:(CGPoint)center {
CGRect zoomRect;
//the zoom rect is in the content view's coordinates. At a zoom scale of 1.0, the zoom rect would be the size
//of the scroll view's bounds. As the zoom scale decreases, so more content is visible, the size of the rect
//grows.
zoomRect.size.width = scrollView.frame.size.width / scale;
zoomRect.size.height = scrollView.frame.size.height / scale;
//choose an origin so as to get the right center
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
return zoomRect;
}
- (void)doubleTapZoom:(UITapGestureRecognizer *)sender {
UIView *tappedView = sender.view;
CGPoint tappedPoint = [sender locationInView:tappedView];
if (tappedPoint.x <= 0) {
tappedPoint.x = 1;
}
if (tappedPoint.y <= 0) {
tappedPoint.y = 1;
}
if (tappedPoint.x >= tappedView.bounds.size.width) {
tappedPoint.x = tappedView.bounds.size.width - 1;
}
if (tappedPoint.y >= tappedView.bounds.size.height) {
tappedPoint.y = tappedView.bounds.size.height - 1;
}
CGFloat zoomScale;
if (self.scrollView.zoomScale < 1) {
zoomScale = 1;
} else if (self.scrollView.zoomScale < self.scrollView.maximumZoomScale) {
zoomScale = self.scrollView.maximumZoomScale;
} else {
zoomScale = self.scrollView.minimumZoomScale;
}
CGRect zoomRect = [self zoomRectForScrollView:self.scrollView withScale:zoomScale withCenter:tappedPoint];
[self.scrollView zoomToRect:zoomRect animated:YES];
}
- (UIScrollView *)scrollView {
if (!self->_scrollView) {
self->_scrollView = [[UIScrollView alloc] init];
self->_scrollView.translatesAutoresizingMaskIntoConstraints = NO;
self->_scrollView.minimumZoomScale = 0.1f;
self->_scrollView.maximumZoomScale = 4.0f;
self->_scrollView.bounces = YES;
self->_scrollView.bouncesZoom = YES;
self->_scrollView.delegate = self;
self->_scrollView.backgroundColor = [UIColor blackColor];
}
return self->_scrollView;
}
- (UIImageView *)imageView {
if (!self->_imageView) {
self->_imageView = [[UIImageView alloc] init];
self->_imageView.translatesAutoresizingMaskIntoConstraints = NO;
self->_imageView.userInteractionEnabled = YES;
}
return self->_imageView;
}
- (UIImage *)image {
return self.imageView.image;
}
- (void)setImage:(UIImage *)image {
self.imageView.image = image;
// ---------------------------------
// set the frame here
self.imageView.frame = CGRectMake(0.0, 0.0, image.size.width, image.size.height);
// ---------------------------------
// not needed ... unless maybe changing the image while view is showing?
//self.needsZoomScale = YES;
//[self updateZoomScale];
}
- (void)updateZoomScale {
if (self.needsZoomScale && self.image) {
CGSize size = self.view.bounds.size;
if (size.width == 0.0f || size.height == 0.0f) {
return;
}
UIImage *image = self.image;
CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
if (imageSize.width > 0 && imageSize.height > 0) {
CGFloat widthScale = size.width / imageSize.width;
CGFloat heightScale = size.height / imageSize.height;
CGFloat minScale = MIN(widthScale, heightScale);
self.scrollView.minimumZoomScale = minScale;
self.scrollView.zoomScale = minScale;
self.needsZoomScale = NO;
}
}
}
// ---------------------------------
// Don't need this
//- (void)viewWillLayoutSubviews {
// [super viewWillLayoutSubviews];
// [self updateZoomScale];
//}
// ---------------------------------
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// ---------------------------------
// update zoom scale here
[self updateZoomScale];
// ---------------------------------
[self recenterContent:self.scrollView];
}
// ---------------------------------
// Don't need this
//- (void)viewDidAppear:(BOOL)animated {
// [super viewDidAppear:animated];
// [self recenterContent:self.scrollView];
//}
// ---------------------------------
#pragma mark - UIScrollViewDelegate
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
[self recenterContent:scrollView];
}
- (void)recenterContent:(UIScrollView *)scrollView {
//this centers the content when it is smaller than the scrollView's bounds
CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);
self.scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0.f, 0.f);
}
#end
and here's how I call it:
MyImageViewController *vc = [MyImageViewController new];
UIImage *img = [UIImage imageNamed:#"bkg"];
if (nil == img) {
NSLog(#"Could not load image!!!!");
return;
}
[vc setImage:img];
[self.navigationController pushViewController:vc animated:YES];

iOS - layoutIfNeeded not creating frame in custom UITableViewCell

I have this class here for a custom tableview cell but in awakeFromNib, self.frame is always 0,0,0,0. I tried calling [self layoutIfNeeded] but that has no effect. I need the frame to place c in the right place in the cell. The code definitely runs (I've tried breakpoints), so why isn't it working?
#import "ChangeColourSubjectColourTableViewCell.h"
#import <QuartzCore/QuartzCore.h>
#define COLOUR_HEIGHT_DECIMAL 0.8
#define CORNER_RADIUS 6.0
#implementation ChangeColourSubjectColourTableViewCell
- (void)awakeFromNib {
// Initialization code
[self layoutIfNeeded];
//Colour View
//Size
CGRect rect;
rect.size.height = self.frame.size.height * COLOUR_HEIGHT_DECIMAL; //80% height
rect.size.width = rect.size.height;
//Position
CGFloat gap = self.frame.size.height * (1 - COLOUR_HEIGHT_DECIMAL) / 2;
rect.origin.y = gap;
rect.origin.x = self.frame.size.width - rect.size.width - gap;
UIView *c = [[UIView alloc] initWithFrame:rect];
[c setBackgroundColor:[UIColor blackColor]];
[c.layer setCornerRadius:CORNER_RADIUS];
[self addSubview:c];
_colourView = c;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
- (void)setColourViewColour:(UIColor *)colour {
[_colourView setBackgroundColor:colour];
}
#end
The awakeFromNib method is called when the cell is loaded from the xib, but the cell's frame is manage by the tableview.
You should create subviews as variables in method awakeFromNib and override method setFrame/layoutSubviews to layout your subview, the cell.frame in method layoutSubviews is always true.
you can do like this:
- (void)awakeFromNib {
// Initialization code
UIView *c = [[UIView alloc] initWithFrame:CGRectZero];
[c setBackgroundColor:[UIColor blackColor]];
[c.layer setCornerRadius:CORNER_RADIUS];
[self addSubview:c];
_colourView = c;
}
- (void)layoutSubviews {
[super layoutSubviews];
//Colour View
//Size
CGRect rect;
rect.size.height = self.frame.size.height * COLOUR_HEIGHT_DECIMAL; //80% height
rect.size.width = rect.size.height;
//Position
CGFloat gap = self.frame.size.height * (1 - COLOUR_HEIGHT_DECIMAL) / 2;
rect.origin.y = gap;
rect.origin.x = self.frame.size.width - rect.size.width - gap;
_colourView.frame = rect;
}
In iOS6 or later, you can use
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
[cell reloadUI];// you can do like this
}
// cell
- (void) reloadUI {
CGRect rect;
rect.size.height = self.frame.size.height * COLOUR_HEIGHT_DECIMAL; //80% height
rect.size.width = rect.size.height;
//Position
CGFloat gap = self.frame.size.height * (1 - COLOUR_HEIGHT_DECIMAL) / 2;
rect.origin.y = gap;
rect.origin.x = self.frame.size.width - rect.size.width - gap;
_colourView.frame = rect;
}

Add hidden UIView to UITableView similar to UIRefreshControl

Very similar to a UIRefreshControl, I'm trying to put a UIView on top of a UITableView. Dragging down the table view should reveal the view and have it stay there. Dragging up should hide it again and then scroll the table view. When hidden the table view should scroll normally. Scrolling back to the top should either reveal the hidden view again, or snap to the hidden state. Ultimately the revealed view should contain some buttons or a segmented control. It should look and behave very similar to the OmniFocus App.
Hidden View in OmniFocus
Revealed View in OmniFocus
This is how far I got. Especially the snapping back to the hidden state when the table view scrolls back up does not work. If you time it right you'll end up stuck in the middle of top view, which is exactly not what I want.
static CGFloat const kTopViewHeight = 40;
#interface ViewController ()
#property (nonatomic, weak) UIView *topView;
#property (nonatomic, assign) CGFloat dragStartY;
#end
#implementation ViewController
#pragma mark - View Lifecycle
- (void)viewDidLoad
{
CGRect topViewFrame = CGRectMake(0.0, -kTopViewHeight, 320, kTopViewHeight);
UIView *myView = [[UIView alloc] initWithFrame:topViewFrame];
myView.backgroundColor = [UIColor greenColor]; // DEBUG
self.topView = myView;
[self.tableView addSubview:myView];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
NSLog(#"scrollViewWillBeginDragging %#", NSStringFromCGPoint(scrollView.contentOffset));
self.dragStartY = scrollView.contentOffset.y;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSLog(#"scrollViewDidScroll %#", NSStringFromCGPoint(scrollView.contentOffset));
if (scrollView.contentOffset.y > 0) {
// reset the inset
scrollView.contentInset = UIEdgeInsetsZero;
} else if (scrollView.contentOffset.y >= -kTopViewHeight) {
// set the inset for the section headers
scrollView.contentInset = UIEdgeInsetsMake(-scrollView.contentOffset.y, 0, 0, 0);
} else if (scrollView.contentOffset.y < -kTopViewHeight) {
// don't scroll further when the topView's height is reached
scrollView.contentInset = UIEdgeInsetsMake(-kTopViewHeight, 0, 0, 0);
scrollView.contentOffset = CGPointMake(0, -kTopViewHeight);
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
NSLog(#"scrollViewDidEndDragging %#", NSStringFromCGPoint(scrollView.contentOffset));
CGFloat yOffset = scrollView.contentOffset.y;
if (yOffset < 0) {
BOOL dragDown = self.dragStartY > yOffset;
CGFloat dragThreshold = 10;
if (dragDown) {
if (yOffset <= -dragThreshold) {
[self snapDown:YES scrollView:scrollView];
} else {
[self snapDown:NO scrollView:scrollView];
}
} else if (!dragDown) {
if (yOffset >= dragThreshold - kTopViewHeight) {
[self snapDown:NO scrollView:scrollView];
} else {
[self snapDown:YES scrollView:scrollView];
}
}
}
}
- (void)snapDown:(BOOL)down scrollView:(UIScrollView *)scrollView
{
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionCurveEaseOut|UIViewAnimationOptionBeginFromCurrentState
animations:^{
if (down) {
// snap down
scrollView.contentOffset = CGPointMake(0, -kTopViewHeight);
scrollView.contentInset = UIEdgeInsetsMake(kTopViewHeight, 0, 0, 0);
} else {
// snap up
scrollView.contentOffset = CGPointMake(0, 0);
scrollView.contentInset = UIEdgeInsetsZero;
}
}
completion:nil];
}
//paging for data you can use this spinner
spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[spinner stopAnimating];
spinner.hidesWhenStopped = NO;
spinner.frame = CGRectMake(0, 0, 320, 44);
tblView.tableFooterView = spinner;
tblView.tableFooterView.hidden = YES;
#pragma mark Table pull to refresh data....
- (void)scrollViewDidEndDragging:(UIScrollView *)aScrollView
willDecelerate:(BOOL)decelerate{
CGPoint offset = aScrollView.contentOffset;
CGRect bounds = aScrollView.bounds;
CGSize size = aScrollView.contentSize;
UIEdgeInsets inset = aScrollView.contentInset;
float y = offset.y + bounds.size.height - inset.bottom;
float h = size.height;
float reload_distance = 50;
if(y > h + reload_distance && _nextPage) {
NSLog(#"load more data");
tblView.tableFooterView.hidden = NO;
// index for new page of data will increment here
index = index + 1;
[spinner startAnimating];
[self performSelector:#selector(requestData) withObject:nil afterDelay:1.0f];
}
}
// when you request for data and wants to stop spinner
CGPoint offset = tblView.contentOffset;
// if new page is there set bool
_nextPage = YES;
// want to remove spinner
tblView.tableFooterView.hidden = YES;
[spinner stopAnimating];
[tblView setContentOffset:offset animated:NO];

Creating a stretchy UICollectionView like Evernote on iOS 7

I've been working on trying to recreate the stretchy collection view that Evernote uses in iOS 7 and I'm really close to having it working. I've managed to create a custom collection view flow layout that modifies the layout attribute transforms when the content offset y value lies outside collection view bounds. I'm modifying the layout attributes in the layoutAttributesForElementsInRect method and it behaves as expected except that the bottom cells can disappear when you hit the bottom of the scroll view. The further you pull the content offset the more cells can disappear. I think the cells basically get clipped off. It doesn't happen at the top though and I'd expect to see the same behavior in both places. Here's what my flow layout implementation looks like right now.
#implementation CNStretchyCollectionViewFlowLayout
{
BOOL _transformsNeedReset;
CGFloat _scrollResistanceDenominator;
}
- (id)init
{
self = [super init];
if (self)
{
// Set up the flow layout parameters
self.minimumInteritemSpacing = 10;
self.minimumLineSpacing = 10;
self.itemSize = CGSizeMake(320, 44);
self.sectionInset = UIEdgeInsetsMake(10, 0, 10, 0);
// Set up ivars
_transformsNeedReset = NO;
_scrollResistanceDenominator = 800.0f;
}
return self;
}
- (void)prepareLayout
{
[super prepareLayout];
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
// Set up the default attributes using the parent implementation
NSArray *items = [super layoutAttributesForElementsInRect:rect];
// Compute whether we need to adjust the transforms on the cells
CGFloat collectionViewHeight = self.collectionViewContentSize.height;
CGFloat topOffset = 0.0f;
CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height;
CGFloat yPosition = self.collectionView.contentOffset.y;
// Update the transforms if necessary
if (yPosition < topOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = topOffset - yPosition;
NSLog(#"Stretching Top by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromTop = item.center.y;
CGFloat scrollResistance = distanceFromTop / 800.0f;
item.transform = CGAffineTransformMakeTranslation(0, -stretchDelta + (stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (yPosition > bottomOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = yPosition - bottomOffset;
NSLog(#"Stretching bottom by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromBottom = collectionViewHeight - item.center.y;
CGFloat scrollResistance = distanceFromBottom / 800.0f;
item.transform = CGAffineTransformMakeTranslation(0, stretchDelta + (-stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (_transformsNeedReset)
{
NSLog(#"Resetting transforms");
_transformsNeedReset = NO;
for (UICollectionViewLayoutAttributes *item in items)
item.transform = CGAffineTransformIdentity;
}
return items;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
// Compute whether we need to adjust the transforms on the cells
CGFloat collectionViewHeight = self.collectionViewContentSize.height;
CGFloat topOffset = 0.0f;
CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height;
CGFloat yPosition = self.collectionView.contentOffset.y;
// Handle cases where the layout needs to be rebuilt
if (yPosition < topOffset)
return YES;
else if (yPosition > bottomOffset)
return YES;
else if (_transformsNeedReset)
return YES;
return NO;
}
#end
I also zipped up the project for people to try out. Any help would be greatly appreciated as I'm pretty new to creating custom collection view layouts. Here's the link to it:
https://dl.dropboxusercontent.com/u/2975688/StackOverflow/stretchy_collection_view.zip
Thanks everyone!
I was able to solve the problem. I'm not sure if there's actually a bug in iOS or not, but the issue was that the cells were actually getting translated outside the content view of the collection view. Once the cell would get translated far enough, it would get clipped off. I find it interesting that this does not happen in the simulator for non-retina displays, but does with retina displays which is why I feel this may actually be a bug.
With that in mind, a workaround for now is to add padding to the top and bottom of the collection view by overriding the collectionViewContentSize method. Once you do this, if you add padding to the top, you need to adjust the layout attributes for the cells as well so they are in the proper location. The final step is to set the contentInset on the collection view itself to adjust for the padding. Leave the scroll indicator insets alone since those are fine. Here's the implementation of my final collection view controller and the custom flow layout.
CNStretchyCollectionViewController.m
#implementation CNStretchyCollectionViewController
static NSString *CellIdentifier = #"Cell";
- (void)viewDidLoad
{
// Register the cell
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:CellIdentifier];
// Tweak out the content insets
CNStretchyCollectionViewFlowLayout *layout = (CNStretchyCollectionViewFlowLayout *) self.collectionViewLayout;
self.collectionView.contentInset = layout.bufferedContentInsets;
// Set the delegate for the collection view
self.collectionView.delegate = self;
self.collectionView.clipsToBounds = NO;
// Customize the appearance of the collection view
self.collectionView.backgroundColor = [UIColor whiteColor];
self.collectionView.indicatorStyle = UIScrollViewIndicatorStyleDefault;
}
#pragma mark - UICollectionViewDataSource Methods
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 20;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
if ([indexPath row] % 2 == 0)
cell.backgroundColor = [UIColor orangeColor];
else
cell.backgroundColor = [UIColor blueColor];
return cell;
}
#end
CNStretchyCollectionViewFlowLayout.m
#interface CNStretchyCollectionViewFlowLayout ()
- (CGSize)collectionViewContentSizeWithoutOverflow;
#end
#pragma mark -
#implementation CNStretchyCollectionViewFlowLayout
{
BOOL _transformsNeedReset;
CGFloat _scrollResistanceDenominator;
UIEdgeInsets _contentOverflowPadding;
}
- (id)init
{
self = [super init];
if (self)
{
// Set up the flow layout parameters
self.minimumInteritemSpacing = 10;
self.minimumLineSpacing = 10;
self.itemSize = CGSizeMake(320, 44);
self.sectionInset = UIEdgeInsetsMake(10, 0, 10, 0);
// Set up ivars
_transformsNeedReset = NO;
_scrollResistanceDenominator = 800.0f;
_contentOverflowPadding = UIEdgeInsetsMake(100.0f, 0.0f, 100.0f, 0.0f);
_bufferedContentInsets = _contentOverflowPadding;
_bufferedContentInsets.top *= -1;
_bufferedContentInsets.bottom *= -1;
}
return self;
}
- (void)prepareLayout
{
[super prepareLayout];
}
- (CGSize)collectionViewContentSize
{
CGSize contentSize = [super collectionViewContentSize];
contentSize.height += _contentOverflowPadding.top + _contentOverflowPadding.bottom;
return contentSize;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
// Set up the default attributes using the parent implementation (need to adjust the rect to account for buffer spacing)
rect = UIEdgeInsetsInsetRect(rect, _bufferedContentInsets);
NSArray *items = [super layoutAttributesForElementsInRect:rect];
// Shift all the items down due to the content overflow padding
for (UICollectionViewLayoutAttributes *item in items)
{
CGPoint center = item.center;
center.y += _contentOverflowPadding.top;
item.center = center;
}
// Compute whether we need to adjust the transforms on the cells
CGFloat collectionViewHeight = [self collectionViewContentSizeWithoutOverflow].height;
CGFloat topOffset = _contentOverflowPadding.top;
CGFloat bottomOffset = collectionViewHeight - self.collectionView.frame.size.height + _contentOverflowPadding.top;
CGFloat yPosition = self.collectionView.contentOffset.y;
// Update the transforms if necessary
if (yPosition < topOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = topOffset - yPosition;
NSLog(#"Stretching Top by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromTop = item.center.y - _contentOverflowPadding.top;
CGFloat scrollResistance = distanceFromTop / _scrollResistanceDenominator;
item.transform = CGAffineTransformMakeTranslation(0, -stretchDelta + (stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (yPosition > bottomOffset)
{
// Compute the stretch delta
CGFloat stretchDelta = yPosition - bottomOffset;
NSLog(#"Stretching bottom by: %f", stretchDelta);
// Iterate through all the visible items for the new bounds and update the transform
for (UICollectionViewLayoutAttributes *item in items)
{
CGFloat distanceFromBottom = collectionViewHeight + _contentOverflowPadding.top - item.center.y;
CGFloat scrollResistance = distanceFromBottom / _scrollResistanceDenominator;
item.transform = CGAffineTransformMakeTranslation(0, stretchDelta + (-stretchDelta * scrollResistance));
}
// Update the ivar for requiring a reset
_transformsNeedReset = YES;
}
else if (_transformsNeedReset)
{
NSLog(#"Resetting transforms");
_transformsNeedReset = NO;
for (UICollectionViewLayoutAttributes *item in items)
item.transform = CGAffineTransformIdentity;
}
return items;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
#pragma mark - Private Methods
- (CGSize)collectionViewContentSizeWithoutOverflow
{
return [super collectionViewContentSize];
}
#end
CNStretchyCollectionViewFlowLayout.h
#interface CNStretchyCollectionViewFlowLayout : UICollectionViewFlowLayout
#property (assign, nonatomic) UIEdgeInsets bufferedContentInsets;
#end
I'm actually going to through this onto Github and I'll post a link to the project once it's up. Thanks again everyone!

Multiline UILabel with padding in autolayout, in UITableView

I have a UILabel in a UITableView that should be a maximum of 2 lines and have a bit of padding around it (7 left and right and 2 top and bottom). I'm using autolayout and targeting only iOS6 and above. All views are being created and added programatically.
I've subclassed my UILabel, here's the init method:
- (id)init
{
self = [super init];
self.translatesAutoresizingMaskIntoConstraints = NO;
self.numberOfLines = 2;
self.backgroundColor = UIColorFromARGB(0x99000000);
self.textColor = [UIColor whiteColor];
self.font = [UIFont boldSystemFontOfSize:14.0f];
return self;
}
If I add this in, I get the right padding, but it makes it just one line:
- (void)drawTextInRect:(CGRect)rect {
UIEdgeInsets insets = {2, 7, 2, 7};
return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)];
}
I've seen this answer a few times, but it doesn't work for me (no effect):
- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines
{
UIEdgeInsets insets = {2, 7, 2, 7};
return [super textRectForBounds:UIEdgeInsetsInsetRect(bounds,insets) limitedToNumberOfLines:numberOfLines];
}
Does it make a difference that its in a table view? Any help would be appreciated.
As you saw in my above comment, you didn't really say what it's doing that you don't expect. I've just done this myself, and this works with autolayout:
#implementation InsetLabel
- (void) setInsets:(UIEdgeInsets)insets
{
_insets = insets ;
[self invalidateIntrinsicContentSize] ;
}
- (void)drawTextInRect:(CGRect)rect
{
return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, self.insets)];
}
- (void)resizeHeightToFitText
{
CGRect frame = [self bounds];
CGFloat textWidth = frame.size.width - (self.insets.left + self.insets.right);
CGSize newSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(textWidth, 1000000) lineBreakMode:self.lineBreakMode];
frame.size.height = newSize.height + self.insets.top + self.insets.bottom;
self.frame = frame;
}
- (CGSize) intrinsicContentSize
{
CGSize superSize = [super intrinsicContentSize] ;
superSize.height += self.insets.top + self.insets.bottom ;
superSize.width += self.insets.left + self.insets.right ;
return superSize ;
}
#end
#implementation InsetLabel
- (void) setInsets:(UIEdgeInsets)insets
{
_insets = insets ;
[self invalidateIntrinsicContentSize] ;
}
- (void)drawTextInRect:(CGRect)rect
{
return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, self.insets)];
}
- (void)resizeHeightToFitText
{
CGRect frame = [self frame];
CGFloat textWidth = frame.size.width - (self.insets.left + self.insets.right);
CGSize newSize = [self.text sizeWithFont:self.font constrainedToSize:CGSizeMake(textWidth, 1000000) lineBreakMode:self.lineBreakMode];
frame.size.height = newSize.height + self.insets.top + self.insets.bottom;
self.frame = frame;
}
- (CGSize) intrinsicContentSize
{
CGSize superSize = [super intrinsicContentSize] ;
superSize.height += self.insets.top + self.insets.bottom ;
superSize.width += self.insets.left + self.insets.right ;
return superSize ;
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self resizeHeightToFitText];
}
This works even better imho.

Resources