Dequeue cells more than default UICollectionView dequeued cells - ios

Is there a way to dequeue more cells than default UICollectionView dequeued cells? For example in the picture below UICollectionView dequeues showing cell, one before and one after. Is there a way to dequeue more than this three cells?func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { print("Prefetch: \(indexPaths)") }
only gives me one indexpath after. What I need is to create four or five cells after and before showing cells to Implement my functionality before cell is shown.

An official Apple sample code shows a technique that you might find useful. Instead of relying on UICollectionView to receive callbacks and load data as needed, you can observe scrolling and based on contentOffset value, you will know how much look ahead you need.
Copy pasting relevant parts here in case link isn't available for a future reader. All of this lives inside a UICollectionViewController subclass - it can be adapted to work with any UIScrollView in general.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// This is your trigger point
updateCachedAssets()
}
var fetchResult: PHFetchResult<PHAsset>!
fileprivate let imageManager = PHCachingImageManager()
fileprivate let thumbnailSize: CGSize = CGSize(width: 100, height: 100)
fileprivate var previousPreheatRect = CGRect.zero
fileprivate func updateCachedAssets() {
// Update only if the view is visible.
guard isViewLoaded && view.window != nil else { return }
// The window you prepare ahead of time is twice the height of the visible rect.
let visibleRect = CGRect(origin: collectionView!.contentOffset, size: collectionView!.bounds.size)
let preheatRect = visibleRect.insetBy(dx: 0, dy: -0.5 * visibleRect.height)
// Update only if the visible area is significantly different from the last preheated area.
let delta = abs(preheatRect.midY - previousPreheatRect.midY)
guard delta > view.bounds.height / 3 else { return }
// Compute the assets to start and stop caching.
let (addedRects, removedRects) = differencesBetweenRects(previousPreheatRect, preheatRect)
let addedAssets = addedRects
.flatMap { rect in collectionView!.indexPathsForElements(in: rect) }
.map { indexPath in fetchResult.object(at: indexPath.item) }
let removedAssets = removedRects
.flatMap { rect in collectionView!.indexPathsForElements(in: rect) }
.map { indexPath in fetchResult.object(at: indexPath.item) }
// Update the assets the PHCachingImageManager is caching.
imageManager.startCachingImages(for: addedAssets,
targetSize: thumbnailSize, contentMode: .aspectFill, options: nil)
imageManager.stopCachingImages(for: removedAssets,
targetSize: thumbnailSize, contentMode: .aspectFill, options: nil)
// Store the computed rectangle for future comparison.
previousPreheatRect = preheatRect
}
fileprivate func differencesBetweenRects(_ old: CGRect, _ new: CGRect) -> (added: [CGRect], removed: [CGRect]) {
if old.intersects(new) {
var added = [CGRect]()
if new.maxY > old.maxY {
added += [CGRect(x: new.origin.x, y: old.maxY,
width: new.width, height: new.maxY - old.maxY)]
}
if old.minY > new.minY {
added += [CGRect(x: new.origin.x, y: new.minY,
width: new.width, height: old.minY - new.minY)]
}
var removed = [CGRect]()
if new.maxY < old.maxY {
removed += [CGRect(x: new.origin.x, y: new.maxY,
width: new.width, height: old.maxY - new.maxY)]
}
if old.minY < new.minY {
removed += [CGRect(x: new.origin.x, y: old.minY,
width: new.width, height: new.minY - old.minY)]
}
return (added, removed)
} else {
return ([new], [old])
}
}
Notes
The code is several years old and you will need to adapt this to your requirements.
You can adjust the preheatRect compute to be what fits your needs.
// The window you prepare ahead of time is twice the height of the visible rect.
let visibleRect = CGRect(origin: collectionView!.contentOffset, size: collectionView!.bounds.size)
let preheatRect = visibleRect.insetBy(dx: 0, dy: -0.5 * visibleRect.height)
The sample implements this preheatRect logic for preloading PHAsset thumbnails. You need to adapt your dataSource to a similar style and start/stop loading your resources.

Related

How to animate collection view layout change while using `layoutAttributesForElements`?

