Programatic constraints collapsing cell view - ios

I am developing a UITableViewCell that starts as a xib, has views added to it programmatically, and has a dynamically sized height. However, it looks like when adding the programatic views with constraints, it is conflicting with the auto-resize constraint initially applied to the xib, and causing issues. Please see below:
Dequeuing my cells:
//Table Delegate/Datasource
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:S360SSessionMatchTableCell? = tableView.dequeueReusableCellWithIdentifier(XIBFiles.SESSIONMATCHTABLECELL + String(indexPath.row)) as? S360SSessionMatchTableCell
if ((cell == nil)){
tableView.registerNib(UINib(nibName: XIBFiles.SESSIONMATCHTABLECELL, bundle: nil), forCellReuseIdentifier: XIBFiles.SESSIONMATCHTABLECELL + String(indexPath.row))
cell = tableView.dequeueReusableCellWithIdentifier(XIBFiles.SESSIONMATCHTABLECELL + String(indexPath.row)) as? S360SSessionMatchTableCell
}
cell!.setupEvents(sessionMatches[indexPath.row]["sessions"]! as! [[String:String]])
return cell!
}
Setup Events Method in Custom UITableViewCell:
func setupEvents(events:[[String:String]]){
//Set up start and end times
self.startTimeLbl.text = events[0]["startTime"]!
self.endTimeLbl.text = events[events.count - 1]["endTime"]!
//Set up events
var pastEventView:S360SScheduledEventView? = nil
var pastEvent:[String:String]? = nil
for (index, event) in events.enumerate(){
var topAnchor:NSLayoutConstraint!
//Create event view
let eventView:S360SScheduledEventView = NSBundle.mainBundle().loadNibNamed(XIBFiles.SCHEDULEDEVENTVIEW, owner: self, options: nil)[0] as! S360SScheduledEventView
//Deal with first view added
if pastEvent == nil{
//Top anchor setup for first view
topAnchor = NSLayoutConstraint(item: eventView, attribute: .Top, relatedBy: .Equal, toItem: toLbl, attribute: .Bottom, multiplier: 1, constant: 10)
}
else{
//Check for a break
let timeFormatter:NSDateFormatter = NSDateFormatter()
timeFormatter.dateFormat = "hh:mm a"
let startTime = timeFormatter.dateFromString(pastEvent!["endTime"]!)
let endTime = timeFormatter.dateFromString(event["startTime"]!)
if startTime != endTime {
//Create break view
let breakView = NSBundle.mainBundle().loadNibNamed(XIBFiles.SCHEDULEDBREAKVIEW, owner: self, options: nil)[0] as! S360SScheduledBreakView
//Setup breakview constraints
let bTopAnchor = NSLayoutConstraint(item: breakView, attribute: .Top, relatedBy: .Equal, toItem: pastEventView, attribute: .Bottom, multiplier: 1, constant: 0)
let bLeftAnchor = NSLayoutConstraint(item: breakView, attribute: .Leading, relatedBy: .Equal, toItem: self.contentView, attribute: .LeadingMargin, multiplier: 1, constant: 0)
let bRightAnchor = NSLayoutConstraint(item: breakView, attribute: .Trailing, relatedBy: .Equal, toItem: self.contentView, attribute: .TrailingMargin, multiplier: 1, constant: 0)
let bHeightAnchor = NSLayoutConstraint(item: breakView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 30)
//Add break view and constraints
self.addSubview(breakView)
self.addConstraints([bTopAnchor, bLeftAnchor, bRightAnchor, bHeightAnchor])
//Top anchor setup for subsequent view
topAnchor = NSLayoutConstraint(item: eventView, attribute: .Top, relatedBy: .Equal, toItem: breakView, attribute: .Bottom, multiplier: 1, constant: 0)
}
else{
//Top anchor setup for subsequent views
topAnchor = NSLayoutConstraint(item: eventView, attribute: .Top, relatedBy: .Equal, toItem: pastEventView, attribute: .Bottom, multiplier: 1, constant: 0)
}
}
//Setup other anchors
let leftAnchor = NSLayoutConstraint(item: eventView, attribute: .Leading, relatedBy: .Equal, toItem: self.contentView, attribute: .LeadingMargin, multiplier: 1, constant: 0)
let rightAnchor = NSLayoutConstraint(item: eventView, attribute: .Trailing, relatedBy: .Equal, toItem: self.contentView, attribute: .TrailingMargin, multiplier: 1, constant: 0)
let heightAnchor = NSLayoutConstraint(item: eventView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 60)
//Setup event view
eventView.iconImg.image = Images.get_event_image(event["title"]!)
eventView.titleLbl.text = event["title"]!
eventView.courtLbl.text = "court" + event["court"]!
eventView.timeLbl.text = event["startTime"]! + " to " + event["endTime"]!
//Add event view and constraints
self.addSubview(eventView)
self.addConstraints([topAnchor, leftAnchor, rightAnchor, heightAnchor])
//Prepare for next iteration
pastEventView = eventView
pastEvent = event
//Set up last cell with bottom bound
if index == events.count - 1 {
let bottomAnchor = NSLayoutConstraint(item: eventView, attribute: .Bottom, relatedBy: .Equal, toItem: self.contentView, attribute: .BottomMargin, multiplier: 1, constant: 0)
self.addConstraint(bottomAnchor)
}
}
}
Constraints in xib:
This is the error I get (pasted once, but it occurs for each cell):
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
2016-07-05 15:13:01.654 Shoot360 Scheduler[32779:642808] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSAutoresizingMaskLayoutConstraint:0x7fedd85d5590 h=--& v=--& V:[UITableViewCellContentView:0x7fedda431120(44)]>",
"<NSLayoutConstraint:0x7fedda43a7e0 V:[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda438b20(60)]>",
"<NSLayoutConstraint:0x7fedda436590 UITableViewCellContentView:0x7fedda431120.topMargin == UILabel:0x7fedda4312a0'10:00 AM'.top - 15>",
"<NSLayoutConstraint:0x7fedda436630 UILabel:0x7fedda431c00'to'.top == UILabel:0x7fedda4312a0'10:00 AM'.top>",
"<NSLayoutConstraint:0x7fedda433b60 V:[UILabel:0x7fedda431c00'to']-(10)-[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda438b20]>",
"<NSLayoutConstraint:0x7fedda445910 V:[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda4443f0(60)]>",
"<NSLayoutConstraint:0x7fedda448310 V:[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda438b20]-(0)-[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda4443f0]>",
"<NSLayoutConstraint:0x7fedda449a00 V:[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda448540(60)]>",
"<NSLayoutConstraint:0x7fedda4479e0 V:[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda4443f0]-(0)-[Shoot360_Scheduler.S360SScheduledEventView:0x7fedda448540]>",
"<NSLayoutConstraint:0x7fedda44a100 Shoot360_Scheduler.S360SScheduledEventView:0x7fedda448540.bottom == UITableViewCellContentView:0x7fedda431120.bottomMargin>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7fedda436590 UITableViewCellContentView:0x7fedda431120.topMargin == UILabel:0x7fedda4312a0'10:00 AM'.top - 15>
Row height is being set to dynamic:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//Styling
showAllBtn.layer.cornerRadius = Numbers.CORNERRADIUS
sessionsTbl.rowHeight = UITableViewAutomaticDimension
sessionsTbl.estimatedRowHeight = 500
sessionsTbl.layer.borderColor = Colors.REALLIGHTGREY.CGColor
sessionsTbl.layer.borderWidth = Numbers.BORDERREG
sessionsTbl.layer.cornerRadius = Numbers.CORNERRADIUS
sessionsTbl.separatorStyle = UITableViewCellSeparatorStyle.None
}
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}

