Adjust UICollectionView scrollOffset in intervals of the cell width - ios

I have a collection view as such (scrolls sideways):
When the user scrolls, I want to adjust the table so it lines up perfectly around the static square.
I decided to do this using the scrollView when it ends dragging. However it is always just a little but off or at the very end completely off.
Where are my calculations going wrong?
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
if (![scrollView isKindOfClass:[IndexedCollectionView class]]) return;
IndexedCollectionView *collectionView = (IndexedCollectionView *)scrollView;
NSMutableArray *data = self.sectionsData[collectionView.index];
int cellsCount = data.count;
if (cellsCount==0) {
cellsCount++;
}
//Adjust cells so they line up correctly
CGPoint adjustedOffset = scrollView.contentOffset;
long interval = scrollView.contentSize.width/cellsCount;
long remainder = (long)adjustedOffset.x%interval;
long adjustment = interval-remainder;
adjustedOffset.x +=adjustment;
[UIView animateWithDuration:.3 animations:^{
[scrollView setContentOffset:adjustedOffset];
}];
NSInteger index = collectionView.index;
self.contentOffsetDictionary[[#(index) stringValue]] = #(adjustedOffset.x);
}

Related

Change UICollectionView cell size with custom Flow Layout

I have five rows of cells each row containing four cells - 5x4. I am trying to accomplish almost an Apple watch like effect. The row in the center of the screen should have cell sizes of 100x100 and the rest return sizes of 80x80. When scrolled, the row moving away from the center should turn to 80x80 and the row moving into the center should turn 100x100.
I have implemented prepareLayout, layoutAttributesForElementsInRect, and layoutAttributesForItemAtIndexPath
So right now I have a center row of 100x100 cells and the rest 80x80.
To make it dynamic I implement shouldInvalidateLayoutForBoundsChange but nothing happens.
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:self.layoutInfo.count];
[self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSString *elementIdentifier,
NSDictionary *elementsInfo,
BOOL *stop) {
[elementsInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath,
UICollectionViewLayoutAttributes *attributes,
BOOL *innerStop) {
// NSLog(#"%f, %f", newBounds.origin.x, newBounds.origin.y);
if ((newBounds.origin.y < [[UIScreen mainScreen]bounds].size.height/2-50) && (newBounds.origin.y > [[UIScreen mainScreen]bounds].size.height/2+50)) {
CGRect frame = attributes.frame;
frame.size.width = 100.0f;
frame.size.height = 100.0f;
attributes.frame = frame;
}else{
CGRect frame = attributes.frame;
frame.size.width = 80.0f;
frame.size.height = 80.0f;
attributes.frame = frame;
}
[allAttributes addObject:attributes];
}];
}];
return YES;
}
shouldInvalidateLayoutForBoundsChange: is called pretty frequently (whenever the view is resized, or scrolled...), so you probably don't need to be doing all that work there. If you know your cells will always have a fixed size and position then you can just return NO here.
As long as you have implemented prepareLayout, layoutAttributesForElementsInRect, and layoutAttributesForItemAtIndexPath, then make sure you also implement collectionViewContentSize to return the total size of the content.

How can I keep the same collection view cells on the screen when zooming a UICollectionView?

