I am trying to layout a UICollectionView like the mock-up I have drawn in the photo(also showing the index of each item):
The code I currently have to try and achieve this is:
In ViewDidLoad:
collectionView.dataSource = self
collectionView.delegate = self
flowLayout.scrollDirection = .horizontal
flowLayout.minimumLineSpacing = 5
flowLayout.minimumInteritemSpacing = 5
flowLayout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 5)
Then later on the the file:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let totalWidth = collectionView.bounds.size.width
let totalHeight = collectionView.bounds.size.height
let heightOfView = totalHeight / 3
let numberOfCellsPerRow = 1
let dimensions = CGFloat(Int(totalWidth) / numberOfCellsPerRow)
if (indexPath.item == 4) {
return CGSize(width: collectionView.bounds.size.width, height: heightOfView)
} else {
return CGSize(width: dimensions / 2, height: heightOfView)
}
}
This code isn't having the desired effect, and is producing a UICollectionView that looks like this:
As you can see, the cells are not even in the UICollectionView, with the right-hand side cells overflowing into scroll space. The section gaps between the items are wrong and the 5th, the larger cell is on the right-hand side of the screen out of view unless scrolled to.
You cannot create such layout with standard UICollectionViewFlowLayout class. First of all watch WWDC videos related to UICollectionView and it's layout: https://developer.apple.com/videos/play/wwdc2012/219/. Then you can check Swift tutorials with sample code to get started: http://swiftiostutorials.com/tutorial-creating-custom-layouts-uicollectionview/
Simplest working, but not best structured will be such code in custom UICollectionViewLayout. Use this as a starting point:
#interface UICollectionViewCustomLayout ()
#property (nonatomic) NSArray<UICollectionViewLayoutAttributes *> *attributes;
#property (nonatomic) CGSize size;
#end
#implementation UICollectionViewCustomLayout
- (void)prepareLayout
{
[super prepareLayout];
NSMutableArray<UICollectionViewLayoutAttributes *> *attributes = [NSMutableArray new];
id<UICollectionViewDelegate> delegate = (id<UICollectionViewDelegate>)self.collectionView.delegate;
id<UICollectionViewDataSource> dataSource = (id<UICollectionViewDataSource>)self.collectionView.dataSource;
NSInteger count = [dataSource collectionView:self.collectionView numberOfItemsInSection:0];
CGFloat collectionViewWidth = CGRectGetWidth(self.collectionView.frame);
CGFloat collectionViewHeight = CGRectGetHeight(self.collectionView.frame);
CGFloat rowHeight = floor(collectionViewHeight / 3);
NSUInteger numberOfPages = count / 5;
if (count % 5) {
numberOfPages++;
}
for (NSInteger item = 0; item < count; item++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:0];
UICollectionViewLayoutAttributes *attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
NSInteger index = item % 5;
NSInteger page = item / 5;
CGFloat width = index == 4 ? collectionViewWidth : collectionViewWidth / 2;
CGSize size = CGSizeMake(width, rowHeight);
CGFloat x = page * collectionViewWidth + (index % 2 == 0 ? 0 : collectionViewWidth / 2);
CGFloat y = (index / 2) * rowHeight;
CGPoint origin = CGPointMake(x, y);
CGRect frame = CGRectZero;
frame.size = size;
frame.origin = origin;
attribute.frame = frame;
[attributes addObject:attribute];
}
self.attributes = attributes;
self.size = CGSizeMake(numberOfPages * collectionViewWidth, collectionViewHeight);
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray<UICollectionViewLayoutAttributes *> *result = [NSMutableArray new];
for (UICollectionViewLayoutAttributes *attribute in self.attributes) {
if (CGRectIntersectsRect(attribute.frame, rect)) {
[result addObject:attribute];
}
}
return result;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"indexPath.section == %# AND indexPath.item == %#", #(indexPath.section), #(indexPath.item)];
UICollectionViewLayoutAttributes *attributes = [self.attributes filteredArrayUsingPredicate:predicate].firstObject;
return attributes;
}
- (CGSize)collectionViewContentSize
{
return self.size;
}
#end
And controller:
// Views
#import "UICollectionViewCustomLayout.h"
#interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>
#property (nonatomic) UICollectionView *collectionView;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:[UICollectionViewCustomLayout new]];
self.collectionView.backgroundColor = [UIColor whiteColor];
self.collectionView.frame = self.view.bounds;
self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:NSStringFromClass([UICollectionViewCell class])];
[self.view addSubview:self.collectionView];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 10;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath
{
NSArray *colors = #[[UIColor redColor], [UIColor blueColor], [UIColor grayColor], [UIColor greenColor], [UIColor purpleColor], [UIColor cyanColor]];
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([UICollectionViewCell class]) forIndexPath:indexPath];
cell.contentView.backgroundColor = colors[arc4random() % colors.count];
cell.contentView.alpha = (arc4random() % 500 + 500) / 1000.0;
return cell;
}
#end
Related
I want to create a layout like this image below but having trouble understanding how flow layout can be used to achieve this kind of layout
Basically what I want is 1 large picture than 2 small pictures right to it,
Then from index 3,4,5... and so on will be a 3 images grid. Only the First 3 cells will have a different layout. Does anyone know how we can achieve this kind of Layout in collectionView?
Update Code for collectionView
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 100;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
AppDelegate* delegate = [AppDelegate applicationDelegate];
FriendsListCell *cell= (FriendsListCell*)[collectionView dequeueReusableCellWithReuseIdentifier:kFriendsListCellIdentifier forIndexPath:indexPath];
cell.backgroundColor = [UIColor redColor];
return cell;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
CGSize newSize = CGSizeZero;
newSize.height = 100; // use your prefered size
newSize.width = 100; // use your prefered size
if(indexPath.item == 0) // set custom size for the first item
{
newSize.width = 200; // use your prefered size
newSize.height = 200; // use your prefered size
}
return newSize;
}
You have to customiz the width and height of collectionview items according to the index .
like the follwoing:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let width : CGFloat
let height : CGFloat
if indexPath.item == 0 { // set custom size for the first item
width = 200 // use your prefered size
height = 200 // use your prefered size
} else {
width = 100 // use your prefered size
height = 100 // use your prefered size
}
return CGSizeMake(width, height)
}
and don't forgot to conform the UICollectionViewDelegateFlowLayout
For Objective-c
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
CGSize newSize = CGSizeZero;
newSize.height = 100; // use your prefered size
newSize.width = 100; // use your prefered size
if(indexPath.item == 0) // set custom size for the first item
{
newSize.width = 200; // use your prefered size
newSize.height = 200; // use your prefered size
}
return newSize;
}
Update screenshot
In Objective-C:
#interface MyFlowLayoutObjC()
#property (nonatomic, strong) NSMutableDictionary *cellsLayout;
#property (nonatomic, assign) CGSize computedContentSize;
#end
#implementation MyFlowLayoutObjC
-(id)init {
self = [super init];
if (self) {
_cellsLayout = [[NSMutableDictionary alloc] init];
}
return self;
}
-(void)prepareLayout {
[super prepareLayout];
[self setComputedContentSize:CGSizeZero];
[[self cellsLayout] removeAllObjects];
CGSize fullFrameSize = [[self collectionView] frame].size;
NSUInteger numberOfitems = [[self collectionView] numberOfItemsInSection:0];
CGSize unitSize = CGSizeMake(fullFrameSize.width / 3.0, fullFrameSize.width / 3.0);
for (NSUInteger anIndex = 0; anIndex < numberOfitems; anIndex ++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:anIndex inSection:0];
UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
switch (anIndex) {
case 0:
[attributes setFrame:CGRectMake(0, 0, unitSize.width * 2, unitSize.height * 2)];
break;
case 1:
[attributes setFrame:CGRectMake(unitSize.width * 2, 0, unitSize.width, unitSize.height)];
break;
case 2:
[attributes setFrame:CGRectMake(unitSize.width * 2, unitSize.height, unitSize.width, unitSize.height)];
break;
default:
[attributes setFrame:CGRectMake(unitSize.width * (anIndex % 3), unitSize.height * (CGFloat)((anIndex / 3) + 1), unitSize.width, unitSize.height)];
break;
}
[[self cellsLayout] setObject:attributes forKey:indexPath];
[self setComputedContentSize:CGSizeMake(MAX([attributes frame].origin.x + [attributes frame].size.width, [self computedContentSize].width),
MAX([attributes frame].origin.y + [attributes frame].size.height, [self computedContentSize].height))];
}
}
-(NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *layouts = [[self cellsLayout] allValues];
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes * _Nullable aLayout, NSDictionary<NSString *,id> * _Nullable bindings) {
return CGRectIntersectsRect([aLayout frame], rect);
}];
return [layouts filteredArrayUsingPredicate:predicate];
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
return [self cellsLayout][indexPath];
}
- (CGSize)collectionViewContentSize {
return [self computedContentSize];
}
#end
And in Swift:
class MyFlowLayout: UICollectionViewLayout {
private var cellsLayout: [IndexPath: UICollectionViewLayoutAttributes] = [:]
override func prepare() {
cellsLayout.removeAll()
guard let fullFrameSizeWidth = collectionView?.frame.size else { return }
guard let numberOfItems = collectionView?.numberOfItems(inSection: 0) else { return }
let unitSize = CGSize(width: fullFrameSizeWidth.width / 3.0, height: fullFrameSizeWidth.width / 3.0)
for anIndex in 0..<numberOfItems {
let indexPath = IndexPath(item: anIndex, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
switch anIndex {
case 0:
attributes.frame = CGRect(origin: .zero, size: CGSize(width: unitSize.width * 2, height: unitSize.height * 2))
case 1:
attributes.frame = CGRect(origin: CGPoint(x: unitSize.width * 2, y: 0), size: unitSize)
case 2:
attributes.frame = CGRect(origin: CGPoint(x: unitSize.width * 2, y: unitSize.height), size: unitSize)
default:
attributes.frame = CGRect(origin: CGPoint(x: unitSize.width * CGFloat(anIndex % 3),
y: unitSize.height * CGFloat(Int(anIndex / 3) + 1)),
size: unitSize)
}
cellsLayout[indexPath] = attributes
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cellsLayout.compactMap { $0.value.frame.intersects(rect) ? $0.value : nil }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cellsLayout[indexPath]
}
override var collectionViewContentSize: CGSize {
return CGSize(width: cellsLayout.max(by: { $0.1.frame.maxX < $1.1.frame.maxX })?.value.frame.maxX ?? 0,
height: cellsLayout.max(by: { $0.1.frame.maxY < $1.1.frame.maxY })?.value.frame.maxY ?? 0)
}
}
This should do the trick.
Now, let's explain, you need a custom UICollectionViewFlowLayout. Because with just overriding -collectionView:layout:sizeForItemAtIndexPath:, you can't do that. It will get the size of the first item, put it in the frame. Then, get the frame of the second item, put it at its right if it's fits, else under it, etc.
When everything is set, it will center on y axis for each line. That's why you have this behavior.
So what's needed?
Let's override -layoutAttributesForElementsInRect: to return the layouts for each cells which intersect with the rect (which is the visible rect).
In order to do so, we need to know theses frame, so I made de needed calculations in -prepareLayout.
This is my code:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
CGFloat pageWidth = scrollView.frame.size.width;
int page = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
NSLog(#"Page No Scroll %i",page);
}
you can get any subview by doing this on UIScrollView.
for(UIView * subView in scrollView.subviews )
{
// Here You can Get all subViews of your myScrollView.
}
-(void)scrollViewDidScroll:(UIScrollView *)ScrollView
{
int scrollPositionX = callerScrollView.contentOffset.x;
if (callerScrollView == tbl_ProductView) return;
for (UITableViewCell *cell in tbl_ProductView.visibleCells) {
UIScrollView cellScrollView = (UIScrollView )[cell viewWithTag:100];
if (callerScrollView == cellScrollView) continue;
NSInteger indexPath = callerScrollView.tag;
//NSLog(#"IndexPath Of Cell: %ld",(long)indexPath);
cellScrollView.contentOffset = CGPointMake(scrollPositionX, 0);
}
//ScollPage Position
static NSInteger previousPage = 0;
CGFloat pageWidth = callerScrollView.frame.size.width;
float fractionalPage = callerScrollView.contentOffset.x / pageWidth;
NSInteger page = lround(fractionalPage);
if (previousPage != page)
{
previousPage = page;
NSLog(#"Scrolling Page: %li",(long)page);
}
}
I am have a lot of difficulty trying to create a UICollectionView like in Spotify's Player that acts like this:
The problem for me is two fold.
1) How do I center the cells so that you can see the middle cell as well as the one of the left and right.
If I create cells that are square and add spacing between each cell, the cells are correctly displayed but are not centered.
2) With pagingEnabled = YES, the collectionview correctly swipes from one page to another. However, without the cells being centered, it simply moves the collection view over a page which is the width of the screen. So the question is how do you make the pages move so you get the effect above.
3) how do you animate the size of the cells as they move
I don't want to worry about this too much. If I can get that to work it would be great, but the harder problems are 1 and 2.
The code I have currently is a simple UICollectionView with normal delegate setup and custom UICollectionview cells that are squares. Maybe I neeed to subclass UICollectionViewFlowLayout? Or maybe I need to turn pagingEnabled to NO and then use custom swipe events? Would love any help!
In order to create an horizontal carousel layout, you'll have to subclass UICollectionViewFlowLayout then override targetContentOffset(forProposedContentOffset:withScrollingVelocity:), layoutAttributesForElements(in:) and shouldInvalidateLayout(forBoundsChange:).
The following Swift 5 / iOS 12.2 complete code shows how to implement them.
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let collectionDataSource = CollectionDataSource()
let flowLayout = ZoomAndSnapFlowLayout()
override func viewDidLoad() {
super.viewDidLoad()
title = "Zoomed & snapped cells"
guard let collectionView = collectionView else { fatalError() }
//collectionView.decelerationRate = .fast // uncomment if necessary
collectionView.dataSource = collectionDataSource
collectionView.collectionViewLayout = flowLayout
collectionView.contentInsetAdjustmentBehavior = .always
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}
}
ZoomAndSnapFlowLayout.swift
import UIKit
class ZoomAndSnapFlowLayout: UICollectionViewFlowLayout {
let activeDistance: CGFloat = 200
let zoomFactor: CGFloat = 0.3
override init() {
super.init()
scrollDirection = .horizontal
minimumLineSpacing = 40
itemSize = CGSize(width: 150, height: 150)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
guard let collectionView = collectionView else { fatalError() }
let verticalInsets = (collectionView.frame.height - collectionView.adjustedContentInset.top - collectionView.adjustedContentInset.bottom - itemSize.height) / 2
let horizontalInsets = (collectionView.frame.width - collectionView.adjustedContentInset.right - collectionView.adjustedContentInset.left - itemSize.width) / 2
sectionInset = UIEdgeInsets(top: verticalInsets, left: horizontalInsets, bottom: verticalInsets, right: horizontalInsets)
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
// Make the cells be zoomed when they reach the center of the screen
for attributes in rectAttributes where attributes.frame.intersects(visibleRect) {
let distance = visibleRect.midX - attributes.center.x
let normalizedDistance = distance / activeDistance
if distance.magnitude < activeDistance {
let zoom = 1 + zoomFactor * (1 - normalizedDistance.magnitude)
attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1)
attributes.zIndex = Int(zoom.rounded())
}
}
return rectAttributes
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else { return .zero }
// Add some snapping behaviour so that the zoomed cell is always centered
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.frame.width, height: collectionView.frame.height)
guard let rectAttributes = super.layoutAttributesForElements(in: targetRect) else { return .zero }
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2
for layoutAttributes in rectAttributes {
let itemHorizontalCenter = layoutAttributes.center.x
if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude {
offsetAdjustment = itemHorizontalCenter - horizontalCenter
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
// Invalidate layout so that every cell get a chance to be zoomed when it reaches the center of the screen
return true
}
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext
context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size
return context
}
}
CollectionDataSource.swift
import UIKit
class CollectionDataSource: NSObject, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 9
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
return cell
}
}
CollectionViewCell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .green
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Expected result:
Source:
WWDC 2012 session 219 - "Advanced Collection Views and Building Custom Layouts"
Well, I made UICollectionview moving just like this, yesterday.
I can share my code with you :)
Here's my storyboard
make sure uncheck 'Paging Enabled'
Here's my code.
#interface FavoriteViewController () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
{
NSMutableArray * mList;
CGSize cellSize;
}
#property (weak, nonatomic) IBOutlet UICollectionView *cv;
#end
#implementation FavoriteViewController
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// to get a size.
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
CGRect screenFrame = [[UIScreen mainScreen] bounds];
CGFloat width = screenFrame.size.width*self.cv.frame.size.height/screenFrame.size.height;
cellSize = CGSizeMake(width, self.cv.frame.size.height);
// if cell's height is exactly same with collection view's height, you get an warning message.
cellSize.height -= 1;
[self.cv reloadData];
// setAlpha is for hiding looking-weird at first load
[self.cv setAlpha:0];
}
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self scrollViewDidScroll:self.cv];
[self.cv setAlpha:1];
}
#pragma mark - scrollview delegate
- (void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if(mList.count > 0)
{
const CGFloat centerX = self.cv.center.x;
for(UICollectionViewCell * cell in [self.cv visibleCells])
{
CGPoint pos = [cell convertPoint:CGPointZero toView:self.view];
pos.x += cellSize.width/2.0f;
CGFloat distance = fabs(centerX - pos.x);
// If you want to make side-cell's scale bigger or smaller,
// change the value of '0.1f'
CGFloat scale = 1.0f - (distance/centerX)*0.1f;
[cell setTransform:CGAffineTransformMakeScale(scale, scale)];
}
}
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{ // for custom paging
CGFloat movingX = velocity.x * scrollView.frame.size.width;
CGFloat newOffsetX = scrollView.contentOffset.x + movingX;
if(newOffsetX < 0)
{
newOffsetX = 0;
}
else if(newOffsetX > cellSize.width * (mList.count-1))
{
newOffsetX = cellSize.width * (mList.count-1);
}
else
{
NSUInteger newPage = newOffsetX/cellSize.width + ((int)newOffsetX%(int)cellSize.width > cellSize.width/2.0f ? 1 : 0);
newOffsetX = newPage*cellSize.width;
}
targetContentOffset->x = newOffsetX;
}
#pragma mark - collectionview delegate
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return mList.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"list" forIndexPath:indexPath];
NSDictionary * dic = mList[indexPath.row];
UIImageView * iv = (UIImageView *)[cell.contentView viewWithTag:1];
UIImage * img = [UIImage imageWithData:[dic objectForKey:kKeyImg]];
[iv setImage:img];
return cell;
}
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return cellSize;
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
CGFloat gap = (self.cv.frame.size.width - cellSize.width)/2.0f;
return UIEdgeInsetsMake(0, gap, 0, gap);
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section
{
return 0;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section
{
return 0;
}
Key code of make cell centered is
scrollViewWillEndDragging
insetForSectionAtIndex
Key code of animate the size is
scrollviewDidScroll
I wish this helps you
P.S.
If you want to change alpha just like the image that you uploaded, add [cell setalpha] in scrollViewDidScroll
As you have said in the comment you want that in the Objective-c code, there is a very famous library called iCarousel which can be helpful in completing your requirement.Link: https://github.com/nicklockwood/iCarousel
You may use 'Rotary' or 'Linear' or some other style with little or no modification to implement the custom view
To implement it you have implement only some delegate methods of it and it's working for ex:
//specify the type you want to use in viewDidLoad
_carousel.type = iCarouselTypeRotary;
//Set the following delegate methods
- (NSInteger)numberOfItemsInCarousel:(iCarousel *)carousel
{
//return the total number of items in the carousel
return [_items count];
}
- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view
{
UILabel *label = nil;
//create new view if no view is available for recycling
if (view == nil)
{
//don't do anything specific to the index within
//this `if (view == nil) {...}` statement because the view will be
//recycled and used with other index values later
view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200.0f, 200.0f)];
((UIImageView *)view).image = [UIImage imageNamed:#"page.png"];
view.contentMode = UIViewContentModeCenter;
label = [[UILabel alloc] initWithFrame:view.bounds];
label.backgroundColor = [UIColor clearColor];
label.textAlignment = NSTextAlignmentCenter;
label.font = [label.font fontWithSize:50];
label.tag = 1;
[view addSubview:label];
}
else
{
//get a reference to the label in the recycled view
label = (UILabel *)[view viewWithTag:1];
}
//set item label
label.text = [_items[index] stringValue];
return view;
}
- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value
{
if (option == iCarouselOptionSpacing)
{
return value * 1.1;
}
return value;
}
You can check the full working demo from 'Examples/Basic iOS Example' which is included with the Github repository link
As it is old and popular you can find some related tutorials for it and it will also be much stable than the custom code implementation
I wanted similar behavior a little while back, and with the help of #Mike_M I was able to figure it out. Although there are many, many way to do this, this particular implementation is to create a custom UICollectionViewLayout.
Code below(gist can be found here: https://gist.github.com/mmick66/9812223)
Now it's important to set the following: *yourCollectionView*.decelerationRate = UIScrollViewDecelerationRateFast, this prevents cells being skipped by a quick swipe.
That should cover part 1 and 2. Now, for part 3 you could incorporate that in the custom collectionView by constantly invalidating and updating, but it's a bit of a hassle if you ask me. So another approach would be to to set a CGAffineTransformMakeScale( , ) in the UIScrollViewDidScroll where you dynamically update the cell's size based on it's distance from the center of the screen.
You can get the indexPaths of the visible cells of the collectionView using [*youCollectionView indexPathsForVisibleItems] and then getting the cells for these indexPaths. For every cell, calculate the distance of its center to the center of yourCollectionView
The center of the collectionView can be found using this nifty method: CGPoint point = [self.view convertPoint:*yourCollectionView*.center toView:*yourCollectionView];
Now set up a rule, that if the cell's center is further than x away, the size of the cell is for example the 'normal size', call it 1. and the closer it gets to the center, the closer it gets to twice the normal size 2.
then you can use the following if/else idea:
if (distance > x) {
cell.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} else if (distance <= x) {
float scale = MIN(distance/x) * 2.0f;
cell.transform = CGAffineTransformMakeScale(scale, scale);
}
What happens is that the cell's size will exactly follow your touch. Let me know if you have any more questions as I'm writing most of this out of the top of my head).
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)offset
withScrollingVelocity:(CGPoint)velocity {
CGRect cvBounds = self.collectionView.bounds;
CGFloat halfWidth = cvBounds.size.width * 0.5f;
CGFloat proposedContentOffsetCenterX = offset.x + halfWidth;
NSArray* attributesArray = [self layoutAttributesForElementsInRect:cvBounds];
UICollectionViewLayoutAttributes* candidateAttributes;
for (UICollectionViewLayoutAttributes* attributes in attributesArray) {
// == Skip comparison with non-cell items (headers and footers) == //
if (attributes.representedElementCategory !=
UICollectionElementCategoryCell) {
continue;
}
// == First time in the loop == //
if(!candidateAttributes) {
candidateAttributes = attributes;
continue;
}
if (fabsf(attributes.center.x - proposedContentOffsetCenterX) <
fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
candidateAttributes = attributes;
}
}
return CGPointMake(candidateAttributes.center.x - halfWidth, offset.y);
}
pagingEnabled should not be enabled as it needs each cell to be the width of you view which will not work for you since you need to see the edges of other cells. For your points 1 and 2. I think you'll find what you need here from one of my late answers to another question.
The animation of the cell sizes can be achieved by subclassing UIcollectionviewFlowLayout and overriding layoutAttributesForItemAtIndexPath: Within that modify the layout attributes provided by first calling super and then modify the layout attributes size based on the position as it relates to the window centre.
Hopefully this helps.
If you want to have uniform spacing between cells you can replace the following method in ZoomAndSnapFlowLayout from Imanou Petit's solution:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)
let visibleAttributes = rectAttributes.filter { $0.frame.intersects(visibleRect) }
// Keep the spacing between cells the same.
// Each cell shifts the next cell by half of it's enlarged size.
// Calculated separately for each direction.
func adjustXPosition(_ toProcess: [UICollectionViewLayoutAttributes], direction: CGFloat, zoom: Bool = false) {
var dx: CGFloat = 0
for attributes in toProcess {
let distance = visibleRect.midX - attributes.center.x
attributes.frame.origin.x += dx
if distance.magnitude < activeDistance {
let normalizedDistance = distance / activeDistance
let zoomAddition = zoomFactor * (1 - normalizedDistance.magnitude)
let widthAddition = attributes.frame.width * zoomAddition / 2
dx = dx + widthAddition * direction
if zoom {
let scale = 1 + zoomAddition
attributes.transform3D = CATransform3DMakeScale(scale, scale, 1)
}
}
}
}
// Adjust the x position first from left to right.
// Then adjust the x position from right to left.
// Lastly zoom the cells when they reach the center of the screen (zoom: true).
adjustXPosition(visibleAttributes, direction: +1)
adjustXPosition(visibleAttributes.reversed(), direction: -1, zoom: true)
return rectAttributes
}
I am lazy loading some images in a table view
I followed the below tutorial
Lazy Load ios
This is the cell style which I want to display.
The problem is on the server side image is too big so the image view takes the full width and height of the image. Which results in an abrupt display of the table.
I cant reduce the size on the server. Isnt there any way with which I can set the image height and width for all images that are being lazy loaded.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"videoCell1";
CellTableViewCell *cell =(CellTableViewCell *)[tableView1 dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"videoCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
cell.videoNameLabel.text = [videoTitle objectAtIndex:indexPath.row];
cell.videoImage.contentMode = UIViewContentModeScaleAspectFill;
[cell.videoImage sd_setImageWithURL:[NSURL URLWithString:[videoImage objectAtIndex:indexPath.row]]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 200;
}
Plus on scrolling the table view it also disrupts. How do I solve the issue? Any suggestions.
I would recommend looking at PHImageManager
Example code in swift
var assets: [PHAsset]
var imageRequests: [NSIndexPath: PHImageRequestID]
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
let manager = PHImageManager.defaultManager()
if let request = imageRequests[indexPath] {
manager.cancelImageRequest(request)
}
let asset = assets[indexPath.row]
cell.textLabel?.text = NSDateFormatter.localizedStringFromDate(asset.creationDate, dateStyle: .MediumStyle, timeStyle: .MediumStyle)
imageRequests[indexPath] = manager.requestImageForAsset(asset, targetSize: CGSize(width: 100.0, height: 100.0), contentMode: .AspectFill, options: nil) { (result, _) in
cell.imageView?.image = result
}
return cell
}
The cell reusing is not properly implemented.
Add this code:
- (void)viewDidLoad
{
[super viewDidload];
[self.tableView registerNib:[UINib nibWithNibName:#"videoCell" bundle:nil]
forCellReuseIdentifier:#"videoCell1"];
}
And remove this code
if (cell == nil) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"videoCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
Before saving the image in your _diskCachePath, resize the image and then save. When you scroll the table view it will load smaller size images and it looks smooth scrolling.
For resizing the image use the following method. This will maintains the aspect ratio when resizing the image.
-(UIImage*) imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize{
CGFloat width = image.size.width;
CGFloat height = image.size.height;
if ((width/newSize.width) > (height/newSize.height)) {
if (width < newSize.width) {
newSize.width = width;
}
newSize.height = height*(newSize.width/width);
}
else if ((width/newSize.width) < (height/newSize.height)) {
if (height < newSize.height) {
newSize.height = height;
}
newSize.width = width*(newSize.height/height);
}
else if(newSize.width > width && newSize.height > height){
newSize.height = height;
newSize.width = width;
}
UIGraphicsBeginImageContext( newSize );
[image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
I would like to know when a UITableView did scroll to bottom in order to load and show more content, something like a delegate or something else to let the controller know when the table did scroll to bottom.
How can I do this?
in the tableview delegate do something like this
ObjC:
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
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;
// NSLog(#"offset: %f", offset.y);
// NSLog(#"content.height: %f", size.height);
// NSLog(#"bounds.height: %f", bounds.size.height);
// NSLog(#"inset.top: %f", inset.top);
// NSLog(#"inset.bottom: %f", inset.bottom);
// NSLog(#"pos: %f of %f", y, h);
float reload_distance = 10;
if(y > h + reload_distance) {
NSLog(#"load more rows");
}
}
Swift:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset
let bounds = scrollView.bounds
let size = scrollView.contentSize
let inset = scrollView.contentInset
let y = offset.y + bounds.size.height - inset.bottom
let h = size.height
let reload_distance:CGFloat = 10.0
if y > (h + reload_distance) {
print("load more rows")
}
}
Modified neoneyes answer a bit.
This answer targets those of you who only wants the event to be triggered once per release of the finger.
Suitable when loading more content from some content provider (web service, core data etc).
Note that this approach does not respect the response time from your web service.
- (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) {
NSLog(#"load more rows");
}
}
add this method in the UITableViewDelegate:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat height = scrollView.frame.size.height;
CGFloat contentYoffset = scrollView.contentOffset.y;
CGFloat distanceFromBottom = scrollView.contentSize.height - contentYoffset;
if(distanceFromBottom < height)
{
NSLog(#"end of the table");
}
}
None of the answers above helped me, so I came up with this:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)aScrollView
{
NSArray *visibleRows = [self.tableView visibleCells];
UITableViewCell *lastVisibleCell = [visibleRows lastObject];
NSIndexPath *path = [self.tableView indexPathForCell:lastVisibleCell];
if(path.section == lastSection && path.row == lastRow)
{
// Do something here
}
}
The best way is to test a point at the bottom of the screen and use this method call when ever the user scrolls (scrollViewDidScroll):
- (NSIndexPath *)indexPathForRowAtPoint:(CGPoint)point
Test a point near the bottom of the screen, and then using the indexPath it returns check if that indexPath is the last row then if it is, add rows.
Use – tableView:willDisplayCell:forRowAtIndexPath: (UITableViewDelegate method)
Simply compare the indexPath with the items in your data array (or whatever data source you use for your table view) to figure out if the last element is being displayed.
Docs: http://developer.apple.com/library/ios/documentation/uikit/reference/UITableViewDelegate_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/UITableViewDelegate/tableView:willDisplayCell:forRowAtIndexPath:
UITableView is a subclass of UIScrollView, and UITableViewDelegate conforms to UIScrollViewDelegate. So the delegate you attach to the table view will get events such as scrollViewDidScroll:, and you can call methods such as contentOffset on the table view to find the scroll position.
NSLog(#"%f / %f",tableView.contentOffset.y, tableView.contentSize.height - tableView.frame.size.height);
if (tableView.contentOffset.y == tableView.contentSize.height - tableView.frame.size.height)
[self doSomething];
Nice and simple
in Swift you can do something like this. Following condition will be true every time you reach end of the tableview
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.row+1 == postArray.count {
println("came to last row")
}
}
Building on #Jay Mayu's answer, which I felt was one of the better solutions:
Objective-C
// UITableViewDelegate
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// Need to call the service & update the array
if(indexPath.row + 1 == self.sourceArray.count) {
DLog(#"Displayed the last row!");
}
}
Swift 2.x
// UITableViewDelegate
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
if (indexPath.row + 1) == sourceArray.count {
print("Displayed the last row!")
}
}
Here is the swift 3.0 version code.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset
let bounds = scrollView.bounds
let size = scrollView.contentSize
let inset = scrollView.contentInset
let y: Float = Float(offset.y) + Float(bounds.size.height) + Float(inset.bottom)
let height: Float = Float(size.height)
let distance: Float = 10
if y > height + distance {
// load more data
}
}
I generally use this to load more data , when last cell starts display
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == myDataArray.count-1) {
NSLog(#"load more");
}
}
Taking neoneye excellent answers but in swift, and renaming of the variables..
Basically we know we have reached the bottom of the table view if the yOffsetAtBottom is beyond the table content height.
func isTableViewScrolledToBottom() -> Bool {
let tableHeight = tableView.bounds.size.height
let contentHeight = tableView.contentSize.height
let insetHeight = tableView.contentInset.bottom
let yOffset = tableView.contentOffset.y
let yOffsetAtBottom = yOffset + tableHeight - insetHeight
return yOffsetAtBottom > contentHeight
}
My solution is to add cells before tableview will decelerate on estimated offset. It's predictable on by velocity.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)offset {
NSLog(#"offset: %f", offset->y+scrollView.frame.size.height);
NSLog(#"Scroll view content size: %f", scrollView.contentSize.height);
if (offset->y+scrollView.frame.size.height > scrollView.contentSize.height - 300) {
NSLog(#"Load new rows when reaches 300px before end of content");
[[DataManager shared] fetchRecordsNextPage];
}
}
Update for Swift 3
Neoneye's answer worked best for me in Objective C, this is the equivalent of the answer in Swift 3:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let offset: CGPoint = scrollView.contentOffset
let bounds: CGRect = scrollView.bounds
let size: CGSize = scrollView.contentSize
let inset: UIEdgeInsets = scrollView.contentInset
let y: CGFloat = offset.y + bounds.size.height - inset.bottom
let h: CGFloat = size.height
// print("offset: %f", offset.y)
// print("content.height: %f", size.height)
// print("bounds.height: %f", bounds.size.height)
// print("inset.top: %f", inset.top)
// print("inset.bottom: %f", inset.bottom)
// print("position: %f of %f", y, h)
let reloadDistance: CGFloat = 10
if (y > h + reloadDistance) {
print("load more rows")
}
}
I want perform some action on my any 1 full Tableviewcell.
So the code is link the :
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSArray* cells = self.tableView.visibleCells;
NSIndexPath *indexPath = nil ;
for (int aIntCount = 0; aIntCount < [cells count]; aIntCount++)
{
UITableViewCell *cell = [cells objectAtIndex:aIntCount];
CGRect cellRect = [self.tableView convertRect:cell.frame toView:self.tableView.superview];
if (CGRectContainsRect(self.tableView.frame, cellRect))
{
indexPath = [self.tableView indexPathForCell:cell];
// remain logic
}
}
}
May this is help to some one.
#neoneye's answer worked for me. Here is the Swift 4 version of it
let offset = scrollView.contentOffset
let bounds = scrollView.bounds
let size = scrollView.contentSize
let insets = scrollView.contentInset
let y = offset.y + bounds.size.height - insets.bottom
let h = size.height
let reloadDistance = CGFloat(10)
if y > h + reloadDistance {
//load more rows
}