I made a custom collection view flow layout that can toggle (with animation) between "film-strip" and "list" layouts. But after adding some fancy animations to the edge cells, the toggle animation broke. Here's what it looks like currently, without those changes:
The animation is nice and smooth, right? Here's the current, working code (full demo project here):
enum LayoutType {
case strip
case list
}
class FlowLayout: UICollectionViewFlowLayout {
var layoutType: LayoutType
var layoutAttributes = [UICollectionViewLayoutAttributes]() /// store the frame of each item
var contentSize = CGSize.zero /// the scrollable content size of the collection view
override var collectionViewContentSize: CGSize { return contentSize } /// pass scrollable content size back to the collection view
/// pass attributes to the collection view flow layout
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return layoutAttributes[indexPath.item]
}
// MARK: - Problem is here
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
/// edge cells don't shrink, but the animation is perfect
return layoutAttributes.filter { rect.intersects($0.frame) } /// try deleting this line
/// edge cells shrink (yay!), but the animation glitches out
return shrinkingEdgeCellAttributes(in: rect)
}
/// makes the edge cells slowly shrink as you scroll
func shrinkingEdgeCellAttributes(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
let rectAttributes = layoutAttributes.filter { rect.intersects($0.frame) }
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size) /// rect of the visible collection view cells
let leadingCutoff: CGFloat = 50 /// once a cell reaches here, start shrinking it
let trailingCutoff: CGFloat
let paddingInsets: UIEdgeInsets /// apply shrinking even when cell has passed the screen's bounds
if layoutType == .strip {
trailingCutoff = CGFloat(collectionView.bounds.width - leadingCutoff)
paddingInsets = UIEdgeInsets(top: 0, left: -50, bottom: 0, right: -50)
} else {
trailingCutoff = CGFloat(collectionView.bounds.height - leadingCutoff)
paddingInsets = UIEdgeInsets(top: -50, left: 0, bottom: -50, right: 0)
}
for attributes in rectAttributes where visibleRect.inset(by: paddingInsets).contains(attributes.center) {
/// center of each cell, converted to a point inside `visibleRect`
let center = layoutType == .strip
? attributes.center.x - visibleRect.origin.x
: attributes.center.y - visibleRect.origin.y
var offset: CGFloat?
if center <= leadingCutoff {
offset = leadingCutoff - center /// distance from the cutoff, 0 if exactly on cutoff
} else if center >= trailingCutoff {
offset = center - trailingCutoff
}
if let offset = offset {
let scale = 1 - (pow(offset, 1.1) / 200) /// gradually shrink the cell
attributes.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
return rectAttributes
}
/// initialize with a LayoutType
init(layoutType: LayoutType) {
self.layoutType = layoutType
super.init()
}
/// make the layout (strip vs list) here
override func prepare() { /// configure the cells' frames
super.prepare()
guard let collectionView = collectionView else { return }
var offset: CGFloat = 0 /// origin for each cell
let cellSize = layoutType == .strip ? CGSize(width: 100, height: 50) : CGSize(width: collectionView.frame.width, height: 50)
for itemIndex in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: itemIndex, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let origin: CGPoint
let addedOffset: CGFloat
if layoutType == .strip {
origin = CGPoint(x: offset, y: 0)
addedOffset = cellSize.width
} else {
origin = CGPoint(x: 0, y: offset)
addedOffset = cellSize.height
}
attributes.frame = CGRect(origin: origin, size: cellSize)
layoutAttributes.append(attributes)
offset += addedOffset
}
self.contentSize = layoutType == .strip /// set the collection view's `collectionViewContentSize`
? CGSize(width: offset, height: cellSize.height) /// if strip, height is fixed
: CGSize(width: cellSize.width, height: offset) /// if list, width is fixed
}
/// boilerplate code
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 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
}
}
class ViewController: UIViewController {
var data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
var isExpanded = false
lazy var listLayout = FlowLayout(layoutType: .list)
lazy var stripLayout = FlowLayout(layoutType: .strip)
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
#IBAction func toggleExpandPressed(_ sender: Any) {
isExpanded.toggle()
if isExpanded {
collectionView.setCollectionViewLayout(listLayout, animated: true)
} else {
collectionView.setCollectionViewLayout(stripLayout, animated: true)
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = stripLayout /// start with the strip layout
collectionView.dataSource = self
collectionViewHeightConstraint.constant = 300
}
}
/// sample data source
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ID", for: indexPath) as! Cell
cell.label.text = "\(data[indexPath.item])"
cell.contentView.layer.borderWidth = 5
cell.contentView.layer.borderColor = UIColor.red.cgColor
return cell
}
}
class Cell: UICollectionViewCell {
#IBOutlet weak var label: UILabel!
}
Again, everything works perfectly, including the animation. So then, I tried to make the cells shrink as they neared the screen's edge. I overrode layoutAttributesForElements to do this.
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return layoutAttributes.filter { rect.intersects($0.frame) } /// delete this line
return shrinkingEdgeCellAttributes(in: rect) /// replace with this
}
Film-strip
List
The scale/shrink animation is great. However, when I toggle between the layouts, the transition animation is broken.
Before (return layoutAttributes.filter...)
After (return shrinkingEdgeCellAttributes(in: rect))
How can I fix this animation? Should I be using a custom UICollectionViewTransitionLayout, and if so, how?
Whew! This was a workout. I was able to modify your FlowLayout so that there are no hiccups in animation. See below.
It works!
Problem
This is what was happening. When you change layouts, the layoutAttributesForElements method in FlowLayout is called twice if the content offset of the collection view is anything but (0, 0).
This is because you have overridden 'shouldInvalidateLayout' to return true regardless of whether it is actually needed. I believe the UICollectionView calls this method on the layout before and after the layout change (as per the observation).
The side effect of this is that your scale transform is applied twice - before and after the animations to the visible layout attributes.
Unfortunately, the scale transform is applied based on the contentOffset of the collection view (link)
let visibleRect = CGRect(
origin: collectionView.contentOffset,
size: collectionView.frame.size
)
During layout changes the contentOffset is not consistent. Before the animation starts contentOffset is applicable to the previous layout. After the animation, it is relative to the new layout. Here I also noticed that without a good reason, the contentOffset "jumps" around (see note 1)
Since you use the visibleRect to query the set of Layout Attributes to apply the scale on, it introduces further errors.
Solution
I was able to find a solution by applying these changes.
Write helpers methods to transform the content offset (and dependent visibleRect) left by the previous layout to values meaningful for this layout.
Prevent redundant layout attribute calculates in prepare method
Track when and when not the layout is animating
// In Flow Layout
class FlowLayout: UICollectionViewFlowLayout {
var animating: Bool = false
// ...
}
// In View Controller,
isExpanded.toggle()
if isExpanded {
listLayout.reset()
listLayout.animating = true // <--
// collectionView.setCollectionViewLayout(listLayout)
} else {
stripLayout.reset()
stripLayout.animating = true // <--
// collectionView.setCollectionViewLayout(stripLayout)
}
Override targetContentOffset method to handle content offset changes (prevent jumps)
// In Flow Layout
class FlowLayout: UICollectionViewFlowLayout {
var animating: Bool = false
var layoutType: LayoutType
// ...
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
guard animating else {
// return super
}
// Use our 'graceful' content content offset
// instead of arbitrary "jump"
switch(layoutType){
case .list: return transformCurrentContentOffset(.fromStripToList)
case .strip: return transformCurrentContentOffset(.fromListToStrip)
}
}
// ...
The implementation of content offset transforming is as follows.
/**
Transforms this layouts content offset, to the other layout
as specified in the layout transition parameter.
*/
private func transformCurrentContentOffset(_ transition: LayoutTransition) -> CGPoint{
let stripItemWidth: CGFloat = 100.0
let listItemHeight: CGFloat = 50.0
switch(transition){
case .fromStripToList:
let numberOfItems = collectionView!.contentOffset.x / stripItemWidth // from strip
var newPoint = CGPoint(x: 0, y: numberOfItems * CGFloat(listItemHeight)) // to list
if (newPoint.y + collectionView!.frame.height) >= contentSize.height{
newPoint = CGPoint(x: 0, y: contentSize.height - collectionView!.frame.height)
}
return newPoint
case .fromListToStrip:
let numberOfItems = collectionView!.contentOffset.y / listItemHeight // from list
var newPoint = CGPoint(x: numberOfItems * CGFloat(stripItemWidth), y: 0) // to strip
if (newPoint.x + collectionView!.frame.width) >= contentSize.width{
newPoint = CGPoint(x: contentSize.width - collectionView!.frame.width, y: 0)
}
return newPoint
}
}
There are some minor details I left out in the comments and as a pull request to OP's demo project so anyone interested can study it.
The key take-aways are,
Use targetContentOffset when arbitrary changes in content offset occur in response to layout changes.
Be careful about incorrect query of layout attributes in layoutAttributesForElements. Debug your rects!
Remember to clear your cached layout attributes on the prepare() method.
Notes
The "jump" behavior is evident even before you introduced scale transforms as seen in your gif.
I sincerely apologize if the answer is lengthy. Or, The solution is not quite what you wanted. The question was interesting which is why I spent the whole day trying to find a way to help.
Fork and Pull request.
Thanks for your detailed investigation #Thisura Dodangoda
– it was instrumental in helping me solve a similar problem. For folks who end up here, I want to add a tiny detail in case you run into another issue that I did.
The UICollectionViewLayout API has 2 very similar methods:
func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint
This method Retrieves the point at which to stop scrolling
and
func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint
This method Retrieves the content offset to use after an animated layout update or change
I had already implemented the first for some custom behaviour during scrolling, and I was trying to implement the solution posted by #Thisura Dodangoda in that method.
However, these are used for completely different purposes. You need to use the second method (without the velocity parameter) to implement the solution for layout changes.

