I have a custom UITableViewCell that contains a StackView with top, bottom, leading and trailing constraints to the content view of the cell.
When I set up my tableView, I give it an estimated height and also set the rowHeight to UITableViewAutomaticDimension.
In my cellForRowAt datasource method, I dequeue the cell and then call cell.setup() which adds any given number of views to my cell's stackView.
The problem is: My cell is always being sized to the estimated height of 80p. No matter how many views I add to the cell's stackView, it all crams into 80p height. The stackView, and thus the cell, isn't growing with each new item I insert into the cell before returning it in cellForRowAt datasource method.
I tried different distribution settings for my stackView, but I'm not sure what I'm doing wrong here.
Here is a simple demonstration of adding buttons to a stack view inside an auto-sizing table view cell:
class StackOfButtonsTableViewCell: UITableViewCell {
#IBOutlet var theStackView: UIStackView!
func setup(_ numButtons: Int) -> Void {
// cells are reused, so remove any previously created buttons
theStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
for i in 1...numButtons {
let b = UIButton(type: .system)
b.setTitle("Button \(i)", for: .normal)
b.backgroundColor = .blue
b.setTitleColor(.white, for: .normal)
theStackView.addArrangedSubview(b)
}
}
}
class ViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 6
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "StackOfButtonsTableViewCell", for: indexPath) as! StackOfButtonsTableViewCell
cell.setup(indexPath.row + 1)
return cell
}
}
Assuming you have created a prototype cell, and its only content is a UIStackView configured as:
Axis: Vertical
Alignment: Fill
Distribution: Equal Spacing
Spacing: 8
and you have it constrained Top/Leading/Trailing/Bottom to the cell's content view, this is the result:
No need for any height calculations, and, since buttons do have intrinsic size, no need to set height constraints on the buttons.
Related
A list of content needs to be displayed in each tableviewcell, So here I used tableview inside Tableview cell. But the problem is main tableview cell height is not changing according to the content. I don't want to fix the UITableViewCell height, I want it to be adjusted dynamically based on the content.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! SubCell
if indexPath.row%2==0{
cell.backgroundColor = .orange
}else{
cell.backgroundColor = .green
}
return cell
}
class SubCell: UITableViewCell, UITableViewDelegate,UITableViewDataSource {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 4
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell2") as! SubCell2
cell.titleLabel.text = "asdf asdf asdf asdf asdf adfs asdf asdf asdf asdf asdf asdf asd fasd fasd fasd fasd fasd fasd f asdf asdf asdf asdf asd fasd fasd fasd fasd fasd fasdf asdf asd fasd 1234"
return cell
}
#IBOutlet weak var tablView2: UITableView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
class SubCell2: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Expected Output :
Actual Output:
Constraints:
Solution:
make your label constraint Bottom space to container to its superview(content view).
Now select "yourTableView" in size inspector make it "Row height as Automatic"
Now select "yourTableViewCell" in size inspector make it "Row height as Automatic"
As based on label height, the content view will be Resized, And the allocation of height tableViewcell & tableview is automatically adjusted.
i have posted the above answer only for StoryBoard & Xib.
You don't have any height settings for your Title Label. If you set one, your cells will act dynamically.
You can add another constraint to the Title Label and set the height to "greater than or equal to" whatever number you want to start with (30, for example). So long as you have that title label's number of lines set to 0, then the title label should dynamically grow in height and still maintain the same padding you've had with the rest of your constraints.
In the case of dynamic content where we need to place a UITableView inside a UITableViewCell, Automatic Dimension doesn’t seem to work because the outer UITableView needs to have a height for every cell it has. By the time this height is needed, the child UITableView hasn’t loaded its own cells yet, and therefore compiler cannot get the height in runtime.
So how do we achieve this ? One way is to use a vertical axis UIStackView (instead of a UITableView) inside the parent cell with leading, trailing, top and bottom constraints with respect to the cell’s content view without any need for height constraint. Instead of UITableViewCell create a custom view with necessary UI components and add as addArrangedSubview to the UIStackView. As each view is added to the stack view, the stack view’s height dynamically increases which in turn expands the height of the outer UITableViewCell thereby achieving dynamic height with automatic dimension of the outer table.
this is the layout i want to acheive
I am trying to figure out how to make this layout work on different screens. i have tried to get the screen height and programatically set the video layer height equal to it. The volume button constrains are 25 from the right margin and 25 from the bottom of the video. The problem is that when i try to run the app the volume button shows in the middle right of the screen instead on the right of the bottom corner. Also the image views are overlapping the video instead of showing up under it. My assumption is that the constrains of these elements see the height of the video layer in the story board, not the height that i set programatically in the ViewController.swift.
You can achieve a more coherent layout with a table view and having a section for your full screen video and the rest of the image views in another section. The section for your video will have UIScreen.main.bounds.height for the cell height so that you can have a dynamic full screen height regardless of the device.
class ViewController: UITableViewController {
var arr: [[String]] = {
var arr = [[String]]()
arr.append(["Video"])
var imageArr = [String]()
for index in 1...20 {
imageArr.append(String(index))
}
arr.append(imageArr)
return arr
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return arr.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arr[section].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = arr[indexPath.section][indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.section == 0 {
return UIScreen.main.bounds.height
} else {
return 100
}
}
}
As for the volume button, you can create a custom cell for the video section and load it in the cellForRowAt method. Make sure you're using Autolayout and not CGRect to position it.
first of all you are working with ScrollView so you need to give each item in you screen a height so you will give the video screen height "Fixed Height" then you will give your button a fixed height as well and connect it from trailing, leading, bottom and Top and I think it will work with you, and if there is any issue appeared feel free to comment with it
Let's say I have hierarchy like this:
*TableViewCell
**TableView
***TableViewCell
and all of them should be resizable. Did someone face this kind of problem? In past I've used many workarounds like systemLayoutSizeFitting or precalculation of height in heightForRowAt, but it always breaks some constraints, because TableViewCell has height constraint equal to estimated row height and there appear some kinds of magic behavior. Any ways to make this live?
Current workaround:
class SportCenterReviewsTableCell: UITableViewCell, MVVMView {
var tableView: SelfSizedTableView = {
let view = SelfSizedTableView(frame: .zero)
view.clipsToBounds = true
view.tableFooterView = UIView()
view.separatorStyle = .none
view.isScrollEnabled = false
view.showsVerticalScrollIndicator = false
view.estimatedRowHeight = 0
if #available(iOS 11.0, *) {
view.contentInsetAdjustmentBehavior = .never
} else {
// Fallback on earlier versions
}
return view
}()
private func markup() {
contentView.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.register(ReviewsTableViewCell.self, forCellReuseIdentifier: "Cell")
tableView.snp.makeConstraints() { make in
make.top.equalTo(seeAllButton.snp.bottom).offset(12)
make.left.equalTo(contentView.snp.left)
make.right.equalTo(contentView.snp.right)
make.bottom.lessThanOrEqualTo(contentView.snp.bottom)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ReviewsTableViewCell
cell.viewModel = viewModel.cellViewModels[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! ReviewsTableViewCell
cell.viewModel = viewModel.cellViewModels[indexPath.row]
cell.setNeedsLayout()
cell.layoutIfNeeded()
let size = cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultLow)
return size.height
}
}
Self sizing tableView class:
class SelfSizedTableView: UITableView {
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
self.layoutIfNeeded()
}
override var intrinsicContentSize: CGSize {
self.setNeedsLayout()
self.layoutIfNeeded()
return contentSize
}
}
This is actually not an answer to the question, but just an explanation.
(Wrote here because of the character count limitation for the comments).
The thing is that you're trying to insert a vertically scrollable view inside another vertically scrollable view. If you don't disable the nested tableview's scroll ability, you will have a glitch while scrolling, because the system wouldn't know to whom pass the scroll event (to the nested tableview, or to the parent tableview).
So in our case, you'll have to disable the "scrollable" property for the nested tableviews, hence you'll have to set the height of the nested tableview to be equal to its content size. But this way you will lose the advantages of tableview (i.e. cell reusing advantage) and it will be the same as using an actual UIScrollView. But, on the other hand, as you'll have to set the height to be equal to its content size, then there is no reason to use UIScrollView at all, you can add your nested cells to a UIStackView, and you tableview will have this hierarchy:
*TableView
**TableViewCell
***StackView
****Items
****Items
****Items
****Items
But again, the right solution is using multi-sectional tableview. Let your cells be section headers of the tableview, and let inner cells be the rows of the tableview.
here is an example of how to make a tableview inside a table view cell with automatic height for the cells.
You should use the 'ContentSizedTableView' class for the inner tableViews.
class ViewController: UIViewController {
#IBOutlet weak var outerTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
outerTableView.rowHeight = UITableView.automaticDimension
outerTableView.estimatedRowHeight = UITableView.automaticDimension
outerTableView.delegate = self
outerTableView.dataSource = self
}
}
final class ContentSizedTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
sizeToFit()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? TableTableViewCell
return cell!
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
Use xib files to simplify the hierarchy.
Get a tableView on your storyboard, and create a nib file for your tableViewCell(say CustomTableViewCell). Inside it create a tableView and again create one more tableViewCell xib file. Now, no need of setting labels into your xib file,(if you want only labels in cells and nothing else, if not, there is another way of adding constraints)
Say you have an array of text, some strings are long and some are short.
register nib file in CustomTableViewCell and extend it to use Delegate and DataSource.
register this CustomTableViewCell in ViewController.
While declaring a cell in CustomTableViewCell, just do=
cell.textLabel?.text = content
cell.textLabel?.numberOfLines = 0
Use heightForRowAt to set outer tableViewCell's height, and let the inner tableView to scroll inside.
I put a UICollectionView into the UITableViewCell by following this tutorial and in my UICollectionViewCell, there's a Image View. So when I run my app, the collection view is not resizing itself at the same time in my cell I put a Text View which is resizing itself according to content, see the below images:
In this first image, I have a text view at the top which have some text in it, and below it with (pink background) is my collection view and inside of that with greenBackground is my image view, as you can see that collection view is taking the extra space instead of reducing itself as Text View Did.
in this second image you can see that my textView haves more content then before so its resized itself now overlapping the CollectionView
this is my TableViewCell:
class TableViewCell: UITableViewCell {
#IBOutlet var txtView: UITextView!
#IBOutlet private weak var collectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
// collectionView.frame = self.bounds;
// collectionView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func setCollectionViewDataSourceDelegate
<D: protocol<UICollectionViewDataSource, UICollectionViewDelegate>>
(dataSourceDelegate: D, forRow row: Int) {
collectionView.delegate = dataSourceDelegate
collectionView.dataSource = dataSourceDelegate
collectionView.tag = row
collectionView.reloadData()
}
var collectionViewOffset: CGFloat {
get {
return collectionView.contentOffset.x
}
set {
collectionView.contentOffset.x = newValue
}
}
}
this is my collectionViewCell
class CollectionViewCell: UICollectionViewCell {
#IBOutlet var imgView: UIImageView!
override func awakeFromNib() {
self.setNeedsLayout()
//
// self.contentView.frame = self.bounds;
// self.contentView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
}
}
and this is my TableviewController
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return imageModel.count
}
override func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell",
forIndexPath: indexPath) as! TableViewCell
cell.txtView.text = txtArray[indexPath.row]
return cell
}
override func tableView(tableView: UITableView,
willDisplayCell cell: UITableViewCell,
forRowAtIndexPath indexPath: NSIndexPath) {
guard let tableViewCell = cell as? TableViewCell else { return }
tableViewCell.setCollectionViewDataSourceDelegate(self, forRow: indexPath.row)
tableViewCell.collectionViewOffset = storedOffsets[indexPath.row] ?? 0
}
override func tableView(tableView: UITableView,
didEndDisplayingCell cell: UITableViewCell,
forRowAtIndexPath indexPath: NSIndexPath) {
guard let tableViewCell = cell as? TableViewCell else { return }
storedOffsets[indexPath.row] = tableViewCell.collectionViewOffset
}
}
extension TableViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return imageModel[collectionView.tag].count
}
func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell",
forIndexPath: indexPath) as! CollectionViewCell
cell.imgView.frame = CGRectMake(0, 0, collectionView.frame.width, collectionView.frame.height)
cell.imgView.contentMode = .ScaleAspectFit
//cell.addSubview(imageView)
cell.imgView.image = ResizeImage(UIImage(named: imageModel[collectionView.tag][indexPath.item])!, targetSize: CGSizeMake( cell.imgView.frame.width , cell.imgView.frame.height))
//imageView.image = UIImage(named: imageModel[collectionView.tag][indexPath.item])
return cell
}
}
How can I make this collection view to AutoLayout itself according to the content in it? I also tried this:
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 100;
but didn't worked (my collection view got disappear) if anybody knows how to do this, then please guide me..
I faced a similar issue when i used collection view inside a table view cell. No matter what i did i couldn't get the table view to resize automatically but the collection view did. Soo instead of autolayout i did it using code.
I ended up having to calculate the size of the label in the collection view numberOfSections in collection view and passing this height using a delegate to the view controller that has the tableView's delegate and dataSource and reloading the appropriate row of the table view.
As it happens, the numberOfSections in collectionview data source gets called everytime and the delegate resizes the table view height.
Some thing like this-
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView{
[self.delegate setViewHeight:[self getHeightForCellAtIndexPath:[NSIndexPath indexPathForRow:currentSelected inSection:0]]];
return 1;
}
This ought to give you a general idea.
EDIT: Sorry i misunderstood, your question before. Here is something that should work for you:
As per my understanding, you have a table view cell with a label view and collection view inside of it.
Now, inside your table view cell, you should add top, leading and trailing constraints space to the label. Now inside your collection view position your image vertically in the center and add an appropriate top and bottom to the cell superview. Your collection view itself should have a CONSTANT value in leading, trailing, top to label and bottom to superview. Also add a fixed height constraint to your collection View (assuming you want the image sizes to remain the same).
Now lets says View Controller A is the data source for your table view and the table view cell is the data source for your collection view.
In your viewController A, you should write your height for row at indexPath as-
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
CGSize stringSize = [yourStringArray[indexPath.row] boundingRectWithSize:CGSizeMake(_yourCollectionView.frame.size.width, FLT_MAX)
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
attributes:#{NSFontAttributeName:[UIFont yourFont size:yourFontSize]} context:nil].
return stringSize.height + 35; //magic number 35, play around with it to suit your need. Did this to always have a minimum fixed height.
}
This will allow your tableViewRowForHeight for that particular index to have the height of your label added to it and the constraints ought to do the rest.
UITableViewWrapperView size is not changing as per the content if row height is UITableViewAutomaticDimension.
I am trying to create a Feed listing with TableView and dynamic height rows.
TableView is scrollable but UITableViewWrapperView looks like it's not changing its size!
Below is the code and screenshot of the view hierarchy.
I think its strange.
Code:
class FeedController: UITableViewController {
var items: NSMutableArray = []
override func viewDidLoad() {
super.viewDidLoad()
refreshControl?.addTarget(self, action: "feedLoad", forControlEvents: UIControlEvents.ValueChanged)
self.feedLoad()
}
override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 120.0
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
println("called")
return UITableViewAutomaticDimension
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.items.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return self.getCellForItemAtIndexPath( indexPath )
}
func getCellForItemAtIndexPath(indexPath: NSIndexPath) -> UITableViewCell {
let item: Dictionary = self.items[indexPath.row] as Dictionary<String, AnyObject>
let cell = tableView.dequeueReusableCellWithIdentifier( (item["type"] as String) + "Cell", forIndexPath: indexPath ) as FeedViewCell
cell.fillData(item)
return cell as UITableViewCell
}
func feedLoaded(){
tableView.reloadData()
}
func feedLoad(){
// Feed Load Logic which will call next line once feed loaded
self.feedLoaded()
}
}
I am assuming you have auto layout configured for your cell, as that's the only way to get this working. Auto Layout should be able to calculate the height based on your cell constraints.
So you should have constraints between your views vertically and between the views on the top and bottom edges the super view (in this case the cell view).
Suppose you have a cell with title and description, like this:
|------------------|
| Title |
| |
| Description with |
| long multiple |
| lines of text |
|------------------|
In this example you have to set add a constraint with a vertical space between title and description, a constraint of vertical space between the title's top and the cell view top and a vertical space between the description's bottom and the cell view's bottom.
The documentation for estimatedHeightForRowAtIndexPath() says that the value retuned by that call is the placeholder value for all till a scroll event occurs. I am guessing that when the scroll even does occur, heightForRowAtIndexPath will get called. So:
Change the value for estimated height and see if that is the fixed
height that you observe.
Does heightForRow get called when you scroll? If not, may be there are other issues about scrolling, such as contentSize etc., that need addressing so that scrolling does take place.
When you using automaticDimention, You should add constraints to both sides of UILabels in UItableViewCell.