Adding Right Aligning Constraints to the Programmatically Added Subview - ios

I am trying to add a right aligned Segmented Control to the `UITableViewCell like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "mycell")
// ...
addSegmentedControlToCell(cell)
// ...
and then
func addSegmentedControlToCell(_ cell:UITableViewCell){
let items = ["One","Two"]
let unitControl = UISegmentedControl(items: items)
unitControl.selectedSegmentIndex = 0
let maxWd = cell.frame.size.width
let maxHt = cell.frame.size.height
let padding:CGFloat = 5
unitControl.frame = CGRect(x:maxWd/2, y:padding, width:maxWd/2, height: maxHt-padding*2)
unitControl.addTarget(self, action: #selector(SettingsTableViewController.unitControlValueDidChange(_:)), for: .valueChanged)
cell.addSubview(unitControl)
}
This works well on my default device iPhone 6. But when I run this app on a smaller width device, like iPhone 5, the programmatically added Segment Control, which receives 50% of cell width (cell.frame.size.width/2) seems much bigger that 50% of width and stretches to the right under the cell view port.
This is because of auto-layout and constraints as I see because iPhone 5 cell view gets resized. So I am trying to add a constraint to my new Segment Control, which fails with app crush. Note, I am not very good with programmatically adding constraints yet.
let widthContr = NSLayoutConstraint(item: unitControl,
attribute: NSLayoutAttribute.width,
relatedBy: NSLayoutRelation.equal,
toItem: cell,
attribute: NSLayoutAttribute.notAnAttribute,
multiplier: 1,
constant: 0.5)
cell.addConstraints([widthContr])
How to right align subview correctly (Segment Control) for correct cell size?

You can set constraints like this:
yourView.rightAnchor.constraint(equalTo: yourCell.rightAnchor, constant: -10).isActive = true
Or:
yourView.heightAnchor.constraint(equalToConstant: 50).isActive = true
But make sure that you include this code:
yourSegmentedControl.translatesAutoresizingMaskIntoConstraints = false

You can do this in 3 ways.
1) You have written 0.5 constant in NSLayoutConstraint(this is a mistake).
You need to write constant is 1 and multiplier should be 0.5.
2) Or you should update frame of UISegmentControl in layoutSubviews() method of Custom Cell.
3) Or you should write cell.layoutSubviews() after UISegmentControl adding to Cell.

Related

How to create "real" UITableView(Cell) margins where cells cannot be selected outside their content frame?

I would like to create a UITableView layout where the table view itself fills the complete screen (-> scrollbars are shown at screen endges) but the cells are horizontally centered with a fixed width. It should only be possible to select a cell / tap on it within this fixed width but not within the margins:
Simply giving the UITableView and fixed with + center alignment does work, but in this case the scrollbars are not at the screen edges and it is not possible to scroll using the complete screen but only within the tableView frame.
I tried different other solutions:
let cellWidth = 200
let widthDiff = (tableView.frame.width - cellWidth) / 2
// Solution 1: Change horizontal content inset
tableView.contentInset.left = widthDiff
tableView.contentInset.rigth = widthDiff
// => Cells still use the complete width but can be scrolled horizontally by widthDiff
// => Does NOT work
// Solution 2: Setting layout margins on tableView
tableView.layoutMargins.left = widthDiff
tableView.layoutMargins.rigth = widthDiff
// Solution 3: Setting layout margins on cells...
...
cell.layoutMargins.left = widthDiff
cell.layoutMargins.rigth = widthDiff
// Solution 4: Manually center the cell content using constraints within the cell layout.
// => Layout looks correct in all three cases, but while the cell content
// has the correct margins, the cells itselfs still use the complete
// screen width and is still possible to tap/select sells outside
// their frame.
// => Does NOT work
So, I was not able to find a solution which fulfills all three requirements:
Fixed width, centered cells with left and right margins
Scrollbars at screen edges / table view can be scrolled using the complete screen
Cells can only be selected / tapped on within their content frame
Is there a solution using UITableView properties and methods?
EDIT: As requested this image shows what it should look like:
Cells are centered in the middle with some margins on both sides
Scrollbars are at the screen endge
But: The cells still occupy the complete width. When tapping in the area of the margins cell is still selected and its selected-background uses the complete width. This should be avoided.
From touches point of view you seem to want that cells are selectable only at specific position but table view can be selected everywhere where table view is (for scrolling).
From views point of view you wish to limit cells to specific location but want to draw table view everywhere (scrollbars at the edge).
Then I would say that table view needs to stretch through whole screen and the cell content should be limited. This would best be done with simply constraining a custom view within your cell. The selection of cells would then need to be custom. Consider something like the following:
(I intentionally did some parts programmatically to show what is being done. But I would put most of this in storyboard).
class ViewController: UIViewController {
#IBOutlet private var tableView: UITableView?
var dataSource: [Bool] = []
var cellWidth: CGFloat = 200.0
override func viewDidLoad() {
super.viewDidLoad()
tableView?.allowsSelection = false
dataSource = .init(repeating: false, count: 100)
tableView?.reloadData()
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = MyCell()
cell.setupIfNeeded(width: cellWidth)
cell.isCellSelected = dataSource[indexPath.row]
cell.onMiddleViewPressed = { [weak self, weak cell] in
self?.dataSource[indexPath.row] = true
cell?.isCellSelected = true
}
return cell
}
}
class MyCell: UITableViewCell {
private var isSetup: Bool = false
private var middleView: UIView?
var onMiddleViewPressed: (() -> Void)?
var isCellSelected: Bool = false {
didSet {
middleView?.backgroundColor = isCellSelected ? .blue : .gray
}
}
func setupIfNeeded(width: CGFloat) {
guard isSetup == false else { return }
isSetup = true
let middleView = UIView(frame: .zero)
middleView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(middleView)
contentView.addConstraint(.init(item: middleView, attribute: .centerX, relatedBy: .equal, toItem: contentView, attribute: .centerX, multiplier: 1.0, constant: 0.0))
middleView.addConstraint(.init(item: middleView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: width))
contentView.addConstraint(.init(item: middleView, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1.0, constant: 0.0))
contentView.addConstraint(.init(item: middleView, attribute: .bottom, relatedBy: .equal, toItem: contentView, attribute: .bottom, multiplier: 1.0, constant: 0.0))
middleView.backgroundColor = isCellSelected ? .blue : .gray
middleView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onMiddleViewSelected)))
self.middleView = middleView
}
#objc private func onMiddleViewSelected() {
onMiddleViewPressed?()
}
}
To create a toggle in selection you would then simply do:
cell.onMiddleViewPressed = { [weak self, weak cell] in
guard let self = self else { return }
self.dataSource[indexPath.row].toggle()
cell?.isCellSelected = self.dataSource[indexPath.row]
}
To use a single selection you would do
cell.onMiddleViewPressed = { [weak self, weak cell, weak tableView] in
guard let self = self else { return }
if let currentSelectionIndex = self.dataSource.firstIndex(of: true), currentSelectionIndex != indexPath.row {
self.dataSource[currentSelectionIndex] = false
let targetIndexPath = IndexPath(row: currentSelectionIndex, section: 0)
if tableView?.indexPathsForVisibleRows?.contains(targetIndexPath) == true {
tableView?.reloadRows(at: [targetIndexPath], with: .none)
}
}
self.dataSource[indexPath.row].toggle()
cell?.isCellSelected = self.dataSource[indexPath.row]
}
so this is nothing too heavy. And a pretty standard procedures in cases like having UISwitch (or similar components) for selection where selecting the cell does something completely different (navigate to details for instance).
Perhaps you also need transparent cells and table view. This is just:
tableView?.backgroundColor = .clear
cell.backgroundColor = .clear
cell.contentView.backgroundColor = .clear

UICollectionView .reloadData() only shows sections

I have a collection view which has a section with a title and every section has some words. Words differ in size.
Because the words differ in size i've added the following, to prevent long words from being cut off: layout.estimatedItemSize = CGSize(width: 1.0, height: 1.0)
However after setting that and invoking reloadData(), the cells (words) do not get loaded only the sections (title).
But after scrolling all the sections that went out of screen will load their words. However when I don't use layout.estimatedItemSize it works correctly, but the words are cut off.
So my question is if there is another way to display those words (which are basically a small view with a label) without them being cut off. Or am I using estimatedSize wrongly?
As I read from the docs from apple itself: docs
The default value of this property is CGSizeZero. Setting it to any other value causes the collection view to query each cell for its actual size using the cell’s preferredLayoutAttributesFitting(_:) method.
I do set constraints dynamically and statically (StoryBoard),
my dynamic constraint is as following:
if prevCell != nil {
let constraint = NSLayoutConstraint(item: cell, attribute: NSLayoutAttribute.leading, relatedBy: NSLayoutRelation.equal, toItem: prevCell, attribute: NSLayoutAttribute.trailing, multiplier: 1.0, constant: 5.0)
cell.addConstraint(constraint)
self.prevCell = cell
}
Things I have tried myself so far:
//before reload data invalidate the layout
self.collectionView!.collectionViewLayout.invalidateLayout()
self.collectionView!.reloadData()
After some more research found the correct way to handle this.
I forgot to override the preferredLayoutAttributesFitting in the cell's class. To make it work I just left the layout.estimatedItemSize = CGSize(width: 1.0, height: 1.0) in viewDidLoad of the collection view.
However somehow this is not enough because it wont be able to calculate the right size not until you scroll it out of screen. It probably has some logic behind it that I do not know of.
But when overriding the the preferredLayoutAttributesFitting as follow:
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
setNeedsLayout()
layoutIfNeeded()
let updatedSize = contentView.systemLayoutSizeFitting(layoutAttributes.size)
var updatedFrame = layoutAttributes.frame
updatedFrame.size.width = CGFloat(ceilf(Float(updatedSize.width)))
updatedFrame.size.height = CGFloat(ceilf(Float(updatedSize.height)))
layoutAttributes.frame = updatedFrame
return layoutAttributes
}

iOS - subivew can't be centered in UITableViewCell

It's really weird.
No matter how I set the constraints, it just ignors all of them after layouts.
I've tried to use cell.indicator.center = cell.center and cell.indicator.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin] in the func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
but still not worked.
Is there any special way to center a subview inside a UITableViewCell?
LoadingCell.xib
Screenshot(Simulator)
updated:
After trying #Vibha Singh's advice, I put these codes in my LoadingCell.
override func layoutSubviews() {
print("layoutSubviews")
print(center)
print(contentView.bounds)
indicator.center = contentView.center
}
And it printed these lines:
layoutSubviews
(207.0, 1055.16668891907)
(0.0, 0.0, 414.0, 44.6666666666667)
But the indicator is still not centered.
updated:
I fixed it by creating a new cell with an indicator.
Both of them have exactly the same constraints. The new one is centered as expected, but the old one is still positioned at left top.
updated:
I did use centerX and centerY as constraints at the first time. But it's not worked. So I have to try another way. That's why I use so many constraints in the first screenshot.
Here are the screenshots with two exactly the same xibs.
Both of them use the same codes to dequeue.
let dequeue = tableView.dequeueReusableCell(withIdentifier: SharedCell.loading.rawValue, for: indexPath)
if let cell = dequeue as? CenterLoadingCell {
cell.indicator.startAnimating()
}
else if let cell = dequeue as? LoadingCell {
cell.indicator.startAnimating()
}
return dequeue
The first one is named LoadingCell, which is not centered on the simulator.
The second one is named CenterLoadingCell, which I created after I asked this question. And this one is centered on the simulator.
You are confusing your layout setting big time by adding unnecessary constraints. Check this implementation.
With Custom Cell:
You can add indicator like
let activityView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
activityView.center = CGPoint(x: CGFloat(cell.bounds.midX), y: CGFloat(cell.bounds.midY))
cell.contentView.addSubview(activityView)
activityView.startAnimating()
Or you can set frame in below method of cell
override func layoutSubviews()
{
}
You can make it easily with constraints
func setConstraints(item:UIView , relatedTo:UIView , attr:NSLayoutAttribute , relatedBy:NSLayoutRelation , multiplier: CGFloat , constant : CGFloat)
{
relatedTo.addSubview(item)
item.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint(item: item, attribute: attr, relatedBy: relatedBy, toItem: relatedTo, attribute: attr, multiplier: multiplier, constant: constant).isActive = true
}
and usage :
setConstraints(item: indicator, relatedTo:cell.contentView, attr:centerX, relatedBy:.equal, multiplier: 0 , constant : 0)

Two vertically aligned views start differently when their shared parent view embedded in Navigation Controller

I have two table views within a view controller, and the view controller is embedded in a navigation controller.
I have added the following constraints vertically, and I expect the two table views start at the same point, near the bottom of the navigation bar.
constraints.append(t1.topAnchor.constraintEqualToAnchor(self.topLayoutGuide.bottomAnchor, constant: 8.0))
constraints.append(t1.bottomAnchor.constraintEqualToAnchor(self.bottomLayoutGuide.topAnchor, constant: -8.0))
constraints.append(t2.topAnchor.constraintEqualToAnchor(t1.topAnchor))
constraints.append(t2.bottomAnchor.constraintEqualToAnchor(t1.bottomAnchor))
However, it turns out the table view t1 starts much lower than the table view t2, the latter of which starts near the bottom of the navigation bar as expected.
Why does this happen? How to fix this?
UPDATE
Constraints:
t1.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: "cell")
t2.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: "cell")
t1.dataSource = self
t2.dataSource = self
self.view.addSubview(t1)
self.view.addSubview(t2)
t1.translatesAutoresizingMaskIntoConstraints = false
t2.translatesAutoresizingMaskIntoConstraints = false
var constraints = [NSLayoutConstraint]()
constraints.append(t1.topAnchor.constraintEqualToAnchor(self.topLayoutGuide.bottomAnchor, constant: 8.0))
constraints.append(t1.bottomAnchor.constraintEqualToAnchor(self.bottomLayoutGuide.topAnchor, constant: -8.0))
constraints.append(t2.topAnchor.constraintEqualToAnchor(t1.topAnchor))
constraints.append(t2.bottomAnchor.constraintEqualToAnchor(t1.bottomAnchor))
constraints.append(t1.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor, constant: 8.0))
constraints.append(t2.trailingAnchor.constraintEqualToAnchor(self.view.trailingAnchor, constant: -8.0))
constraints.append(t2.leadingAnchor.constraintEqualToAnchor(t1.trailingAnchor, constant: 8.0))
constraints.append(t1.widthAnchor.constraintEqualToAnchor(t2.widthAnchor))
NSLayoutConstraint.activateConstraints(constraints)
Data Sources:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel?.text = "Start"
return cell
}
Unexpected Alignments:
just tried to replicate the issue.
The table views get perfectly aligned if I use the code below. Just added the constraints in the view controller.
t1.backgroundColor = UIColor.redColor()
t2.backgroundColor = UIColor.blueColor()
view.addSubview(t1)
view.addSubview(t2)
t1.translatesAutoresizingMaskIntoConstraints = false
t2.translatesAutoresizingMaskIntoConstraints = false
view.addConstraint(t1.leftAnchor.constraintEqualToAnchor(view.leftAnchor, constant: 0.0))
view.addConstraint(t2.rightAnchor.constraintEqualToAnchor(view.rightAnchor, constant: 0.0))
t1.addConstraint(NSLayoutConstraint(item: t1, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: 100.0))
t2.addConstraint(NSLayoutConstraint(item: t2, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: 100.0))
view.addConstraint(t1.topAnchor.constraintEqualToAnchor(topLayoutGuide.bottomAnchor, constant: 8.0))
view.addConstraint(t1.bottomAnchor.constraintEqualToAnchor(bottomLayoutGuide.topAnchor, constant: -8.0))
view.addConstraint(t2.topAnchor.constraintEqualToAnchor(t1.topAnchor))
view.addConstraint(t2.bottomAnchor.constraintEqualToAnchor(t1.bottomAnchor))
Just gives me this layout:
Maybe you can give more insights into your whole layout code?
UPDATE
After investigating the issue with your provided layout code:
The table views are layouted exactly as expected. The problem is with the
contentInset.
Add this for some console logs:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("Insets t1 -> \(t1.contentInset)")
print("Insets t2 -> \(t2.contentInset)")
}
This prints out:
Insets t1 -> UIEdgeInsets(top: 64.0, left: 0.0, bottom: 0.0, right: 0.0)
Insets t2 -> UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
There is an option in Interface Builder for your view controller named Adjust Scroll View Insets. Disable it (or use automaticallyAdjustsScrollViewInsets = false in code).
In general table views (as can be seen in table view controllers) go under the navigation bar (so you get the blurring scroll effect...) but the content inset is set so that your first cell is below the navigation bar.
To avoid this behavior when referencing the top layout guide you have to disable this option. The other table view is not affected because it only references the layout constraints from the other one.
It is always very helpful to color the views, so you can see if misalignments come from your layout code or any other side effect. (Or use the view debugger from Xcode)
So after setting the flag it gets from this:
to this:
Cheers
Orlando 🍻
Modify one line code would resolve it.
Replace
NSLayoutConstraint.activateConstraints(constraints)
with
self.view.addConstraints(constraints)
I think you don't understand the concept of Auto Layout completely.
UPDATE:
Sorry about that I did't mean to the UINavigationController.
UIViewController defaultly adjust scrollview's inset, you can prevent it via automaticallyAdjustsScrollViewInsets = false.
BTW, you can use Mansory or Purelayout to set constraints easily.
class ViewController: UIViewController, UITableViewDataSource {
var t1: UITableView = UITableView(frame: CGRectZero, style: .Plain)
var t2: UITableView = UITableView(frame: CGRectZero, style: .Plain)
override func viewDidLoad() {
super.viewDidLoad()
// This line is the point.
automaticallyAdjustsScrollViewInsets = false
commonInit()
}
func commonInit(){
view.backgroundColor = UIColor .whiteColor()
t1.backgroundColor = UIColor.blueColor()
t2.backgroundColor = UIColor.greenColor()
view.addSubview(t1)
view.addSubview(t2)
t1.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: "cell")
t2.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: "cell")
t1.dataSource = self
t2.dataSource = self
self.view.addSubview(t1)
self.view.addSubview(t2)
t1.translatesAutoresizingMaskIntoConstraints = false
t2.translatesAutoresizingMaskIntoConstraints = false
var constraints = [NSLayoutConstraint]()
constraints.append(t1.topAnchor.constraintEqualToAnchor(self.topLayoutGuide.bottomAnchor, constant: 8.0))
constraints.append(t1.bottomAnchor.constraintEqualToAnchor(self.bottomLayoutGuide.topAnchor, constant: -8.0))
constraints.append(t2.topAnchor.constraintEqualToAnchor(t1.topAnchor))
constraints.append(t2.bottomAnchor.constraintEqualToAnchor(t1.bottomAnchor))
constraints.append(t1.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor, constant: 8.0))
constraints.append(t2.trailingAnchor.constraintEqualToAnchor(self.view.trailingAnchor, constant: -8.0))
constraints.append(t2.leadingAnchor.constraintEqualToAnchor(t1.trailingAnchor, constant: 8.0))
constraints.append(t1.widthAnchor.constraintEqualToAnchor(t2.widthAnchor))
view.addConstraints(constraints)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel?.text = "Start"
return cell
}
}

UITableView with variable cell height: Working in IB but not programmatically

TL;DR
My programmatically created table view cells are not resizing according to the intrinsic content height of their custom views, even though I am using UITableViewAutomaticDimension and setting both the top and bottom constraints.
The problem probably lies in my implementation of the UITableViewCell subclass. See the code below under Doesn't work programmatically > Code > MyCustomCell.swift.
Goal
I'm trying to make a suggestion bar for a custom Mongolian keyboard. Mongolian is written vertically. In Android it looks like this:
Progress
I've learned that I should use a UITableView with variable cell heights, which is available starting with iOS 8. This requires using auto layout and telling the table view to use automatic dimensions for the cell heights.
Some things I've had to learn along the way are represented in my recent SO questions and answers:
How to make a custom table view cell
Getting variable height to work with in a table view with a standard UILabel
Getting intrinsic content size to work for a custom view
Using a programmatically created UITableViewCell
Set constraints programmatically
So I have come to the point where I have the vertical labels that support intrinsic content size. These labels go in my custom table view cells. And as described in the next section, they work when I do it in the storyboard, but not when I create everything programmatically.
Works in IB
In order to isolate the problem I created two basic projects: one for where I use the storyboard and one where I do everything programmatically. The storyboard project works. As can be seen in the following image, each table view cell resizes to match the height of custom vertical label.
In IB
I set constraints to pin the top and bottom as well as centering the label.
Code
ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let myStrings: [String] = ["a", "bbbbbbb", "cccc", "dddddddddd", "ee"]
let cellReuseIdentifier = "cell"
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
}
// number of rows in table view
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.myStrings.count
}
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
return cell
}
// method to run when table view cell is tapped
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("You tapped cell number \(indexPath.row).")
}
}
MyCustomCell.swift
import UIKit
class MyCustomCell: UITableViewCell {
#IBOutlet weak var myCellLabel: UIMongolSingleLineLabel!
}
Doesn't work programmatically
Since I want the suggestion bar to be a part of the final keyboard, I need to be able to create it programmatically. However, when I try to recreate the above example project programmatically, it isn't working. I get the following result.
The cell heights are not resizing and the custom vertical labels are overlapping each other.
I also get the following error:
Warning once only: Detected a case where constraints ambiguously
suggest a height of zero for a tableview cell's content view. We're
considering the collapse unintentional and using standard height
instead.
This error has been brought up before multiple times on Stack Overflow:
iOS8 - constraints ambiguously suggest a height of zero
Detected a case where constraints ambiguously suggest a height of zero
custom UITableviewcell height not set correctly
ios 8 (UITableViewCell) : Constraints ambiguously suggest a height of zero for a tableview cell's content view
However, the problem for most of those people is that they were not setting both a top and bottom pin constraint. I am, or at least I think I am, as is shown in my code below.
Code
ViewController.swift
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let myStrings: [String] = ["a", "bbbbbbb", "cccc", "dddddddddd", "ee"]
let cellReuseIdentifier = "cell"
var tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
// Suggestion bar
tableView.frame = CGRect(x: 0, y: 20, width: view.bounds.width, height: view.bounds.height)
tableView.registerClass(MyCustomCell.self, forCellReuseIdentifier: cellReuseIdentifier)
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
view.addSubview(tableView)
}
// number of rows in table view
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.myStrings.count
}
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
return cell
}
// method to run when table view cell is tapped
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("You tapped cell number \(indexPath.row).")
}
}
MyCustomCell.swift
I think the problem is probably in here since this is the main difference from the IB project.
import UIKit
class MyCustomCell: UITableViewCell {
var myCellLabel = UIMongolSingleLineLabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.myCellLabel.translatesAutoresizingMaskIntoConstraints = false
self.myCellLabel.centerText = false
self.myCellLabel.backgroundColor = UIColor.yellowColor()
self.addSubview(myCellLabel)
// Constraints
// pin top
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self.contentView, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
}
override internal class func requiresConstraintBasedLayout() -> Bool {
return true
}
}
Supplemental Code
I'll also include the code for the custom vertical label that I used in both projects above, but since the IB project works, I don't think the main problem is here.
import UIKit
#IBDesignable
class UIMongolSingleLineLabel: UIView {
private let textLayer = LabelTextLayer()
var useMirroredFont = false
// MARK: Primary input value
#IBInspectable var text: String = "A" {
didSet {
textLayer.displayString = text
updateTextLayerFrame()
}
}
#IBInspectable var fontSize: CGFloat = 17 {
didSet {
updateTextLayerFrame()
}
}
#IBInspectable var centerText: Bool = true {
didSet {
updateTextLayerFrame()
}
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
func setup() {
// Text layer
textLayer.backgroundColor = UIColor.yellowColor().CGColor
textLayer.useMirroredFont = useMirroredFont
textLayer.contentsScale = UIScreen.mainScreen().scale
layer.addSublayer(textLayer)
}
override func intrinsicContentSize() -> CGSize {
return textLayer.frame.size
}
func updateTextLayerFrame() {
let myAttribute = [ NSFontAttributeName: UIFont.systemFontOfSize(fontSize) ]
let attrString = NSMutableAttributedString(string: textLayer.displayString, attributes: myAttribute )
let size = dimensionsForAttributedString(attrString)
// This is the frame for the soon-to-be rotated layer
var x: CGFloat = 0
var y: CGFloat = 0
if layer.bounds.width > size.height {
x = (layer.bounds.width - size.height) / 2
}
if centerText {
y = (layer.bounds.height - size.width) / 2
}
textLayer.frame = CGRect(x: x, y: y, width: size.height, height: size.width)
textLayer.string = attrString
invalidateIntrinsicContentSize()
}
func dimensionsForAttributedString(attrString: NSAttributedString) -> CGSize {
var ascent: CGFloat = 0
var descent: CGFloat = 0
var width: CGFloat = 0
let line: CTLineRef = CTLineCreateWithAttributedString(attrString)
width = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, nil))
// make width an even integer for better graphics rendering
width = ceil(width)
if Int(width)%2 == 1 {
width += 1.0
}
return CGSize(width: width, height: ceil(ascent+descent))
}
}
// MARK: - Key Text Layer Class
class LabelTextLayer: CATextLayer {
// set this to false if not using a mirrored font
var useMirroredFont = true
var displayString = ""
override func drawInContext(ctx: CGContext) {
// A frame is passed in, in which the frame size is already rotated at the center but the content is not.
CGContextSaveGState(ctx)
if useMirroredFont {
CGContextRotateCTM(ctx, CGFloat(M_PI_2))
CGContextScaleCTM(ctx, 1.0, -1.0)
} else {
CGContextRotateCTM(ctx, CGFloat(M_PI_2))
CGContextTranslateCTM(ctx, 0, -self.bounds.width)
}
super.drawInContext(ctx)
CGContextRestoreGState(ctx)
}
}
Update
The entire code for the project is all here, so if anyone is interested enough to try it out, just make a new project and cut and paste the code above into the following three files:
ViewController.swift
MyCustomCell.swift
UIMongolSingleLineLabel.swift
The error is pretty trivial:
Instead of
self.addSubview(myCellLabel)
use
self.contentView.addSubview(myCellLabel)
Also, I would replace
// pin top
NSLayoutConstraint(...).active = true
// pin bottom
NSLayoutConstraint(...).active = true
// center horizontal
NSLayoutConstraint(...).active = true
with
let topConstraint = NSLayoutConstraint(...)
let bottomConstraint = NSLayoutConstraint(...)
let centerConstraint = NSLayoutConstraint(...)
self.contentView.addConstraints([topConstraint, bottomConstraint, centerConstraint])
which is more explicit (you have to specify the constraint owner) and thus safer.
The problem is that when calling active = true on a constraint, the layout system has to decide to which view it should add the constraints. In your case, because the first common ancestor of contentView and myCellLabel is your UITableViewCell, they were added to your UITableViewCell, so they were not actually constraining the contentView (constraints were between siblings not between superview-subview).
Your code actually triggered a console warning:
Warning once only: Detected a case where constraints ambiguously suggest a height of zero for a tableview cell's content view. We're considering the collapse unintentional and using standard height instead.
Which made me to look immediately at the way the constraints are created for your label.
I have tested your code and found the issue was in setting constraints please use below code part for setting constants in your "MyCustomCell.swift" file setup() function
let topConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1, constant: 0)
let centerConstraint = NSLayoutConstraint(item: myCellLabel, attribute: .CenterX, relatedBy: .Equal, toItem: self, attribute: .CenterX, multiplier: 1, constant: 0)
self.addConstraints([centerConstraint, topConstraint, bottomConstraint])
Also set clips to bound property to your cell lable in "viewcontroller.swift"
// create a cell for each table view row
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:MyCustomCell = self.tableView.dequeueReusableCellWithIdentifier(cellReuseIdentifier) as! MyCustomCell
cell.myCellLabel.text = self.myStrings[indexPath.row]
cell.myCellLabel.clipsToBounds=true
return cell
}
For your ease I have uploaded my sample code on GitHub Dynamic Height Sample
Output is looking like this now
The problem seems to come from the vertical constraints in the cell
By putting them relative to self instead of self.contentView in MyCustomCell you can fix your problem
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
the full class would be:
import UIKit
class MyCustomCell: UITableViewCell {
var myCellLabel = UIMongolSingleLineLabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
self.myCellLabel.translatesAutoresizingMaskIntoConstraints = false
self.myCellLabel.centerText = false
self.myCellLabel.backgroundColor = UIColor.yellowColor()
self.addSubview(myCellLabel)
// Constraints
// pin top
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.TopMargin, multiplier: 1.0, constant: 0).active = true
// pin bottom
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.BottomMargin, multiplier: 1.0, constant: 0).active = true
// center horizontal
NSLayoutConstraint(item: myCellLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.CenterX, multiplier: 1, constant: 0).active = true
}
override internal class func requiresConstraintBasedLayout() -> Bool {
return true
}
}
The thing you are missing is this function:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return heightValue
}
Im not so sure what you should do exactly, but by the fact that you know your labels you should be able to return an exact height value for each cell in this method
I think you are missing to set constraints for tableView with superview. And try to increase estimated row height also.

Resources