Autoresize UICollectionView cells, Align cell to top

I've a UICollectionView, with multiple sections and rows.
Header and Footer views wherever necessary, of fixed size.
Cells that are autoresizable
The cell view are designed like :
Green colour - ImageView
Orange colour - Labels with numberOfLines = 0.
The cell should expand it's size according to label numberOfLines.
I've achieved this using this code in MyCustomCell :
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
super.apply(layoutAttributes)
let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
let targetSize = CGSize(width: Constants.screenWidth/3.5, height: 0)
let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.defaultLow)
let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)
autoLayoutAttributes.frame = autoLayoutFrame
return autoLayoutAttributes
}
The cells are autoresizing but the contentView (in cyan colour) are Centre aligned both vertically and horizontally.
I need to make it vertically align to Top.
I had the alignment problem with headers and footers too. For that i've subclassed UICollectionViewFlowLayout
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext {
let context: UICollectionViewLayoutInvalidationContext = super.invalidationContext(forPreferredLayoutAttributes: preferredAttributes, withOriginalAttributes: originalAttributes)
let indexPath = preferredAttributes.indexPath
context.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter, at: [indexPath])
context.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionHeader, at: [indexPath])
return context
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var topMargin = sectionInset.top
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if layoutAttribute.frame.origin.y >= maxY {
leftMargin = sectionInset.left
topMargin = sectionInset.top
}
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}
Here is an image to illustrate current situation. The cyan are contentView of cells. I've to make it align to Top.
EDIT:
So i realised that UICollectionViewFlowLayout code was creating more bugs than fixing a problem. I added layoutAttributesForElements to left align my cell in case there is only one cell. What it actually did was align all of my cells to the left.
Modified the code as
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
if attributes?.count == 1 {
if let currentAttribute = attributes?.first {
currentAttribute.frame = CGRect(x: self.sectionInset.left, y: currentAttribute.frame.origin.y, width: currentAttribute.frame.size.width, height: currentAttribute.frame.size.height)
}
}
return attributes
}
}
Now my UICollectionViewCell are properly aligned to horizontal centre with exception to if only one cell which will be left aligned.
Still no solution for vertical alignment.
So I worked around this for quite a bit. No answers from stack overflow help in any way. So i played with custom UICollectionViewFlowLayout method layoutAttributesForElements.
This method will call for every section, and will have layoutAttributesForElements for rect. This will give an array of UICollectionViewLayoutAttributes which one can modify as per the need.
I was having problem figuring out the y coordinate which I would like to change for my cells.
So first with a loop in attributes from layoutAttributesForElements I got the header and saved it's height in a variable. Then changed the rest of cell's y coordinate with the headerHeight value.
Not sure if this is the right way or not, nor did any performance testing. For now everything seems fine without any lag or jerk.
Here is full code or custom UICollectionViewFlowLayout.
class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attr = super.layoutAttributesForElements(in: rect)
var attributes = [UICollectionViewLayoutAttributes]()
for itemAttributes in attr! {
let itemAttributesCopy = itemAttributes.copy() as! UICollectionViewLayoutAttributes
// manipulate itemAttributesCopy
attributes.append(itemAttributesCopy)
}
if attributes.count == 1 {
if let currentAttribute = attributes.first {
currentAttribute.frame = CGRect(x: self.sectionInset.left, y: currentAttribute.frame.origin.y, width: currentAttribute.frame.size.width, height: currentAttribute.frame.size.height)
}
} else {
var sectionHeight: CGFloat = 0
attributes.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .supplementaryView else {
return
}
if layoutAttribute.representedElementKind == UICollectionView.elementKindSectionHeader {
sectionHeight = layoutAttribute.frame.size.height
}
}
attributes.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if layoutAttribute.frame.origin.x == 0 {
sectionHeight = max(layoutAttribute.frame.minY, sectionHeight)
}
layoutAttribute.frame = CGRect(origin: CGPoint(x: layoutAttribute.frame.origin.x, y: sectionHeight), size: layoutAttribute.frame.size)
}
}
return attributes
}
}
NOTE: You need to copy the attributes in an array first to work with. Else xcode will yell Logging only once for UICollectionViewFlowLayout cache mismatched frame in console.
If any new/perfect solution is there please let me know.
CHEERS!!!