I have a UICollectionView that uses a UICollectionViewLayout subclass for its layout. The layout is a simple grid.
When I zoom the collection view in or out, the positions of the cells on the screen change. In some cases, when zooming in, cells move off the screen entirely. I zoom the cells with a pinch gesture recognizer that sends x and y scale values to the layout class and then invalidates the layout.
As the cells get bigger, they move because their origins are calculated relative to the 0,0 position of the collection view.
I want to be able to zoom the collection view in, while having as many of the cells that were originally on the screen stay there. A good solution would be to have the cell in the center of the screen stay in the center as it becomes larger. Cells around the center cell would grow, and that might push them off the screen.
I've tried adjusting the collection view's content offset, but I haven't achieved what I want. I'm not quite sure how to calculate its new value, and I've learned that the changes caused by invalidateLayout do not happen immediately.
I tried a key value observer for the collection view's content size, but that caused stuttering because the changes in the KVO method happened well after the original zooming.
I've also worked a little bit with scrollToItemAtIndexPath, but the code in my full app is not guaranteed to have a cell at the exact center of the screen. That solution is less desirable for me.
Here is the code where the pinch recognizer sends changes to the layout class:
[self.gridLayout updateCellWidthScale:xScale];
[self.gridLayout updateCellHeightScale:yScale];
[self.gridLayout invalidateLayout];
Here is the code in the layout class
(numberOfRows and numberOfColumns are both set to 20):
-(id)initWithNumberOfRows:(NSUInteger)numberOfRows
andNumberOfColumns:(NSUInteger)numberOfColumns
{
self = [super init];
if (self)
{
_numberOfRows = numberOfRows;
_numberOfColumns = numberOfColumns;
_cellWidth = 80.0f;
_cellHeight = 80.0f;
_cellWidthScale = 1.0f;
_cellHeightScale = 1.0f;
}
return self;
}
-(void)updateCellWidthScale:(CGFloat)newWidthScale
{
self.cellWidthScale *= newWidthScale;
}
-(void)updateCellHeightScale:(CGFloat)newHeightScale
{
self.cellHeightScale *= newHeightScale;
}
-(CGSize)collectionViewContentSize
{
CGSize returnValue = CGSizeMake(self.numberOfColumns * self.cellWidth * self.cellWidthScale,
self.numberOfRows * self.cellHeight * self.cellHeightScale);
return returnValue;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path
{
UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
CGRect rect = [self frameForItemAtIndexPath:path];
attributes.size = CGSizeMake(rect.size.width, rect.size.height);
attributes.center = CGPointMake(rect.origin.x + (0.5f * rect.size.width),
rect.origin.y + (0.5f * rect.size.height));
return attributes;
}
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *returnValue = [[NSMutableArray alloc] init];
for (NSInteger i=0; i < self.numberOfRows; i++)
{
for (NSInteger j=0; j < self.numberOfColumns; j++)
{
NSIndexPath* indexPath = [NSIndexPath indexPathForItem:j inSection:i];
CGRect frame = [self frameForItemAtIndexPath:indexPath];
if (CGRectIntersectsRect(frame, rect))
{
[returnValue addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
}
}
}
return returnValue;
}
- (CGRect)frameForItemAtIndexPath:(NSIndexPath *)indexPath
{
CGRect returnValue = CGRectMake(indexPath.section * self.cellWidth * self.cellWidthScale,
indexPath.row * self.cellHeight * self.cellHeightScale,
self.cellWidth * self.cellWidthScale,
self.cellHeight * self.cellHeightScale);
return returnValue;
}
You need to set your collectionView contentOffset to the value it was before starting to zoom multiplied the gestures scale.
Your pinch recognizer method should look like this (you need to add some more code to stop changing contentOffset when reaching the MAXIMUM_SCALE or MINIMUM_SCALE).
- (void)didReceivePinchGesture:(UIPinchGestureRecognizer*)gesture
{
static CGFloat scaleStart;
static CGPoint p;
if (gesture.state == UIGestureRecognizerStateBegan)
{
scaleStart = self.scale;
p = self.collectionView.contentOffset;
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
CGFloat tempScale = scaleStart * gesture.scale;
if (tempScale < MINMUM_SCALE)
{
self.scale = MINMUM_SCALE;
}
else if (tempScale > MAXIMUM_SCALE)
{
self.scale = MAXIMUM_SCALE;
}
else
{
self.scale = tempScale ;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView.collectionViewLayout invalidateLayout];
self.collectionView.contentOffset = CGPointMake(0, p.y * gesture.scale);
});
}
}

UITableView bigger than screen inside UIPageView