The constraint
V:[UITableViewCellContentView:0x7fedda431120(44)
means that rowHeight in your table is set to the default value of 44pt while you want the cell height to be dynamic. You will have to set rowHeight to UITableViewAutomaticDimension and also set estimatedRowHeight.
Also note that cells are reused therefore you will have to remove all previously added views everytime you call setupEvents.
Also note you should not call tableView.registerNib(...) from inside cellForRow method. The good place to register cells is inside viewDidLoad.

It seems you've made life much more complicated for yourself than it needs to be.
If we look at what you currently have:
a table with 1 section and many rows
1 cell subclass
each row has an arbitrary number of subviews added on the fly
each subview is pinned to each other with constraints
each cell is explicitly instantiated for each row, not properly reused
cells don't have their subviews removed when they are reused
this doesn't fit well with a table view and means you're writing a lot of code and trying to cram it all into one place.
Looking at your data it would be better to have something like:
a table with multiple sections, each section having multiple rows
1 section per session
1 row per 'event'
1 section header class with date labels
2 cell subclasses, 1 for an event and one for a break
no views added on the fly
In this scenario your constraints are trivial and there are no constraints being added in code, you just set a bit of data and everything else just works. This scheme also breaks down your concerns and separates out the code for each different part into logical parts.
Rather than try to fix your existing issue you should step back and look at your approach.

Related

Why UITableViewCell dynamic view constraint not working properly?

I am trying to create UIButton inside the UITableViewCell using following way
UITableView configuration :
self.tableView.estimatedRowHeight = 1
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.delegate = self
self.tableView.dataSource = self
UITableViewCell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let uiTableViewCell = UITableViewCell(style: .default, reuseIdentifier: "More");
uiTableViewCell.contentView.backgroundColor = .yellow
let moreBtn = UIButton(type: .system)
moreBtn.setTitle("More", for: .normal)
moreBtn.setTitleColor(.white, for: .normal)
moreBtn.backgroundColor = UIColor.init(hexString: "#2A1771")
moreBtn.translatesAutoresizingMaskIntoConstraints = false
uiTableViewCell.contentView.addSubview(moreBtn)
let leading = NSLayoutConstraint(item: moreBtn, attribute: .leading, relatedBy: .equal, toItem: uiTableViewCell.contentView, attribute: .leading, multiplier: 1, constant: 16)
let trailing = NSLayoutConstraint(item: moreBtn, attribute: .trailing, relatedBy: .equal, toItem: uiTableViewCell.contentView, attribute: .trailing, multiplier: 1, constant: 16)
let top = NSLayoutConstraint(item: moreBtn, attribute: .top, relatedBy: .equal, toItem: uiTableViewCell.contentView, attribute: .top, multiplier: 1, constant: 16)
let bottom = NSLayoutConstraint(item: moreBtn, attribute: .bottom, relatedBy: .equal, toItem: uiTableViewCell.contentView, attribute: .bottom, multiplier: 1, constant: 16)
let height = NSLayoutConstraint(item: moreBtn, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 52)
uiTableViewCell.contentView.addConstraints([leading,top,trailing,bottom,height])
NSLayoutConstraint.activate([leading,top,trailing,bottom,height])
return uiTableViewCell
}
See this following image.. Why more button going outside from contentView
Your constraints are saying the the button's trailing edge should be 16 more than the content view trailing edge (The constant is +16) and similarly for the bottom edge.
You can either swap the item and toItem properties in those constraints or use -16 as the constant.
You will also have a problem with adding the button in cellForRow as cells are reused when the table view scrolls, resulting in multiple buttons being added to a cell. You should add the button in the cell itself, either in the initialiser or in awakeFromNib