Why in UITableView, cellForRowAtIndexPath is called before heightForRowAtIndexPath

I have breakpoints in both the two methods. The cellForRowAtIndexPath is always called before heightForRowAtIndexPath.
The problem is I have a logic code in cellForRowAtIndexPath that depends on the row height.
Currently, the row height I get inside cellForRowAtIndexPath is always 0.
Is there a way to let heightForRowAtIndexPath to be called before cellForRowAtIndexPath?
If not, how to call heightForRowAtIndexPatch explicitly in my code?
EDIT:
Here is my code for cellForRowAtIndexPath:
let slideshowCell = tableView.dequeueReusableCell(withIdentifier: "SlideShowCell") as! SlideShowTableViewCell
slideshowCell.delegate = self
slideshowCell.news = featuredNews
return slideshowCell
and here is my code for heightForRowAtIndexPatch:
let filterApplied = categoryNews.count != filteredCategoryNews.count
if featuredNews.count == 0 || filterApplied
{
return 0
}
else
{
return 530
}
and when I set featuredNews to slideshowCell.news, I run this method in the slideshowCell.news's didSet property observer:
private func populateSlideshowScrollView()
{
let imagesCount = CGFloat(slideshowNews.count)
// set the scrollView's content size to the size of all the images
scrollView.contentSize = CGSize(width: scrollView.frame.width * imagesCount, height: scrollView.frame.height)
// Remove subviews of ScrollView before populating it with new images
for subview in scrollView.subviews
{
subview.removeFromSuperview()
}
// populate the scrollView with images form the 'slideshowNews' property
for i in 0..<slideshowNews.count
{
let imgView = UIImageView(frame: CGRect(x: scrollView.frame.width * CGFloat(i),
y: scrollView.bounds.origin.y,
width: scrollView.frame.width,
height: scrollView.frame.height))
imgView.contentMode = .scaleAspectFill
imgView.clipsToBounds = true
imgView.image = slideshowNews[i].image
scrollView.addSubview(imgView)
}
// set scrollview to the first page from the right, only the first two times this method called.
// After that the current page of page control should be in the correct position
if numberOfRefresh < 2
{
scrollView.setContentOffset(CGPoint(x: scrollView.frame.width * (imagesCount - 1), y: 0), animated: false)
newsTitleLabel.text = slideshowNews.last?.title
sourceLabel.text = slideshowNews.last?.source.name
numberOfRefresh += 1
}
scrollToPage(page: pageControl.currentPage, animated: false) // to update scrollView's contentOffset
}
The problem is that scrollView.frame.height will always return zero, because heightForRowAtIndexPath is not yet called at this point.

