I want to draw line in between two Cell in UIcollectionView,
Below is design what exactly I want
Above design is dynamically change depends on array value.Please tell me if any one know how to draw line .
Well this is a request, as you've shown no approach so far, but here I am responding to your post with a solution which works with the collectionView
UI
Add a UICollectionView in your controller which lets say has a height of 100pt and its padding to [left, right, top] is 0. (Configurable via Autolayout)
Inside the UICollectionView add UICollectionViewCell which has 3 elements
A UIView (Or your custom UIImageView which will create your yellow line. It needs to be vertically centered, leading and trailing to superview equal to 0 and the height lets say 5pt
A UIImageView which will be centered and depending on the node, it will show start, progress or end node. It needs to be centered horizontally and vertically, and a custom height and width.
A UILabel which will show the node names (if start or end it needs to be hidden). Put a leading and trailing margin to 8pt and center it vertically.
View tree should look something like this:
Code
The idea is that each node of your route will be represented by a similar cell, and without leaving leaving spaces between UICollectionViewCells you will have a similar effect.
Note: I didn't create a custom cell, and that should be something that
you have to do it on your own, where you will be able to change the
content depending on your data. And let this be a task for you to
learn :)
class ViewController: UIViewController, UICollectionViewDataSource {
#IBOutlet weak var collectionView: UICollectionView!
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
}
// MARK: - Configurations
private func setupCollectionView() {
// Set datasource
collectionView.dataSource = self
// Set flow layout
let layout:UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.itemSize = CGSize(width: 100, height: 100.0)
layout.scrollDirection = .horizontal
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView.collectionViewLayout = layout
}
// MARK: - Protocol Conformance
// MARK: UICollectionViewDataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:"CellIdentifier", for: indexPath)
if indexPath.item == 0 {
// This is the start of the route
} else if indexPath.item == (collectionView.numberOfSections - 1) {
// This the the end of it
} else {
// Other nodes on your route
}
return cell
}
}
Output
Option 1
Create a view that has a horizontal line through the middle.
Add it as a subview to your view controller root view.
Create a collection view without a line. Make the background transparent.
Add it as a subview to your view controller root view.
Make sure both views are the same size.
Option 2
Create a custom UICollectionViewCell that is square in shape.
Position your circular icon in the middle of the cell.
Draw a yellow line through the cell.
Make the rest of the cell transparent, with no edges, so all you see is the icon and the yellow line.
Layout your square shaped cells so that there are no gaps in between.
Option 3
Create a custom UICollectionViewCell that is square in shape.
Position your circular icon in the middle of the cell.
Draw a yellow line through the cell.
Create a UICollectionReusableView subclass that will act as a decoration view. Draw the same yellow line here (you may want to make your 'yellow line drawing code' re-usable).
Create a custom layout that positions your cells side by side, with decoration views in between.
This option allows you to be more creative. Maybe you want to dynamically change the distance between each cell with an animation? Or maybe you want to increase the gap between just a single pair of cells? To do this, you would setup multiple layouts that differ slightly in their implementation (or a single layout that is configurable) and swap between instances thereof.
This is my most recent custom collection view layout implementation. It's a grid layout with section header views, so not exactly what you want, but it should get you started.
Other Refs
Create Custom Layout
Swap Between Custom Layouts
Related
I found the next answer to make UIView's height match its content https://stackoverflow.com/a/39527226/7767664
I tested it, it works fine (if UIView height size in storyboard bigger or smaller than its content then during runtime it autoresize itself to match the content).
But if I use UICollectionViewCell instead of UIView then nothing changes, height of cell is never changed, it always has the hardcoded height we have in storyboard properties:
What else can I do?
Also I have 2 sections in UIControllerView (2 columns).
Two cells in one row should have the same size even if their size content is different (something like this implemented in Android natively when using RecyclerView with GridLayoutManager, very easy)
Update
It works with UIView because I set its top constraint to Safe Are'a top
I can't do it with UICollectionViewCell
Update 2
It seems I have some progress with this answer https://stackoverflow.com/a/25896386/7767664
But instead of newFrame.size.width = CGFloat(ceilf(Float(size.width))) I need newFrame.size.height = CGFloat(ceilf(Float(size.height)))
and when we use this solution, don't add any constraints to cell's bottom otherwise it will not work
With this solution I can't really use any margins otherwise some part of becomes invisible at the bottom of cell
I guess it can be solved with this question https://stackoverflow.com/a/31279726/7767664
You can try with this function called inside you collectionView Extension or inside your native collectionViewController class:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
//return something like the size. I write you an example how to use it.
// You can easily change the value according to your stuff contents height.
return CGSize(width: collectionView.frame.size.width/3, height: 100)
}
I solved my issue thanks to https://stackoverflow.com/a/25896386/7767664 and https://stackoverflow.com/a/31279726/7767664
I decided to put one UIView with all needed child views inside it to UICollecitonViewCell
I set UIView trailing, leading, top to ICollecitonViewCell but I didn't set bottom to cell view (you should use the latest child view inside UIView and connect their bottoms, not with the cell view)
Then I added reference of UIView to my custom cell class and I use its height for cell's height:
public class MyCustomItemCell: UICollectionViewCell {
// other child views references ...
#IBOutlet weak var wrapperView: UIView! // view which contains your other views
//forces the system to do one layout pass
var isHeightCalculated: Bool = false
override public func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
//Exhibit A - We need to cache our calculation to prevent a crash.
if !isHeightCalculated {
setNeedsLayout()
layoutIfNeeded()
var newFrame = layoutAttributes.frame
newFrame.size.height = wrapperView.frame.height // use height of our UIView
layoutAttributes.frame = newFrame
isHeightCalculated = true
}
return layoutAttributes
}
}
I don't know why it is so complicated to design cells that can adapt to its content. It shouldn't need that much code, I still don't understand why UIKit can't handle this properly.
Anyway, here is my issue (I have edited the whole post):
I have an UICollectionViewCell that contains an UITableView.
Here is my sizeForItem method :
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
var cellWidth: CGFloat = collectionView.bounds.size.width
var cellHeight: CGFloat = 0
let cellConfigurator = items[indexPath.item].cellConfigurator
if type(of: cellConfigurator).reuseId == "MoonCollectionViewCell" {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: type(of: cellConfigurator).reuseId, for: indexPath) as? MoonCollectionViewCell {
cell.contentView.layoutIfNeeded()
let size = cell.selfSizedTableView.intrinsicContentSize
cellHeight = size.height
}
}
return CGSize.init(width: cellWidth, height: cellHeight)
}
sizeForItem is called before cellForItem, that's the reason of the layoutIfNeeded, because I couldn't get the correct intrinsic content size.
I have removed the XIB as suggested, and designed my UICollectionViewCell within the Storyboard.
Here is my UICollectionViewCell designed within a Storyboard (only the UITableViewCell is designed in a XIB file)
I only added an UITableView within the UICollectionViewCell.
I want the UICollectionViewCell to adapt its size according to the height of the tableView.
Now here is my tableView :
I have created a subclass of UITableView (from this post)
class SelfSizedTableView: UITableView {
var maxHeight: CGFloat = UIScreen.main.bounds.size.height
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
self.layoutIfNeeded()
}
override var intrinsicContentSize: CGSize {
let height = min(contentSize.height, maxHeight)
return CGSize(width: contentSize.width, height: height)
}
}
Please note that I have disabled scrolling, I have dynamic prototype for the tableView cells, the style is grouped.
EDIT : Check the configure method, it comes from a protocol I used to configure in a generic way all my UICollectionViewCell
func configure(data: [MoonImages]) {
selfSizedTableView.register(UINib.init(nibName: "MoonTableViewCell", bundle: nil), forCellReuseIdentifier: "MoonTableViewCell")
selfSizedTableView.delegate = self
selfSizedTableView.dataSource = moonDataSource
var frame = CGRect.zero
frame.size.height = .leastNormalMagnitude
selfSizedTableView.tableHeaderView = UIView(frame: frame)
selfSizedTableView.tableFooterView = UIView(frame: frame)
selfSizedTableView.maxHeight = 240.0
selfSizedTableView.estimatedRowHeight = 40.0
selfSizedTableView.rowHeight = UITableView.automaticDimension
moonDataSource.data.addAndNotify(observer: self) { [weak self] in
self?.selfSizedTableView.reloadData()
}
moonDataSource.data.value = data
}
FYI the dataSource is a custom dataSource, with dynamic value (Generics) and the observer pattern, to reload the collection/tableView when the data is set.
I also have this warning when I launch the App.
[CollectionView] An attempt to update layout information was detected
while already in the process of computing the layout (i.e. reentrant
call). This will result in unexpected behaviour or a crash. This may
happen if a layout pass is triggered while calling out to a delegate.
Any hints or advice on how I should handle this ?
Because I am facing a strange behavior, it's like my sizeForItem use random values. The UICollectionViewCell height is not the same than my UITableView intrinsic content size height.
If I have 2 rows within my UITableView, the UICollectionView is not always equal at this size. I really don't know how to achieve this...
Should I invalideLayout?
Maybe it's not the answer you wanted, but here're my two cents. For your particular requirements, the better solution is moving away from UITableView, and use UIStackView or your custom container view.
Here's why:
UITableView is a subclass of UIScrollView, but since you've disabled its scrolling feature, you don't need a UIScrollView.
UITableView is mainly used to reuse cells, to improve performance and make code more structured. But since you're making it as large as its content size, none of your cells are reused, so features of UITableView is not taken any advantage of.
Thus, actually you don't need and you should not use either UITableView or UIScrollView inside the UICollectionViewCell for your requirements.
If you agree with above part, here're some learnings from our practices:
We always move most of the underlying views and code logics, mainly data assembling, into a UIView based custom view, instead of putting in UITableViewCell or UICollectionViewCell directly. Then add it to UITableViewCell or UICollectionViewCell's contentView and setup constraints. With this structure, we can reuse our custom view in more scenarios.
For requirements similar to yours, we'll create a factory class to create "rows" similar to how you create "cells" for your UITableView, add them into a vertical UIStackView, create constraints deciding UIStackView's width. Auto layout will take care of the rest things.
In your usage with UICollectionViewCell, to calculate the wanted height, inside preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) func of your cell, you can use contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) to calculate the height, do some check and return. Also, remember to invalidate layout when the width of the UICollectionView changes.
It is indeed very tricky, but I found a working way to solve this problem. As far as i know i got this from a chat app, where message bubble sizes are dynamic.
Here we go:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
// Minimum size
let frame = CGRect(x: 0, y: 0, width: view.frame.width - 30, height: 0)
let cell = MoonCollectionViewCell()
// Fill it with the content it will have in the actual cell,
// cell.content is just an example
let cell.content = items[indexPath.item]
cell.layoutIfNeeded()
// Define the maximum size it can be
let targetSize = CGSize(width: view.frame.width - 30, height: 240)
let estimatedSize = cell.systemLayoutSizeFittingSize(tagetSize)
return CGSize(width: view.frame.width - 30, height: estimatedSize.height)
}
What it basically do is, to define a minimum frame and the size that is targeted. Then by calling systemLayoutSizeFittingSize, it resizes the cell to the optimal size, but not larger than the targetSize.
Adjust the code to your needs, but this should work.
I tried to find the culprit in the posted code, but it seems that there are many moving parts. So, I will try to give some hints, that hopefully could help.
In theory (there is caveat for iOS 12), self sizing UICollectionViewCells should not be difficult. You essentially could set the collectionViewLayout.estimedItemSize to any value (preferred is the constant below), like this:
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
Then you have to make sure the constraints in the cells are set in a way that it can self size; that is auto layout can calculate the width and the height of the cell. You are providing an intrinsicContentSize of the tableView and it is wrapped by its super view from all four ends, so this should be OK.
Once you set the estimatedItemSize as shown above, you should not implement the delegate method returning the size:
func collectionView(_: UICollectionView, layout: UICollectionViewLayout, sizeForItemAt: IndexPath) -> CGSize
A quick tutorial can be found here for further reference: https://medium.com/#wasinwiwongsak/uicollectionview-with-autosizing-cell-using-autolayout-in-ios-9-10-84ab5cdf35a2
As I said in theory it should not be difficult, but cell auto sizing seems broken on iOS 12 see here In iOS 12, when does the UICollectionView layout cells, use autolayout in nib
If I were in you position, I would start from afresh, adding complexity step by step:
try implement the self sizing cells, possibly with with a simple UIView and an override of intrinsicContentSize; possibly by using iOS 11.4 SDK to exclude issues relevant to iOS 12 (the easiest way is to download latest Xcode 9 and work from there); if not possible do the iOS 12 fixes at this step
replace the simple view with a table view (which may also have dynamic sizing per see)
do the tableview reload data flow, i.e. dynamic sizing feature
if everything OK, do the iOS 12 fixes and migrate to iOS 12
Hope this helps.
BTW, the warning in the console is probably due to call to layoutIfNeeded() in the delegate method. It triggers an immediate layout pass, whereas this is done for the UICollectionView once all sizes are collected.
Im making a timeline of sorts with a horizontal collectionView where for each block of time (a month), I have a cell with UIViews for each day with different colors. Some months have 30 views added, some 31, some 28. I am having trouble adding views dynamically to each cell such that they are not duplicated or added to the wrong cell.
To that end, I have created a simplified version of this timeline where each month only has 1 layer/view added to it dynamically - I will then try and tackle adding variable amounts of views/layers.
I made this project as the simplest possible example of what I'm talking about:
https://github.com/AlexMarshall12/testTimeline-iOS
Here is the code in ViewController.swift:
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var filledCells: [Int] = []
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.filledCells = [1,28]
collectionView.delegate = self
collectionView.dataSource = self
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 28
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myCell", for: indexPath) as! MyCollectionViewCell
print(indexPath.item)
cell.myLabel.text = String(indexPath.item)
if filledCells.contains(indexPath.item) {
let tickLayer = CAShapeLayer()
tickLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: cell.layer.bounds.width, height: cell.layer.bounds.height), cornerRadius: 5).cgPath
tickLayer.fillColor = UIColor(red:0.99, green:0.13, blue:0.25, alpha:0.5).cgColor
cell.layer.addSublayer(tickLayer)
} else {
//updated
let tickLayer = CAShapeLayer()
tickLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: cell.layer.bounds.width, height: cell.layer.bounds.height), cornerRadius: 5).cgPath
tickLayer.fillColor = UIColor(red:0.1, green:0.13, blue:0.98, alpha:0.5).cgColor
cell.layer.addSublayer(tickLayer)
}
return cell
}
}
The idea is that for each indexPath item (each cell?) it looks to see if that is contained in the self.filledCells array: 1 or 28 which happen to be the outer edges as there are 28 cells returned for numberOfItemsInSection and 1 section. So what I wanted to happen was for every cell to be light blue except for the 1rst and 28th - light red.
However, as you can see here https://imgur.com/a/KTLn7Cb. There are multiple shades of blue and red and purple as cells are filled multiple times as I scroll back and forth, certainly ones other than 1 and 28.
I think there are two issues.
Somehow indexPath.item is returning 1 or 28 even when I'm not scrolled to the very edge cells. Why is this?
When I revisit cells that are already rendered, it re-renders them. I'm not sure why this is. I was wondering if overriding prepareForReuse() could help but I've heard this is often a bad idea so Im not sure if its what I'm looking for.
Any advice on achieving this?
You're overlooking the fact that cells are reused. When you change a cell fillColor, and it gets scrolled off, the cell that gets scrolled on reuses that cell, and you've just set its fill color to red, and you didn't turn it off. Set the fillColor explicitly for each cell, whether it's red, white or clear color, don't set it for just the selected cells.
Its always a bad idea to add a view or a layer every time in table view cells or collection view cells. As you are reusing cells, there is no need to create tickLayer again and again. Make tickLayer as a global variable, initialize it in awakeFromNib funciton in MyCollectionViewCell, then change its fill color in cellForRow function.
Adding/removing views to a cell is not a good idea.
There are two ways to finish this:
① Create different cells by their subviews;
② Create a generic cell and control the subview‘s display via hidden and layout, and update when you get this cell with the new data.
Adding on to what #Owen Hartnett says about reusability of cells, I would like to point out that you are recreating layers each time! This is equivalent of creating subviews.
Every time you encounter a cell inside cellForItemAt function, you should also check existence of pre-added layers. Something like (not syntactically perfect Swift, but pseudocode):
let bLayerFound = false;
for (layer: CALayer in cell.layer.sublayers)
{
if (layer.name.equals("mylayer"))
{
let shapeLayer = layer as! CAShapeLayer
bLayerFound = true;
//set layer properties based on your business logic, as you don't know what this cell is holding due to reuse
shapeLayer.path = //path
shapeLayer.fillColor = //color
}
}
if (bLayerFound == false)
{
//create layer here, again
let tickLayer = CAShapeLayer()
//set layer properties based on your business logic, as you don't know what this cell is holding due to reuse
tickLayer.path = //path
tickLayer.fillColor = //color
tickLayer.name = "mylayer"
cell.layer.addSublayer(tickLayer)
}
Couple of other caveats:
It is better always to add UI to cell.contentView, not cell itself.
Once you get it running, consider moving this code off to your UICollectionViewCell subclass. If there is no subclass, maybe a separate function, so as not to clutter cellForItemAt with nasty if-else logic.
Update
I have revised the question completely after my latest findings.
Goal
My goal is to implement the following effect:
There is a simple table view
The user selects a row
The selected row expands, revealing another label below the original one
Please note that I am aware, that this can be achieved by inserting/deleting cells below the selected one, I already have a successful implementation using that method.
This time, I want to try to achieve this using Auto Layout constraints.
Current status
I have a sample project available for anyone to check, and also opened an issue. To summarize, here's what I've tried so far:
I have the following views as actors here:
The cell's content view
A top view, containing the main label ("main view")
A bottom view, below the main view, containing the initially hidden label ("detail view")
I have set up Auto Layout constraints within my cell the following way (please note that this is strictly pseudo-language):
mainView.top = contentView.top
mainView.leading = contentView.leading
mainView.trailing = contentView.trailing
mainView.bottom = detailView.top
detailView.leading = contentView.leading
detailView.trailing = contentView.trailing
detailView.bottom = contentView.bottom
detailView.height = 0
I have a custom UITableViewCell subclass, with multiple outlets, but the most important here is an outlet for the height constraint mentioned previously: the idea here is to set its constant to 0 by default, but when the cell is selected, set it to 44, so it becomes visible:
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
detailViewHeightConstraint.constant = selected ? detailViewDefaultHeight : 0
UIView.animateWithDuration(0.3) {
self.layoutIfNeeded()
}
}
I have the following result:
So the effect is working, but not exactly how I originally imagined. Instead of pushing the main view up, I want the cell's height to grow when the detail view is shown, and shrink back when it's hidden.
I have examined my layout hierarchy during runtime:
The initial state is OK. The height of the content view is equal to the height of my main view (in this case, it's 125 points).
When the cell is selected, the height constraint of the detail view is increased to 44 points and the two views are properly stacked vertically.But instead of the cell's content view extending, but instead, the main view shrinks.
Question
What I need is the following: the height of table view cell's content view should be equal to
the height of the main view, when the detail view's height constraint is 0 (currently this works)
main view height + detail view height when the detail view's height constraint is set properly (this does not work).
How do I have to set my constraints to achieve that?
After a significant amount of research, I think I've found the solution with the help of this great article.
Here are the steps needed to make the cell resize:
Within the Main, and Detail Views, I have originally set the labels to be horizontally and vertically centered. This isn't enough for self sizing cells. The first thing I needed is to set up my layout using vertical spacing constraints instead of simple alignment:
Additionally you should set the Main Container's vertical compression resistance to 1000.
The detail view is a bit more tricky: Apart from creating the appropriate vertical constraints, you also have to play with their priorities to reach the desired effect:
The Detail Container's Height is constrained to be 44 points, but to make it optional, set its priority to 999 (according to the docs, anything lower than "Required", will be regarded such).
Within the Detail Container, set up the vertical spacing constraints, and give them a priority of 998.
The main idea is the following:
By default, the cell is collapsed. To achieve this, we must programmatically set the constant of the Detail Container's height constraint to 0. Since its priority is higher than the vertical constraints within the cell's content view, the latter will be ignored, so the Detail Container will be hidden.
When we select the cell, we want it to expand. This means, that the vertical constraints must take control: we set the priority Detail Container's height constraint to something low (I used 250), so it will be ignored in favor of the constraints within the content view.
I had to modify my UITableViewCell subclass to support these operations:
// `showDetails` is exposed to control, whether the cell should be expanded
var showsDetails = false {
didSet {
detailViewHeightConstraint.priority = showsDetails ? lowLayoutPriority : highLayoutPriority
}
}
override func awakeFromNib() {
super.awakeFromNib()
detailViewHeightConstraint.constant = 0
}
To trigger the behavior, we must override tableView(_:didSelectRowAtIndexPath:):
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: false)
switch expandedIndexPath {
case .Some(_) where expandedIndexPath == indexPath:
expandedIndexPath = nil
case .Some(let expandedIndex) where expandedIndex != indexPath:
expandedIndexPath = nil
self.tableView(tableView, didSelectRowAtIndexPath: indexPath)
default:
expandedIndexPath = indexPath
}
}
Notice that I've introduced expandedIndexPath to keep track of our currently expanded index:
var expandedIndexPath: NSIndexPath? {
didSet {
switch expandedIndexPath {
case .Some(let index):
tableView.reloadRowsAtIndexPaths([index], withRowAnimation: UITableViewRowAnimation.Automatic)
case .None:
tableView.reloadRowsAtIndexPaths([oldValue!], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
}
Setting the property will result in the table view reloading the appropriate indexes, giving us a perfect opportunity to tell the cell, if it should expand:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! ExpandableTableViewCell
cell.mainTitle = viewModel.mainTitleForRow(indexPath.row)
cell.detailTitle = viewModel.detailTitleForRow(indexPath.row)
switch expandedIndexPath {
case .Some(let expandedIndexPath) where expandedIndexPath == indexPath:
cell.showsDetails = true
default:
cell.showsDetails = false
}
return cell
}
The last step is to enable self-sizing in viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
tableView.contentInset.top = statusbarHeight
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 125
}
Here is the result:
Cells now correctly size themselves. You may notice that the animation is still a bit weird, but fixing that does not fall into the scope of this question.
Conclusion: this was way harder than it should be. 😀 I really hope to see some improvements in the future.
This is in obj-c, but I'm sure you'll handle that:
Add in your viewDidLoad:
self.tableView.estimatedRowHeight = self.tableView.rowHeight;
self.tableView.rowHeight = UITableViewAutomaticDimension;
This will enable self sizing cells for your tableView, and should work on iOS8+
One of our application screens requires us to place a UICollectionView inside of a UITableViewCell. This UICollectionView will have a dynamic number of items, resulting in a height which must be calculated dynamically as well. However, I am running into problems trying to calculate the height of the embedded UICollectionView.
Our overarching UIViewController was created in Storyboards and does make use of auto layout. But, I don't know how to dynamically increase the height of the UITableViewCell based on the height of the UICollectionView.
Can anyone give some tips or advice on how to accomplish this?
The right answer is YES, you CAN do this.
I came across this problem some weeks ago. It is actually easier than you may think. Put your cells into NIBs (or storyboards) and pin them to let auto layout do all the work
Given the following structure:
TableView
TableViewCell
CollectionView
CollectionViewCell
CollectionViewCell
CollectionViewCell
[...variable number of cells or different cell sizes]
The solution is to tell auto layout to compute first the collectionViewCell sizes, then the collection view contentSize, and use it as the size of your cell. This is the UIView method that "does the magic":
-(void)systemLayoutSizeFittingSize:(CGSize)targetSize
withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority
verticalFittingPriority:(UILayoutPriority)verticalFittingPriority
You have to set here the size of the TableViewCell, which in your case is the CollectionView's contentSize.
CollectionViewCell
At the CollectionViewCell you have to tell the cell to layout each time you change the model (e.g.: you set a UILabel with a text, then the cell has to be layout again).
- (void)bindWithModel:(id)model {
// Do whatever you may need to bind with your data and
// tell the collection view cell's contentView to resize
[self.contentView setNeedsLayout];
}
// Other stuff here...
TableViewCell
The TableViewCell does the magic. It has an outlet to your collectionView, enables the auto layout for collectionView cells using estimatedItemSize of the UICollectionViewFlowLayout.
Then, the trick is to set your tableView cell's size at the systemLayoutSizeFittingSize... method. (NOTE: iOS8 or later)
NOTE: I tried to use the delegate cell's height method of the tableView -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.but it's too late for the auto layout system to compute the CollectionView contentSize and sometimes you may find wrong resized cells.
#implementation TableCell
- (void)awakeFromNib {
[super awakeFromNib];
UICollectionViewFlowLayout *flow = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
// Configure the collectionView
flow.minimumInteritemSpacing = ...;
// This enables the magic of auto layout.
// Setting estimatedItemSize different to CGSizeZero
// on flow Layout enables auto layout for collectionView cells.
// https://developer.apple.com/videos/play/wwdc2014-226/
flow.estimatedItemSize = CGSizeMake(1, 1);
// Disable the scroll on your collection view
// to avoid running into multiple scroll issues.
[self.collectionView setScrollEnabled:NO];
}
- (void)bindWithModel:(id)model {
// Do your stuff here to configure the tableViewCell
// Tell the cell to redraw its contentView
[self.contentView layoutIfNeeded];
}
// THIS IS THE MOST IMPORTANT METHOD
//
// This method tells the auto layout
// You cannot calculate the collectionView content size in any other place,
// because you run into race condition issues.
// NOTE: Works for iOS 8 or later
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority {
// With autolayout enabled on collection view's cells we need to force a collection view relayout with the shown size (width)
self.collectionView.frame = CGRectMake(0, 0, targetSize.width, MAXFLOAT);
[self.collectionView layoutIfNeeded];
// If the cell's size has to be exactly the content
// Size of the collection View, just return the
// collectionViewLayout's collectionViewContentSize.
return [self.collectionView.collectionViewLayout collectionViewContentSize];
}
// Other stuff here...
#end
TableViewController
Remember to enable the auto layout system for the tableView cells at your TableViewController:
- (void)viewDidLoad {
[super viewDidLoad];
// Enable automatic row auto layout calculations
self.tableView.rowHeight = UITableViewAutomaticDimension;
// Set the estimatedRowHeight to a non-0 value to enable auto layout.
self.tableView.estimatedRowHeight = 10;
}
CREDIT: #rbarbera helped to sort this out
I think my solution is much simpler than the one proposed by #PabloRomeu.
Step 1. Create outlet from UICollectionView to UITableViewCell subclass, where UICollectionView is placed. Let, it's name will be collectionView
Step 2. Add in IB for UICollectionView height constraint and create outlet to UITableViewCell subclass too. Let, it's name will be collectionViewHeight.
Step 3. In tableView:cellForRowAtIndexPath: add code:
// deque a cell
cell.frame = tableView.bounds;
[cell layoutIfNeeded];
[cell.collectionView reloadData];
cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height;
Both table views and collection views are UIScrollView subclasses and thus don't like to be embedded inside another scroll view as they try to calculate content sizes, reuse cells, etc.
I recommend you to use only a collection view for all your purposes.
You can divide it in sections and "treat" some sections' layout as a table view and others as a collection view. After all there's nothing you can't achieve with a collection view that you can with a table view.
If you have a basic grid layout for your collection view "parts" you can also use regular table cells to handle them. Still if you don't need iOS 5 support you should better use collection views.
I read through all the answers. This seems to serve all cases.
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
collectionView.layoutIfNeeded()
collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width , height: 1)
return collectionView.collectionViewLayout.collectionViewContentSize
}
Pablo Romeu's answer above (https://stackoverflow.com/a/33364092/2704206) helped me immensely with my issue. I had to do a few things differently, however, to get this working for my problem. First off, I didn't have to call layoutIfNeeded() as often. I only had to call it on the collectionView in the systemLayoutSizeFitting function.
Secondly, I had auto layout constraints on my collection view in the table view cell to give it some padding. So I had to subtract the leading and trailing margins from the targetSize.width when setting the collectionView.frame's width. I also had to add the top and bottom margins to the return value CGSize height.
To get these constraint constants, I had the option of either creating outlets to the constraints, hard-coding their constants, or looking them up by an identifier. I decided to go with the third option to make my custom table view cell class easily reusable. In the end, this was everything I needed to get it working:
class CollectionTableViewCell: UITableViewCell {
// MARK: -
// MARK: Properties
#IBOutlet weak var collectionView: UICollectionView! {
didSet {
collectionViewLayout?.estimatedItemSize = CGSize(width: 1, height: 1)
selectionStyle = .none
}
}
var collectionViewLayout: UICollectionViewFlowLayout? {
return collectionView.collectionViewLayout as? UICollectionViewFlowLayout
}
// MARK: -
// MARK: UIView functions
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
collectionView.layoutIfNeeded()
let topConstraintConstant = contentView.constraint(byIdentifier: "topAnchor")?.constant ?? 0
let bottomConstraintConstant = contentView.constraint(byIdentifier: "bottomAnchor")?.constant ?? 0
let trailingConstraintConstant = contentView.constraint(byIdentifier: "trailingAnchor")?.constant ?? 0
let leadingConstraintConstant = contentView.constraint(byIdentifier: "leadingAnchor")?.constant ?? 0
collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width - trailingConstraintConstant - leadingConstraintConstant, height: 1)
let size = collectionView.collectionViewLayout.collectionViewContentSize
let newSize = CGSize(width: size.width, height: size.height + topConstraintConstant + bottomConstraintConstant)
return newSize
}
}
As a helper function to retrieve a constraint by identifier, I add the following extension:
extension UIView {
func constraint(byIdentifier identifier: String) -> NSLayoutConstraint? {
return constraints.first(where: { $0.identifier == identifier })
}
}
NOTE: You will need to set the identifier on these constraints in your storyboard, or wherever they are being created. Unless they have a 0 constant, then it doesn't matter. Also, as in Pablo's response, you will need to use UICollectionViewFlowLayout as the layout for your collection view. Finally, make sure you link the collectionView IBOutlet to your storyboard.
With the custom table view cell above, I can now subclass it in any other table view cell that needs a collection view and have it implement the UICollectionViewDelegateFlowLayout and UICollectionViewDataSource protocols. Hope this is helpful to someone else!
An alternative to Pablo Romeu's solution is to customise UICollectionView itself, rather than doing the work in table view cell.
The underlying problem is that by default a collection view has no intrinsic size and so cannot inform auto layout of the dimensions to use. You can remedy that by creating a custom subclass which does return a useful intrinsic size.
Create a subclass of UICollectionView and override the following methods
override func intrinsicContentSize() -> CGSize {
self.layoutIfNeeded()
var size = super.contentSize
if size.width == 0 || size.height == 0 {
// return a default size
size = CGSize(width: 600, height:44)
}
return size
}
override func reloadData() {
super.reloadData()
self.layoutIfNeeded()
self.invalidateIntrinsicContentSize()
}
(You should also override the related methods: reloadSections, reloadItemsAtIndexPaths in a similar way to reloadData())
Calling layoutIfNeeded forces the collection view to recalculate the content size which can then be used as the new intrinsic size.
Also, you need to explicitly handle changes to the view size (e.g. on device rotation) in the table view controller
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
Easiest approach I've came up with, so far, Credits to #igor answer above,
In your tableviewcell class just insert this
override func layoutSubviews() {
self.collectionViewOutlet.constant = self.postPoll.collectionViewLayout.collectionViewContentSize.height
}
and of course, change the collectionviewoutlet with your outlet in the cell's class
I was facing the same issue recently and I almost tried every solution in the answers, some of them worked and others didn't my main concern about #PabloRomeu approach is that if you have other contents in the cell (other than the collection view) you will have to calculate their heights and the heights of their constraints and return the result to get the auto layout right and I don't like to calculate things manually in my code. So here is the solution that worked fine for me without doing any manual calculations in my code.
in the cellForRow:atIndexPath of the table view I do the following:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//do dequeue stuff
//initialize the the collection view data source with the data
cell.frame = CGRect.zero
cell.layoutIfNeeded()
return cell
}
I think what happens here is that I force the tableview cell to adjust its height after the collection view height has been calculated. (after providing the collectionView date to the data source)
I would put a static method on the collection view class that will return a size based on the content it will have. Then use that method in the heightForRowAtIndexPath to return the proper size.
Also note that you can get some weird behavior when you embed these kinds of viewControllers. I did it once and had some weird memory issues I never worked out.
Maybe my variant will be useful; i've been deciding this task during last two hours. I don't pretend it's 100% correct or optimal, but my skill's very small yet and i'd like to hear comments from experts. Thank you.
One important note: this works for static table - it's specified by my current work.
So, all I use is viewWillLayoutSubviews of tableView. And a little bit more.
private var iconsCellHeight: CGFloat = 500
func updateTable(table: UITableView, withDuration duration: NSTimeInterval) {
UIView.animateWithDuration(duration, animations: { () -> Void in
table.beginUpdates()
table.endUpdates()
})
}
override func viewWillLayoutSubviews() {
if let iconsCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 1)) as? CategoryCardIconsCell {
let collectionViewContentHeight = iconsCell.iconsCollectionView.contentSize.height
if collectionViewContentHeight + 17 != iconsCellHeight {
iconsCellHeight = collectionViewContentHeight + 17
updateTable(tableView, withDuration: 0.2)
}
}
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
switch (indexPath.section, indexPath.row) {
case ...
case (1,0):
return iconsCellHeight
default:
return tableView.rowHeight
}
}
I know, that the collectionView is located in the first row of the second section;
Let the height of the row is 17 p. bigger, than its content height;
iconsCellHeight is a random number as the program starts (i know, that in the portrait form it has to be exactly 392, but it's not important). If the content of collectionView + 17 is not equal this number, so change its value. Next time in this situation the condition gives FALSE;
After all update the tableView. In my case its the combination of two operations (for nice updating of extending rows);
And of course, in the heightForRowAtIndexPath add one row to code.
I get idea from #Igor post and invest my time to this for my project with swift
Just past this in your
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//do dequeue stuff
cell.frame = tableView.bounds
cell.layoutIfNeeded()
cell.collectionView.reloadData()
cell.collectionView.heightAnchor.constraint(equalToConstant: cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
cell.layoutIfNeeded()
return cell
}
Addition:
If you see your UICollectionView choppy when loading cells.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//do dequeue stuff
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
return cell
}
Pablo's solution did not work very well for me, I had strange visual effects (the collectionView not adjusting correctly).
What worked was to adjust the height constraint of the collectionView (as a NSLayoutConstraint) to the collectionView contentSize during layoutSubviews(). This is the method called when autolayout is applied to the cell.
// Constraint on the collectionView height in the storyboard. Priority set to 999.
#IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!
// Method called by autolayout to layout the subviews (including the collectionView).
// This is triggered with 'layoutIfNeeded()', or by the viewController
// (happens between 'viewWillLayoutSubviews()' and 'viewDidLayoutSubviews()'.
override func layoutSubviews() {
collectionViewHeightConstraint.constant = collectionView.contentSize.height
super.layoutSubviews()
}
// Call `layoutIfNeeded()` when you update your UI from the model to trigger 'layoutSubviews()'
private func updateUI() {
layoutIfNeeded()
}
func configure(data: [Strings]) {
names = data
contentView.layoutIfNeeded()
collectionviewNames.reloadData()
}
Short and sweet. Consider the above method in your tableViewCell class. You would probably call it from func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell after dequeing your cell. Before calling reloadData on your collection view, in your tableCell, you need to tell the collection view to lay out its subviews, if layout updates are pending.
In your UITableViewDelegate:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return ceil(itemCount/4.0f)*collectionViewCellHeight;
}
Substitute itemCount and CollectionViewCellHeight with the real values. If you have an array of arrays itemCount might be:
self.items[indexPath.row].count
Or whatever.
1.Create dummy cell.
2.Use collectionViewContentSize method on UICollectionViewLayout of UICollectionView using current data.
You can calculate the height of the collection based on its properties like itemSize, sectionInset, minimumLineSpacing, minimumInteritemSpacing, if your collectionViewCell has the border of a rule.