Collapsable Tableview with Dynamic sizing cells

I've been trying to follow this github tutorial, however instead of having static cells, they need to be dynamic since I don't know the height at compile time.
In the tableViewController I have set the heightForRowAt to be 0 or UITableViewAutomaticDimension depending if the section is collapsed or not. Similar with the estimatedHeightForRowAtthe value is 0 or a higher value if it's expanded.
Getting to the problem now, I have subclassed the UITableViewCell and when the cellForRowAt is called it will add a subview like the following:
override func addSubview(_ view: UIView) {
guard view != nil else {return}
for view in self.subviews {
view.removeFromSuperview()
}
super.addSubview(view)
//view.applyLeadingAndTrailingPinConstraint(toSuperview: 0)
//view.applyTopAndBottomPinConstraint(toSuperview: 0)
let addedView = self.subviews[0]
let bottom = NSLayoutConstraint(item: addedView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0)
bottom.priority = 750
bottom.isActive = true
NSLayoutConstraint(item: addedView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: addedView, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: addedView, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: addedView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: addedView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0).isActive = true
//NSLayoutConstraint.reportAmbiguity(v: self)
if addedView.hasAmbiguousLayout {
print("Horizontal: \(addedView.constraintsAffectingLayout(for: .horizontal))")
print("Vertical: \(addedView.constraintsAffectingLayout(for: .vertical))")
print("Trace: \(addedView.value(forKey: "_autolayoutTrace"))")
}
//addedView.exerciseAmbiguityInLayout()
//print("self: \(self.frame), subview: \(self.subviews[0].frame)")
}
As you may see, the problem is that there's an ambiguity with the constraints, hence the hasAmbigousLayout. When the view is collapsed there is no error. However when I expand the view, which has also a height constraint, the ambiguity happens. The following is the output of the 3 print statements inside hasAmbigousLayout:
Horizontal: []
Vertical: [<NSLayoutConstraint:0x60800028ea10 UIView:0x7fcfe856a790.height == 100 (active)>]
Trace: Optional(
•ViraVira_Info.WellnessCell:0x7fcfe8859a00'cell', MISSING HOST CONSTRAINTS
| *UIView:0x7fcfe856a790- AMBIGUOUS LAYOUT for UIView:0x7fcfe856a790.minX{id: 776}, UIView:0x7fcfe856a790.minY{id: 771}, UIView:0x7fcfe856a790.Width{id: 778}
Legend:
* - is laid out with auto layout
+ - is laid out manually, but is represented in the layout engine because translatesAutoresizingMaskIntoConstraints = YES
• - layout engine host)
I have tried to lower the bottom constraint priority as sugested in this question. Also I have tried many other sites, which I sadly lost track of.
The result in the app is that the view does not even show up, since it should have a red background, also when i try to print the frame of the subview, the width is 0 which seems odd.
I probably made a silly mistake with the constraint but after 2 days trying I still can't figure it out.
Thanks for your time
-Jorge
There are several issues to note (and may be part of the problem):
override func addSubview(_ view: UIView) -> I don't think thats the right place for your code. I develop several years now iOS and there was never a reason to override that method. View layout should be done in Interface Builder or in the parent view.
guard view != nil else {return} -> should give you a warning, because view is not an optional (UIView?), so it can't be nil. This check does simply nothing useful.
for view in self.subviews { view.removeFromSuperview() } super.addSubview(view) -> This will remove every previously added view, the last added view will win the race, thats completely unexpected by the caller of addSubview - If a cell adds more than one subview this will cause trouble and headache.
let addedView = self.subviews[0] -> must be equal view, so why not using view in the later code?
6 * NSLayoutConstraint -> A view is normally positioned by 4 constraints, 2 vertical and 2 horizontal - I think you should remove the last two (center constraints) - That may be your problem.
Try to refactor your code...