iOS Swift 3: UICollectionView horizontal center and bigger cell

I want to build an collection view like this one:
Collection View
which has bigger cell at the center and cell is snapped to center of container view, but with Swift 3. I don't want to use library since I want to learn how to build a custom Collection View like this.
I've searched over SO but not found any appropriate solution yet
write that function
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.size.width, height: collectionView.frame.size.height)
}
make collection view [scroll Direction] Horizontal
and [scrolling] tick scrolling enable and paging enable
make cell biger
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
let offSet = self.collectionView.contentOffset
let width = self.collectionView.bounds.size.width
let index = round(offSet.x / width)
let newPoint = CGPoint(x: index * size.width, y: offSet.y)
coordinator.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
},completion: {(UIVIewTransitionCoordinatorContext) in
self.collectionView.reloadData()
self.collectionView.setContentOffset(newPoint, animated: true)
})
}
To achieve this you will need to subclass UICollectionViewFlowLayout and override:
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
then call super.layoutAt... and alter the cell it returns via its .transform attribute and return your altered attributes
Here is an example I made previously.
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var att = super.layoutAttributesForElements(in: rect)!
if !(delegate?.reelPlayerFlowIsPreviewMode() ?? true) {
return att
}
let region = CGRect(x: (self.collectionView?.contentOffset.x)!,
y: (self.collectionView?.contentOffset.y)!,
width: (self.collectionView?.bounds.size.width)!,
height: (self.collectionView?.bounds.size.height)!)
let center = CGPoint(x: region.midX, y: region.midY)
for theCell in att {
print("\(theCell.indexPath.item)\n\(theCell)\n")
let cell = theCell.copy() as! UICollectionViewLayoutAttributes
var f = cell.frame
let cellCenter = CGPoint(x: f.midX, y: f.midY)
let realDistance = min(center.x - cellCenter.x, region.width)
let distance = abs(realDistance)
let d = (region.width - distance) / region.width
let p = (max(d, ReelPlayerFlowLayout.minPercent) * ReelPlayerFlowLayout.maxPercent)
f.origin.x += (realDistance * ((1 - ReelPlayerFlowLayout.maxPercent) + (ReelPlayerFlowLayout.maxPercent - ReelPlayerFlowLayout.minPercent)))
cell.frame = f
cell.size = CGSize (width: f.width * p, height: f.height * p)
let index = att.index(of: theCell)!
att[index] = cell
}
return att
}

