I'm sorry for the title, can not synthesize differently the issue. Here's the problem:
In my UICollectionView have some cells that want to put a shadow, they are arranged very close to each other which makes the shadow of each one reach the neighbor (first image), when what I want, is that it only reaches the background (second image).
What I've thought or tried:
I can not put a view behind the cells, adding their frames to it, and apply shadow in this view because the cells has dynamic movement (UIDynamics CollectionView Layout).
I tried, in the subclass of UICollectionViewLayout, put all these cells in the same z-index. Did not work. Find out why:
var zIndex: Int
(...) Items with the same value have an undetermined
order.
I would like some help with my problem, please. Thanks!
From the TheEye response, I decided to implement UIDecorationViews. All great now.
// MARK: At UICollectionViewCustomLayout:
public override init() {
super.init()
// Register the NIB of the view that will hold the shadows:
let nib = UINib(nibName: "Shadow", bundle: nil)
self.registerNib(nib, forDecorationViewOfKind: "shadow")
}
public override func layoutAttributesForDecorationViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let layoutAtt: UICollectionViewLayoutAttributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: "shadow", withIndexPath: indexPath)
layoutAtt.frame = (layoutAttributesForItemAtIndexPath(indexPath)?.frame)!
layoutAtt.zIndex = -1
return layoutAtt
}
public override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var atts = [UICollectionViewLayoutAttributes]()
let numberOfItems:Int = (self.collectionView?.numberOfItemsInSection(0))!
for index in 0..<numberOfItems {
let layoutItem = layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: index, inSection: 0))!
let frameItem = layoutItem.frame
if CGRectIntersectsRect(frameItem, rect) {
atts.append(layoutAttributesForDecorationViewOfKind("shadow", atIndexPath: NSIndexPath(forItem: index, inSection: 0))!)
atts.append(layoutItem)
}
}
return atts
}
// MARK: At the Nib Class File:
// Here I created a additional view to append the shadow.
// Thats because awakeFromNib() is called before the UICollectionViewCustomLayout
// have a chance to layout it, but I want to make
// use of shadowPath to gain performance, thats why
// I make use of the additional UIView with autolayout stuffs.
#IBOutlet weak var shadowView: UIView!
override func awakeFromNib() {
super.awakeFromNib()
shadowView.layer.shadowOffset = CGSizeMake(0, 0)
shadowView.layer.shadowColor = UIColor.yellowColor().CGColor
shadowView.layer.shadowRadius = 3
shadowView.layer.shadowOpacity = 1
let shadowFrame: CGRect = shadowView.layer.bounds
let shadowPath: CGPathRef = UIBezierPath(rect: shadowFrame).CGPath
shadowView.layer.shadowPath = shadowPath
shadowView.clipsToBounds = false
self.clipsToBounds = false
}
You could try to define the shadows as supplementary views, aligned with their respective cells, and give them a lower z order than the cells.
Related
I'm trying to display a list of recepcies for at pet proyect, which I've modeled with a UICollectionView and for each item, a custom UICollectionViewCell.
This UICollectionViewCell has inside a UIImageView and a custom UIView for layout pruposes.
The tree goes like this
HomeViewController (UIViewController)
└── UICollectionView
└── RecepieCardCollectionViewCell (UICollectionViewCell)
├── UIImageView
└── RecepieCardInfoView (UIView)
└── UILabel
I'm comforming to the UICollectionViewDelegate and UICollectionViewDataSource protocols at the HomeViewController, and then I'm configuring the cells and their nested UIViews though a configure method
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: RecepieCardCollectionViewCell.identifier, for: indexPath
) as? RecepieCardCollectionViewCell else {
return UICollectionViewCell()
}
cell.configure(self.recepies[indexPath.row])
return cell
}
Then, at the RecepieCardCollectionViewCell:
func configure(_ recepie: Recepie) {
cardInfoView.configure(recepie)
thumbnail.kf.setImage(with: URL(string: recepie.getCover()))
}
And, finally, at the RecepieCardInfoView
func configure(_ recepie: Recepie) {
label.text = recepie.getName()
}
The thing is that the most deep nested UILabel at the RecepieCardInfoView, is receiving the data, but is not being updated. It always shows the same placeholder text, instead of the actual recepie name.
Things I've tried:
Placing .setNeedsDisplay() to all the element, with no results.
Using DispatchQueue.main.async but didn't work, which makes sense since I'm not using an API request to show the recepies (for now).
Move the label inside RecepieCardInfoView to the RecepieCardCollectionViewCell. This worked for some reason, but I would like to understand why.
If you need more context of the code, you can find the full repository here at the branch feat/recepies-list
I've been asked to add the full code of the cell, here it is:
import UIKit
import Kingfisher
class RecepieCardCollectionViewCell: UICollectionViewCell {
static let identifier = "RecepieCardUICollectionViewCell"
let thumbnail: UIImageView = {
let image = UIImageView()
image.translatesAutoresizingMaskIntoConstraints = false
image.kf.setImage(with: URL(string: "https://i.imgur.com/ISxVZHA.png"))
return image
}()
let content: UIView = {
let view = UIView(frame: .zero)
view.clipsToBounds = true
view.layer.cornerRadius = 40
return view
}()
let cardInfoView = RecepieCardInfoView(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .themeWhite
layer.cornerRadius = 40
addSubview(content)
content.frame = bounds
content.addSubview(thumbnail)
let cardInfoView = RecepieCardInfoView(frame: .zero)
content.addSubview(cardInfoView)
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.17
layer.shadowOffset = .zero
layer.shadowRadius = 10
NSLayoutConstraint.activate([
thumbnail.leadingAnchor.constraint(equalTo: content.leadingAnchor),
thumbnail.trailingAnchor.constraint(equalTo: content.trailingAnchor),
thumbnail.topAnchor.constraint(equalTo: content.topAnchor),
thumbnail.heightAnchor.constraint(equalTo: thumbnail.widthAnchor),
cardInfoView.topAnchor.constraint(equalTo: content.bottomAnchor, constant: -100),
cardInfoView.bottomAnchor.constraint(equalTo: content.bottomAnchor),
cardInfoView.leadingAnchor.constraint(equalTo: content.leadingAnchor),
cardInfoView.trailingAnchor.constraint(equalTo: content.trailingAnchor),
])
}
required init(coder: NSCoder) {
fatalError()
}
func configure(_ recepie: Recepie) {
cardInfoView.configure(recepie)
thumbnail.kf.setImage(with: URL(string: recepie.getCover()))
}
}
Thanks you in advance!
You create 2 instances of cardInfoView inside RecepieCardCollectionViewCell
let cardInfoView = RecepieCardInfoView(frame: .zero) // here 1
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .themeWhite
layer.cornerRadius = 40
addSubview(content)
content.frame = bounds
content.addSubview(thumbnail)
let cardInfoView = RecepieCardInfoView(frame: .zero) // and here 2
The problem is that you add the inner view without updating it's content , and update the outer view without adding it to cell hierarchy
I have a simple problem that I cannot find a solution to on google, the docs or here.
I have a Collectionview in my view controller. I have created a custom cell, DescriptionCell, that contains a UIImage. I want this image to have rounded corners. However, I don't know where to set the cornerradius on the UIImage layer. I have tried in the cells' awakeFromNib method, in the delegate method CellForRowAtIndexPath and overriden LayoutSubview in the cell but it does not work.
Where should I put the code to set the radius for the UIImage?
To specify, I know how to create rounded corners of a UIImage. But if it is a subview of a Collectionview cell, I do not know where to set the cornerradius.
Here is code for my descriptionCell
class DescriptionCell: UICollectionViewCell {
#IBOutlet weak var mImage: UIImageView!
override func awakeFromNib() {
//mImage.layer.cornerradius = 5
//Does not work, the image is still square in the cell
}
And in the cellforRowAtIndePath
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("descriptioncell", forIndexPath: indexPath) as! DescriptionCell
//cell.mImage.layer.cornerradius = 5
//Does not work, the image is still square in the cell
return cell
}
Thanks in advance!
Well you're using part of the code from the answer you said you were using.
the other part is imageView.clipsToBounds = true
Update your awakeFromNib like this:
override func awakeFromNib() {
mImage.layer.cornerRadius = 5
mimage.clipsToBounds = true
}
To make it a circle you need to set cornerRadius to half of the square height. In your cellForItemAtIndexPath add these lines:
cell.layoutIfNeeded()
cell.mImage.layer.cornerRadius = cell.mImage.frame.height/2
Update
To avoid layoutSubviews from being called twice, override layoutSubviews in your DescriptionCell class and put the code there:
override func layoutSubviews() {
super.layoutSubviews()
layoutIfNeeded()
mImage.layer.cornerRadius = mImage.frame.height/2
}
Have you tried placing it inside the custom UICollectionViewCell's init function?
override init(frame: CGRect) {
super.init(frame: frame)
image.layer.masksToBounds = true
image.layer.cornerRadius = 10
}
You can also create an extension like:
extension UIView {
func addBorder(color: UIColor, cornerRadius: CGFloat = 10, borderWidth: CGFloat = 1.0) {
layer.borderWidth = borderWidth;
layer.borderColor = color.cgColor
layer.cornerRadius = cornerRadius
layer.masksToBounds = true
}
}
I recently posted a question about a UITableView with custom UITableCells that was not smooth when the cell's subviews were positioned with AutoLayout. I got some comments suggesting the lack of smoothness was due to the complex layout of the cells. While I agree that the more complex the cell layout, the more calculation the tableView has to do to get the cell's height, I don't think 10-12 UIView and UILabel subviews should cause the amount of lag I was seeing as I scrolled on an iPad.
So to prove my point further, I created a single UIViewController project with a single UITableView subview and custom UITableViewCells with only 2 labels inside of their subclass. And the scrolling is still not perfectly smooth. From my perspective, this is the most basic you can get - so if a UITableView is still not performant with this design, I must be missing something.
The estimatedRowHeight of 110 used below is a very close estimate to the actual row height average. When I used the 'User Interface Inspector' and looked at the heights of each cell, one by one, they ranged from 103 - 124.
Keep in mind, when I switch the code below to not use an estimatedRowHeight and UITableViewAutomaticDimension and instead implement func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {, calculating the height with frame values, the UITableView scrolls like butter.
Screenshot of App (for reference)
Source code of the App (where the scrolling is not perfectly smooth)
// The custom `Quote` object that holds the
// properties for our data mdoel
class Quote {
var text: String!
var author: String!
init(text: String, author: String) {
self.text = text
self.author = author
}
}
// Custom UITableView Cell, using AutoLayout to
// position both a "labelText" (the quote itself)
// and "labelAuthor" (the author's name) label
class CellQuote: UITableViewCell {
private var containerView: UIView!
private var labelText: UILabel!
private var labelAuthor: UILabel!
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.backgroundColor = UIColor.whiteColor()
containerView = UIView()
containerView.backgroundColor = UIColor(
red: 237/255,
green: 237/255,
blue: 237/255,
alpha: 1.0
)
contentView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.leadingAnchor.constraintEqualToAnchor(contentView.leadingAnchor, constant: 0).active = true
containerView.trailingAnchor.constraintEqualToAnchor(contentView.trailingAnchor, constant: 0).active = true
containerView.topAnchor.constraintEqualToAnchor(contentView.topAnchor, constant: 4).active = true
containerView.bottomAnchor.constraintEqualToAnchor(contentView.bottomAnchor, constant: 0).active = true
labelText = UILabel()
labelText.numberOfLines = 0
labelText.font = UIFont.systemFontOfSize(18)
labelText.textColor = UIColor.darkGrayColor()
containerView.addSubview(labelText)
labelText.translatesAutoresizingMaskIntoConstraints = false
labelText.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor, constant: 20).active = true
labelText.topAnchor.constraintEqualToAnchor(containerView.topAnchor, constant: 20).active = true
labelText.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor, constant: -20).active = true
labelAuthor = UILabel()
labelAuthor.numberOfLines = 0
labelAuthor.font = UIFont.boldSystemFontOfSize(18)
labelAuthor.textColor = UIColor.blackColor()
containerView.addSubview(labelAuthor)
labelAuthor.translatesAutoresizingMaskIntoConstraints = false
labelAuthor.topAnchor.constraintEqualToAnchor(labelText.bottomAnchor, constant: 20).active = true
labelAuthor.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor, constant: 20).active = true
labelAuthor.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor, constant: -20).active = true
labelAuthor.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor, constant: -20).active = true
self.selectionStyle = UITableViewCellSelectionStyle.None
}
func configureWithData(quote: Quote) {
labelText.text = quote.text
labelAuthor.text = quote.author
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// The UIViewController that is a
class ViewController: UIViewController, UITableViewDataSource {
var tableView: UITableView!
var dataItems: [Quote]!
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView()
tableView.dataSource = self
tableView.registerClass(CellQuote.self, forCellReuseIdentifier: "cellQuoteId")
tableView.backgroundColor = UIColor.whiteColor()
tableView.separatorStyle = UITableViewCellSeparatorStyle.None
tableView.estimatedRowHeight = 110
tableView.rowHeight = UITableViewAutomaticDimension
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor).active = true
tableView.topAnchor.constraintEqualToAnchor(view.topAnchor, constant: 20).active = true
tableView.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor).active = true
tableView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor).active = true
dataItems = [
Quote(text: "One kernel is felt in a hogshead; one drop of water helps to swell the ocean; a spark of fire helps to give light to the world. None are too small, too feeble, too poor to be of service. Think of this and act.", author: "Michael.Frederick"),
Quote(text: "A timid person is frightened before a danger, a coward during the time, and a courageous person afterward.", author: "Lorem.Approbantibus."),
Quote(text: "There is only one way to defeat the enemy, and that is to write as well as one can. The best argument is an undeniably good book.", author: "Lorem.Fruitur."),
// ... many more quotes ...
]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - UITableViewDataSource
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataItems.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cellQuoteId") as! CellQuote
cell.configureWithData(dataItems[indexPath.row])
return cell
}
}
I like matt's suggestion below, but am still trying to tweak it to work for me:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var cellHeights: [CGFloat] = [CGFloat]()
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if cellHeights.count == 0 {
var cellHeights = [CGFloat]()
let numQuotes: Int = dataItems.count
for index in 0...numQuotes - 1 {
let cell = CellQuote()
let quote = dataItems[index]
cell.configureWithData(quote)
let size = cell.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
cellHeights.append(size.height)
}
self.cellHeights = cellHeights
}
return self.cellHeights[indexPath.row]
}
}
I've never found the automatic row height mechanism to be as smooth as the old calculated layout techniques that we used to use before auto layout came along. The bottleneck, as you can readily see by using Instruments, is that the runtime must call systemLayoutSizeFittingSize: on every new cell as it scrolls into view.
In my book, I demonstrate my preferred technique, which is to calculate the heights for all the cells once when the table view first appears. This means that I can supply the answer to heightForRowAtIndexPath instantly from then on, making for the best possible user experience. Moreover, if you then replace your call to dequeueReusableCellWithIdentifier with the much better and more modern dequeueReusableCellWithIdentifier:forIndexPath, you have the advantage that the cell comes to you with its correct size and no further layout is needed after that point.
the clear background from your two text labels is causing the performance issues. add these lines and you should see a performance increase:
labelText.backgroundColor = containerView.backgroundColor
labelAuthor.backgroundColor = containerView.backgroundColor
a good way to check any other potential issues is by turning on 'Color Blended Layers' in the iOS Simulator's 'Debug' menu option
UPDATE
usually what i do for dynamic cell heights is create a prototype cell and use it for sizing. here is what you'd do in your case:
class CellQuote: UITableViewCell {
private static let prototype: CellQuote = {
let cell = CellQuote(style: .Default, reuseIdentifier: nil)
cell.contentView.translatesAutoresizingMaskIntoConstraints = false
return cell
}()
static func heightForQuote(quote: Quote, tableView:UITableView) -> CGFloat {
prototype.configureWithData(quote)
prototype.labelText.preferredMaxLayoutWidth = CGRectGetWidth(tableView.frame)-40
prototype.labelAuthor.preferredMaxLayoutWidth = CGRectGetWidth(tableView.frame)-40
prototype.layoutIfNeeded();
return CGRectGetHeight(prototype.contentView.frame)
}
// existing code here
}
in your viewDidLoad remove the rowHeight and estimatedRowHeight lines and replace with becoming the delegate
class ViewController {
override func viewDidLoad() {
// existing code
self.tableView.delegate = self
// existing code
}
// get prototype cell height
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let quote = dataItems[indexPath.row]
return CellQuote.heightForQuote(quote, tableView: tableView)
}
I would like to populate UICollectionView in reverse order so that the last item of the UICollectionView fills first and then the second last and so on. Actually I'm applying animation and items are showing up one by one. Therefore, I want the last item to show up first.
Swift 4.2
I found a simple solution and worked for me to show last item first of a collection view:
Inside viewDidLoad() method:
collectionView.transform = CGAffineTransform.init(rotationAngle: (-(CGFloat)(Double.pi)))
and inside collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) method before returning the cell:
cell.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
(optional) Below lines will be necessary to auto scroll and show new item with smooth scroll.
Add below lines after loading new data:
if self.dataCollection.count > 0 {
self.collectionView.scrollToItem(at: //scroll collection view to indexpath
NSIndexPath.init(row:(self.collectionView?.numberOfItems(inSection: 0))!-1, //get last item of self collectionview (number of items -1)
section: 0) as IndexPath //scroll to bottom of current section
, at: UICollectionView.ScrollPosition.bottom, //right, left, top, bottom, centeredHorizontally, centeredVertically
animated: true)
}
I'm surprised that Apple scares people away from writing their own UICollectionViewLayout in the documentation. It's really very straightforward. Here's an implementation that I just used in an app that will do exactly what are asking. New items appear at the bottom, and the while there is not enough content to fill up the screen the the items are bottom justified, like you see in message apps. In other words item zero in your data source is the lowest item in the stack.
This code assumes that you have multiple sections, each with items of a fixed height and no spaces between items, and the full width of the collection view. If your layout is more complicated, such as different spacing between sections and items, or variable height items, Apple's intention is that you use the prepare() callback to do the heavy lifting and cache size information for later use.
This code uses Swift 3.0.
//
// Created by John Lyon-Smith on 1/7/17.
// Copyright © 2017 John Lyon-Smith. All rights reserved.
//
import Foundation
import UIKit
class InvertedStackLayout: UICollectionViewLayout {
let cellHeight: CGFloat = 100.00 // Your cell height here...
override func prepare() {
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttrs = [UICollectionViewLayoutAttributes]()
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numberOfSectionItems = numberOfItemsInSection(section) {
for item in 0 ..< numberOfSectionItems {
let indexPath = IndexPath(item: item, section: section)
let layoutAttr = layoutAttributesForItem(at: indexPath)
if let layoutAttr = layoutAttr, layoutAttr.frame.intersects(rect) {
layoutAttrs.append(layoutAttr)
}
}
}
}
}
return layoutAttrs
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let layoutAttr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let contentSize = self.collectionViewContentSize
layoutAttr.frame = CGRect(
x: 0, y: contentSize.height - CGFloat(indexPath.item + 1) * cellHeight,
width: contentSize.width, height: cellHeight)
return layoutAttr
}
func numberOfItemsInSection(_ section: Int) -> Int? {
if let collectionView = self.collectionView,
let numSectionItems = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: section)
{
return numSectionItems
}
return 0
}
override var collectionViewContentSize: CGSize {
get {
var height: CGFloat = 0.0
var bounds = CGRect.zero
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numItems = numberOfItemsInSection(section) {
height += CGFloat(numItems) * cellHeight
}
}
bounds = collectionView.bounds
}
return CGSize(width: bounds.width, height: max(height, bounds.height))
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if let oldBounds = self.collectionView?.bounds,
oldBounds.width != newBounds.width || oldBounds.height != newBounds.height
{
return true
}
return false
}
}
Just click on UICollectionView in storyboard,
in inspector menu under view section change semantic to Force Right-to-Left
I have attach an image to show how to do it in the inspector menu:
I'm assuming you are using UICollectionViewFlawLayout, and this doesn't have logic to do that, it only works in a TOP-LEFT BOTTOM-RIGHT order. To do that you have to build your own layout, which you can do creating a new object that inherits from UICollectionViewLayout.
It seems like a lot of work but is not really that much, you have to implement 4 methods, and since your layout is just bottom-up should be easy to know the frames of each cell.
Check the apple tutorial here: https://developer.apple.com/library/prerelease/ios/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CreatingCustomLayouts/CreatingCustomLayouts.html
The data collection does not actually have to be modified but that will produce the expected result. Since you control the following method:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
Simply return cells created from inverting the requested index. The index path is the cell's index in the collection, not necessarily the index in the source data set. I used this for a reversed display from a CoreData set.
let desiredIndex = dataProfile!.itemEntries!.count - indexPath[1] - 1;
Don't know if this still would be useful but I guess it might be quite useful for others.
If your collection view's cells are of the same height there is actually a much less complicated solution for your problem than building a custom UICollectionViewLayout.
Firstly, just make an outlet of your collection view's top constraint and add this code to the view controller:
-(void)viewWillAppear:(BOOL)animated {
[self.view layoutIfNeeded]; //for letting the compiler know the actual height and width of your collection view before we start to operate with it
if (self.collectionView.frame.size.height > self.collectionView.collectionViewLayout.collectionViewContentSize.height) {
self.collectionViewTopConstraint.constant = self.collectionView.frame.size.height - self.collectionView.collectionViewLayout.collectionViewContentSize.height;
}
So basically you calculate the difference between collection view's height and its content only if the view's height is bigger. Then you adjust it to the constraint's constant. Pretty simple. But if you need to implement cell resizing as well, this code won't be enough. But I guess this approach may be quite useful. Hope this helps.
A simple working solution is here!
// Change the collection view layer transform.
collectionView.transform3D = CATransform3DMakeScale(1, -1, 1)
// Change the cell layer transform.
cell.transform3D = CATransform3DMakeScale(1, -1, 1)
It is as simple as:
yourCollectionView.inverted = true
PS : Same for Texture/IGListKit..
I need a UICollectionView to display a grid that is potentially larger than the visible frame in both width and height, while maintaining row and column integrity. The default UICollectionViewFlowLayout allows sections to scroll off-screen, but it wraps items within a section to keep them all on-screen, which screws up my grid.
Recognizing that UICollectionView is a subclass of UIScrollView, I tried just manually setting the collection view's content size property in viewDidLoad:
self.collectionView.contentSize = CGSizeMake((columns * (cellWidth + itemSpacingX), (rows * (cellHeight + itemSpacingY));
But this had no effect. Questions:
Is there an easy way to accomplish this without building a custom layout?
Would using a custom layout and overriding the collectionViewContentSize method succeed in getting the collection view to stop wrapping items and scroll in both directions?
If I do have to build a custom layout--which I'll have to invest some time to learn--do I subclass UICollectionViewLayout, or would subclassing UICollectionViewFlowLayout save time?
UPDATE:
I tried embedding the UICollectionView as a subview of a UIScrollView. The collection view itself is behaving correctly--the rows aren't wrapping at the edge of the scroll view, telling me that it is filling the UIScrollView content size that I set. But the scroll view doesn't pan in the horizontal direction, i.e. it only scrolls vertically, which the collection view does by itself anyway. So stuck again. Could there be an issue with the responder chain?
Figured out two ways to do this. Both required a custom layout. The problem is that the default flow layout--and I now know from the Collection View Programming Guide this is partly the definition of a flow layout--generates the cell layout attributes based on the bounds of the superview, and will wrap items in a section to keep them in bounds so that scrolling occurs in only one axis. Will skip the code details, as it isn't hard, and my problem was mainly confusion on what approach to take.
Easy way: use a UIScrollView and subclass 'UICollectionViewFlowLayout'. Embed the UICollectionView in a UIScrollView. Set the contentSize property of the scroll view in viewDiDLoad to match the full size that your collection view will occupy (this will let the default flow layout place items in a single line within a section without wrapping). Subclass UICollectionViewFlowLayout, and set that object as your custom layout for the collection view. In the custom flow layout, override collectionViewContentSize to return the full size of the collection view matrix. With this approach, you'll be using a flow layout, but will be able to scroll in both directions to view un-wrapped sections. The disadvantage is that you still have a flow layout that is pretty limited. Plus, it seems clunky to put a UICollectionView inside an instance of its own superclass just to get the functionality that the collection view by itself should have.
Harder way, but more versatile and elegant: subclass UICollectionViewLayout. I used this tutorial to learn how to implement a complete custom layout. You don't need a UIScrollView here. If you forego the flow layout, subclass UICollectionViewLayout, and set that as the custom layout, you can build out the matrix and get the right behavior from the collection view itself. It's more work because you have to generate all the layout attributes, but you'll be positioned to make the collection view do whatever you want.
In my opinion, Apple should add a property to the default flow layout that suppresses wrapping. Getting a device to display a 2D matrix with intact rows and columns isn't an exotic functionality and it seems like it should be easier to do.
Here is a version of AndrewK's code updated to Swift 4:
import UIKit
class CollectionViewMatrixLayout: UICollectionViewLayout {
var itemSize: CGSize
var interItemSpacingY: CGFloat
var interItemSpacingX: CGFloat
var layoutInfo: [IndexPath: UICollectionViewLayoutAttributes]
override init() {
itemSize = CGSize(width: 50, height: 50)
interItemSpacingY = 1
interItemSpacingX = 1
layoutInfo = [IndexPath: UICollectionViewLayoutAttributes]()
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
guard let collectionView = self.collectionView else {
return
}
var cellLayoutInfo = [IndexPath: UICollectionViewLayoutAttributes]()
var indexPath = IndexPath(item: 0, section: 0)
let sectionCount = collectionView.numberOfSections
for section in 0..<sectionCount {
let itemCount = collectionView.numberOfItems(inSection: section)
for item in 0..<itemCount {
indexPath = IndexPath(item: item, section: section)
let itemAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
itemAttributes.frame = frameForCell(at: indexPath)
cellLayoutInfo[indexPath] = itemAttributes
}
self.layoutInfo = cellLayoutInfo
}
}
func frameForCell(at indexPath: IndexPath) -> CGRect {
let row = indexPath.section
let column = indexPath.item
let originX = (itemSize.width + interItemSpacingX) * CGFloat(column)
let originY = (itemSize.height + interItemSpacingY) * CGFloat(row)
return CGRect(x: originX, y: originY, width: itemSize.width, height: itemSize.height)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
var allAttributes = Array<UICollectionViewLayoutAttributes>()
for (_, attributes) in self.layoutInfo {
if (rect.intersects(attributes.frame)) {
allAttributes.append(attributes)
}
}
return allAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return self.layoutInfo[indexPath]
}
override var collectionViewContentSize: CGSize {
guard let collectionView = self.collectionView else {
return .zero
}
let sectionCount = collectionView.numberOfSections
let height = (itemSize.height + interItemSpacingY) * CGFloat(sectionCount)
let itemCount = Array(0..<sectionCount)
.map { collectionView.numberOfItems(inSection: $0) }
.max() ?? 0
let width = (itemSize.width + interItemSpacingX) * CGFloat(itemCount)
return CGSize(width: width, height: height)
}
}
Here is complete matrix customLayout:
import UIKit
class MatrixLayout: UICollectionViewLayout {
var itemSize: CGSize!
var interItemSpacingY: CGFloat!
var interItemSpacingX: CGFloat!
var layoutInfo: Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
itemSize = CGSizeMake(50.0, 50.0)
interItemSpacingY = 1.0
interItemSpacingX = 1.0
}
override func prepareLayout() {
var cellLayoutInfo = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>()
let sectionCount = self.collectionView?.numberOfSections()
var indexPath = NSIndexPath(forItem: 0, inSection: 0)
for (var section = 0; section < sectionCount; section += 1)
{
let itemCount = self.collectionView?.numberOfItemsInSection(section)
for (var item = 0; item < itemCount; item += 1)
{
indexPath = NSIndexPath(forItem:item, inSection: section)
let itemAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
itemAttributes.frame = frameForCellAtIndexPath(indexPath)
cellLayoutInfo[indexPath] = itemAttributes
}
self.layoutInfo = cellLayoutInfo
}
}
func frameForCellAtIndexPath(indexPath: NSIndexPath) -> CGRect
{
let row = indexPath.section
let column = indexPath.item
let originX = (self.itemSize.width + self.interItemSpacingX) * CGFloat(column)
let originY = (self.itemSize.height + self.interItemSpacingY) * CGFloat(row)
return CGRectMake(originX, originY, self.itemSize.width, self.itemSize.height)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
var allAttributes = Array<UICollectionViewLayoutAttributes>()
for (index, attributes) in self.layoutInfo
{
if (CGRectIntersectsRect(rect, attributes.frame))
{
allAttributes.append(attributes)
}
}
return allAttributes
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return self.layoutInfo[indexPath]
}
override func collectionViewContentSize() -> CGSize {
let width:CGFloat = (self.itemSize.width + self.interItemSpacingX) * CGFloat((self.collectionView?.numberOfItemsInSection(0))!)
let height:CGFloat = (self.itemSize.height + self.interItemSpacingY) * CGFloat((self.collectionView?.numberOfSections())!)
return CGSizeMake(width, height)
}
}
sections are rows,
items are columns