UITableViewCell: Adding constraints programmatically from a UITextView to Content View?

I'm facing an issue when trying to programmatically add constraints to the UITableViewCell's contentView.
In cellForRowAt, I'm programmatically creating a UITextView, then trying to apply constraints and add it to the cell's contentView.
However, Im getting the crash and error:
The view hierarchy is not prepared for the constraint: <NSLayoutConstraint:0x1700951d0 UITableViewCellContentView:0x12de3e150.leading == UITextView:0x12e891400.leading + 8 (inactive)>
When added to a view, the constraint's items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled.
My following code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
// Dequeue the cell to load data
let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
if indexPath.section == 1
{
let textView: UITextView = UITextView()
textView.textColor = UIColor.black
textView.translatesAutoresizingMaskIntoConstraints = false
cell.addSubview(textView)
let leadingConstraint = NSLayoutConstraint(item: cell.contentView, attribute: NSLayoutAttribute.leading, relatedBy: NSLayoutRelation.equal, toItem: textView, attribute: NSLayoutAttribute.leading, multiplier: 1.0, constant: 8.0)
let trailingConstraint = NSLayoutConstraint(item: cell.contentView, attribute: NSLayoutAttribute.trailing, relatedBy: NSLayoutRelation.equal, toItem: textView, attribute: NSLayoutAttribute.trailing, multiplier: 1.0, constant: -8.0)
cell.contentView.addConstraint(leadingConstraint)
cell.contentView.addConstraint(trailingConstraint)
let topConstraint = NSLayoutConstraint(item: cell.contentView, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: textView, attribute: NSLayoutAttribute.top, multiplier: 1.0, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: cell.contentView, attribute: NSLayoutAttribute.bottom, relatedBy: NSLayoutRelation.equal, toItem: textView, attribute: NSLayoutAttribute.bottom, multiplier: 1.0, constant: 0)
cell.contentView.addConstraint(topConstraint)
cell.contentView.addConstraint(bottomConstraint)
}
return cell
}
I do see a lot of questions regarding the mentioned error, but have not found one regarding this error when applied to a UITableViewCell.
Can someone point me in the right direction?
Thanks.
Let add textView to cell.contentView (NOT cell). So let change to cell.contentView.addSubview(textView) and it's working!