Swift: paging by screen doesn't work for two column layout collection view

I have a collectionview controller with custom 2-columns layout:
class CVLayout: UICollectionViewFlowLayout {
override init() {
super.init()
setupLayout()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupLayout()
}
override var itemSize: CGSize {
set {
}
get {
let numberOfColumns: CGFloat = 2
let itemWidth = (self.collectionView!.frame.width - (numberOfColumns - 1)) / numberOfColumns
return CGSize(width: itemWidth, height: itemWidth)
}
}
func setupLayout() {
minimumInteritemSpacing = 1
minimumLineSpacing = 1
scrollDirection = .horizontal
}
}
In my controller I set
class ViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
collectionView?.isScrollEnabled = true
collectionView?.isPagingEnabled = true
}
I have 7 items, 6 items are displayed on the screen and I expect to see one item on the next page, but instead I see 4 (3 from the previous page)
I tried to play with number of sections and items in section, but it doesn't help. What I've found from other topics is that default paging should be by screen, not by cell (in my case a column). What am I doing wrong?
If I understand your question, I do not think you are doing anything wrong per se. The content size of the collection view is determined (in part) by the item size. Right now that is approximately the width of 3 items (or a page and a half of content). I think you need to override the collection view's content size property to reflect full pages of content (in width) to achieve the desired effect.
override var collectionViewContentSize: CGSize {
if let collectionView = collectionView {
let numberOfItems = collectionView.numberOfItems(inSection: 0)
let pages = numberOfItems/2
let size = CGSize(width: collectionView.frame.width * CGFloat(pages), height: collectionView.frame.height)
return size
}
return CGSize(width: 0, height: 0)
}
Please see this answer How to expand UICollectionView contentSize when paging enable?. It is a bit old but I think it is trying to solve the same issue.
First of all UICollectionViewFlowLayout doesn't support paging
There are two solution that i think
first
use UIScrollView add 7 items simply with paging option
second
use UICollectionView modifying your UICollectionViewFlowLayout
subclass UICollectionViewFlowLayout to support paging
ex)
In Swift3
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if abs(itemOffset - horizontalOffset) < abs(offsetAdjustment){
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
you can find more information searching UICollectionViewFlowLayout with Paging

Resources