Lately I'm pulling my hair out because of this. I'm still a rookie iOS Developer therefore a simple solution would be appreciated.
I have a UIPageViewController with three views, the middle view is a UITableView which i populate with data programmatically and try to make a layout like a grid. When the UITableView has too many data the information gets outside of the screen.
I've tried to disable auto layout and add the UITableView to a UIScrollView, enabling the UITableView to scroll horizontal. I've managed to do that but somehow the interaction is messy and most of the times when trying to horizontal scroll the table it catches the UIPageViewController interaction. Also the vertical scroll of the table does not respond well too.
Is there a known solution for this situation?
After testing a few things I ended up with a acceptable solution using autolayout.
So first I added a UIScrollView and added the UITableView inside the UIScrollView.
I've enabled the Paging and disabled the Bounce Horizontally in the UIScrollView.
In the UITableView I've basically disabled everything relative to bouncing paging and scrolling.
Since the data is being populated programmatically to be some sort of a grid I have to know the total width of the longest row for that I've created a few methods to calculate the maximum width and arrange the labels.
I also created constraints for the table Width and Height which I calculate and update after all the calculations are done.
-(void) ArrangeTable
{
for (int i= 0; i < [arrayTable count]; i++) {
for (int j=0; j< [[arrayTable objectAtIndex:i ] count]; j++) {
UILabel * nameLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 8.0, 95.0, 30.0)];
[nameLabel setText:[NSString stringWithFormat:#"%#",[[arrayTable objectAtIndex:i] objectAtIndex:j]]];
[nameLabel sizeToFit];
float widthIs = [nameLabel.text
boundingRectWithSize:nameLabel.frame.size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{ NSFontAttributeName:nameLabel.font }
context:nil].size.width;
[self alignLabels:j currentWidth:[NSNumber numberWithFloat: widthIs]];
}
}
}
-(void) alignLabels:(int)colNumber currentWidth:(NSNumber*)labelWidth
{
if(colNumber >= [arrayLabelWidth count])
{
//Insert without position
[arrayLabelWidth addObject:labelWidth];
}else if ([labelWidth floatValue] > [[arrayLabelWidth objectAtIndex:colNumber] floatValue] )
{
[arrayLabelWidth replaceObjectAtIndex:colNumber withObject:labelWidth];
}
}
-(void) setMaxRowWidth
{
float widthTV =[[arrayLabelWidth valueForKeyPath:#"#sum.self"] floatValue] + ([arrayLabelWidth count]) * 10;
CGRect screenRect = [[UIScreen mainScreen] bounds];
float heightTV = [self tableViewHeight];
if(widthTV > screenRect.size.width)
{
tvWidth.constant = widthTV;
//svWidth.constant = valueTV;
}else{
if (UIDeviceOrientationIsPortrait(self.interfaceOrientation)){
//DO Portrait
tvWidth.constant = screenRect.size.width;
}else{
//DO Landscape
float calc = screenRect.size.width - self.view.frame.size.width;
tvWidth.constant = screenRect.size.width - calc;
}
}
tvHeight.constant = heightTV;
}
- (CGFloat)tableViewHeight
{
[tablePod layoutIfNeeded];
return [tablePod contentSize].height;
}

UIKit Dynamics: Attachment inside UITableViewCell

My table view cells contain a circle in an UIView, indicating a value. I want to add the UIKit Dynamics attachment behaviour to that circle in order to for it to lag a bit when scrolling.
I don't want to attach the individual cells to each other but only the circle view to the UITableViewCell. The rest of the cell should scroll as usual.
Problem: The UITableViewCell has its origin always at (0, 0). How can I add the circle to a view that actually does move when scrolling?
I finally got it to work. The UITableView moves the coordinate system of every cell and of all views contained within that cell. Therefor I needed to manually move my view inside the UITableViewCell during scrolling while still referring to the initial anchor point.
The table view controller:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
BOOL scrollingUp = '\0';
if (self.lastContentOffset > scrollView.contentOffset.y) {
scrollingUp = YES;
}
else if (self.lastContentOffset < scrollView.contentOffset.y) {
scrollingUp = NO;
}
NSInteger offset = 64; // To compensate for the navigation bar.
if (scrollingUp) {
offset = offset - scrollView.contentOffset.y;
}
else {
offset = offset + scrollView.contentOffset.y;
}
// Limit the offset so the views will not disappear during fast scrolling.
if (offset > 10) {
offset = 10;
}
else if (offset < -10) {
offset = -10;
}
// lastContentOffset is an instance variable.
self.lastContentOffset = scrollView.contentOffset.y;
for (UITableViewCell *cell in self.tableView.visibleCells) {
// Use CoreAnimation to prohibit flicker.
[UIView beginAnimations:#"Display notification" context:nil];
[UIView setAnimationDuration:0.5f];
[UIView setAnimationBeginsFromCurrentState:YES];
cell.view.frame = CGRectMake(cell.view.frame.origin.x, offset, cell.view.frame.size.width, cell.view.frame.size.height);
[UIView commitAnimations];
[cell.dynamicAnimator updateItemUsingCurrentState:cell.view];
}
}
The table view cell:
-(void)layoutSubviews {
[super layoutSubviews];
// _view is the animated UIView.
UIDynamicItemBehavior *viewBehavior = [[UIDynamicItemBehavior alloc] initWithItems:#[_view]];
viewBehavior.elasticity = 0.9f;
UIAttachmentBehavior *attachmentBehaviorView = [[UIAttachmentBehavior alloc] initWithItem:_view attachedToAnchor:CGPointMake(_anchorView.frame.origin.x + _anchorView.frame.size.width / 2.0f, _anchorView.frame.origin.y + _anchorView.frame.size.height / 2.0f)];
attachmentBehaviorView.damping = 8.0f;
attachmentBehaviorView.frequency = 4.0f;
attachmentBehaviorView.length = 0.0f;
[_dynamicAnimator addBehavior:viewBehavior];
[_dynamicAnimator addBehavior:attachmentBehaviorView];
}
You can change the anchorPoint of UIAttachmentBehavior during -[scrollViewDidScroll:]. You may refer to the following code snippet:
- (void)viewDidLoad
{
UIDynamicAnimator *animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
UIAttachmentBehavior *behavior1 = [[UIAttachmentBehavior alloc] initWithItem:self.circleView
attachedToAnchor:[self tableViewAnchor]];
behavior1.length = 10.0;
behavior1.damping = 0.3;
behavior1.frequency = 2.5;
[animator addBehavior:behavior1];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
behavior1.anchorPoint = [self.tableView convertPoint:[self tableViewAnchor] toView:self.view];
}
- (CGPoint)tableViewAnchor
{
return CGPointMake(160.0, 154.0); // return your target coordination w.r.t. the table view
}
Preview:

UIScrollView with subView Zooming

I Want to implement a scrollview that has several UIViews inside of it. The left-most item needs to be larger than the rest of the items. So my problem is this. Whenever an item is leaving the screen to the left (its origin.x is less than 15), I need to scale the item down from 470x440 to 235x220 pixels. This is fairly simple to implement. The problem is that the item that is moved to the left of pixel 480 needs to be zoomed in from 235x220 pixels to 470x440 pixels AND it needs to be moved to the left by 235 pixels (so as to not cover the item to its right, but rather move into the space that the leaving element left when it "shrunk".
I have tried a few different approaches to this, but I cannot the the animation to look good, and there is a bunch of glitches here and there.
Does anyone have any idea how I might go about implementing this type of feature ? Note that I do not want to zoom, but I want to resize the elements inside the scroll view in such a way that the left-most element (that is visible on the screen) is double the size of the other elements.
In case anyone else might be interested, I ended up with the following inside scrollViewDidScroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
float contentOffset = scrollView.contentOffset.x;
int leavingElementIndex = [_scrollView indexOfElementLeavingScene:scrollView.contentOffset.x];
int entereingElementIndex = leavingElementIndex + 1;
if (leavingElementIndex >= 0 && contentOffset > 0) {
CGRect leavingFrame = [[[scrollView subviews] objectAtIndex:leavingElementIndex] frame];
CGRect enteringFrame = [[[scrollView subviews] objectAtIndex:entereingElementIndex] frame];
float scalePerentage = (contentOffset - (_scrollView.smallBoxWidth * leavingElementIndex))/(_scrollView.smallBoxWidth);
enteringFrame.size.width = _scrollView.smallBoxWidth + (_scrollView.smallBoxWidth * scalePerentage);
enteringFrame.size.height = _scrollView.smallBoxHeight + (_scrollView.smallBoxHeight * scalePerentage);
enteringFrame.origin.x = [_scrollView leftMostPointAt:entereingElementIndex] - (_scrollView.smallBoxWidth * scalePerentage);
[[[scrollView subviews] objectAtIndex:entereingElementIndex] setFrame:enteringFrame];
leavingFrame.size.width = _scrollView.largeBoxWidth - (_scrollView.smallBoxWidth * scalePerentage);
leavingFrame.size.height = _scrollView.largeBoxHeight - (_scrollView.smallBoxHeight * scalePerentage);
[[[scrollView subviews] objectAtIndex:leavingElementIndex] setFrame:leavingFrame];
//Reset the other visible frames sizes
int index = 0;
for (UIView *view in [scrollView subviews]) {
if([view isKindOfClass:[SlidingView class]] && index > entereingElementIndex) {
CGRect frame = view.frame;
frame.size.width = _scrollView.smallBoxWidth;
frame.size.height = _scrollView.smallBoxHeight;
frame.origin.x = [_scrollView leftMostPointAt:index];
[view setFrame:frame];
}
index++;
}
}
}
This is what it looks like in the end:
End Result http://stuff.haagen.name/iOS%20scroll%20resize.gif

Resources