How to resize UITableView with a large list to fit all the content without scrolling?

I have a UITableView with about at least 60 cells inside that needs to fit and not scroll. How can I get it to fit? I have already tried:
CGRect frame = self.tableView.frame;
frame.size.height = self.tableView.contentSize.height;
self.tableView.frame = frame;
As said in this answer here but in the comments it was mentioned that it would not work with very large lists and you can tell because the bounce will reveal more cells. There was not a solution provided for this. How can I fix this problem?
Obvious solution is to not use UITableView. You can create your contentView as normal view, either programatically or in xib, and then add it to your main view and connect it to the previous custom one using constraint. I have done it multiple times and it works like a charm.
var lastView: UIView? = nil
for item in model.items {
var customView: CustomContentView
let objects = NSBundle.mainBundle().loadNibNamed("CustomContentView", owner: self, options: nil)
customView = objects.first! as! CustomContentView
customView.translatesAutoresizingMaskIntoConstraints = false
customView.setItemModel(item)
viewWrap.addSubview(customView)
viewWrap.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[customView]|", options: [], metrics: nil, views: ["customView": customView]))
if lastView == nil {
viewWrap.addConstraint(NSLayoutConstraint(item: customView, attribute: .Top, relatedBy: .Equal, toItem: viewWrap, attribute: .Top, multiplier: 1, constant: 0))
} else {
viewWrap.addConstraint(NSLayoutConstraint(item: customView, attribute: .Top, relatedBy: .Equal, toItem: lastView!, attribute: .Bottom, multiplier: 1, constant: 0))
}
lastView = customView
}
if lastView != nil {
viewWrap.addConstraint(NSLayoutConstraint(item: viewWrap, attribute: .Bottom, relatedBy: .Equal, toItem: lastView!, attribute: .Bottom, multiplier: 1, constant: 17))
}
Then you just to make sure that your custom view can resize to fit the screen if there is too many of them. I assume you have some max limit and that they all can fit. If not, then you need to add it to scroll view.

How do I add contstraints automatically

I come from front-end development, when I create div it automatically displays itself in top, after second one it will display itself below the first one, and so...
I want to be able to do the same with swift UIViews, I've created some views and want to add constraints automatically after previous view, and if the view is the first one apply it to the parent view, how do I do it any suggestions? I have created custom class from UIView called Car, but then I realized it was not a good idea
if let parent = self.superview {
let siblings = parent.subviews.filter{$0 is Car}
let lastCar: UIView?
if siblings.count > 1{
lastCar = siblings[siblings.count - 2]
} else {
lastCar = nil
}
if let sibling = lastCar {
parent.addConstraint(
NSLayoutConstraint(
item: self,
attribute: .Top,
relatedBy: .Equal,
toItem: sibling,
attribute: .Bottom,
multiplier: 1.0,
constant: 1
))
} else {
parent.addConstraint(
NSLayoutConstraint(
item: self,
attribute: .Top,
relatedBy: .Equal,
toItem: parent,
attribute: .Top,
multiplier: 1.0,
constant: 0
))
}
}
I set the constraints to lastCar or parent view using if let syntax.

Resources