Collapsable Tableview with Dynamic sizing cells - ios

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...

Related

Wrong NSLayoutConstraint Causes Black Screen

I want to place header view on top of screen with NSLayoutConstraint (I must use NSLayoutConstraint). When I do it like in below code, view places corruptly in somewhere else and also controllers background color turns black and nothing works. Where am I doing wrong?
I searched below posts for not opening a duplicate post but nothing fixed it:
Programmatically creating constraints bound to view controller margins
Programmatically Add CenterX/CenterY Constraints
EDIT: This controller is inside navigation controller but I'm not sure If It is related.
override func viewDidLoad(){
self.view.backgroundColor = UIColor.white
boxView.backgroundColor = Color.Common.welcomeScreenBackgroundColor.withAlphaComponent(0.5)
boxView.translatesAutoresizingMaskIntoConstraints = false
self.view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubView(boxView)
}
override func viewDidLayoutSubviews() {
//Header = 20 from left edge of screen
let cn1 = NSLayoutConstraint(item: boxView, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1.0, constant: 0)
//Header view trailing end is 20 px from right edge of the screen
let cn2 = NSLayoutConstraint(item: boxView, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1.0, constant: 0)
//Header view height = constant 240
let cn3 = NSLayoutConstraint(item: boxView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant:240)
//Header view vertical padding from the top edge of the screen = 20
let cn5 = NSLayoutConstraint(item: boxView, attribute: .top, relatedBy: .equal, toItem: self.topLayoutGuide, attribute: .bottom, multiplier: 1.0, constant: 0)
self.view.addConstraints([cn1,cn2,cn3,cn5])
}
The problem was setting translatesAutoresizingMaskIntoConstraints to false on Superview. So I deleted the;
self.view.translatesAutoresizingMaskIntoConstraints = false
and this solves the problem. I think this causes app creates constraint for superview.

Programatic constraints collapsing cell view

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.

UIScrollView dynamic height content (autolayout) not filling whole space

I am creating a small chat app in which I use a custom subclass of UIScrollView to present the messages. This app is just for practicing so I don't want to use a third party library. I am implementing this UIScrollView via autolayout, after reading the technical note 2154 by Apple and several tutorials explaining this, and my implementation is almost working but the content view of my UIScrollView doesn't seem to fill all the space available.
The code which presents the ScrollView is:
public class ChatView: UIScrollView {
private var contentView: UIView
...
// This get called by all the init methods. contentView is already created ( contentView = UIView() )
private func setupViews() {
self.translatesAutoresizingMaskIntoConstraints = false
contentView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(contentView)
let constraint1 = NSLayoutConstraint(item: self, attribute: .Top, relatedBy: .Equal, toItem: contentView, attribute: .Top, multiplier: 1.0, constant: 0.0)
let constraint2 = NSLayoutConstraint(item: self, attribute: .Bottom, relatedBy: .Equal, toItem: contentView, attribute: .Bottom, multiplier: 1.0, constant: 0.0)
let constraint3 = NSLayoutConstraint(item: self, attribute: .Width, relatedBy: .Equal, toItem: contentView, attribute: .Width, multiplier: 1.0, constant: 0.0)
let constraint4 = NSLayoutConstraint(item: self, attribute: .CenterX, relatedBy: .Equal, toItem: contentView, attribute: .CenterX, multiplier: 1.0, constant: 0.0)
self.addConstraints([constraint1, constraint2, constraint3, constraint4])
self.layoutIfNeeded()
// Later, the messages are added to the contentView. I don't think is relevant to see the exact code (but I can post it if needed)
// Each message is added using autolayout and the constraints only reference the messages themselves and contentView
}
}
When I add a ChatView to my view controller (using storyboards), with its four sides pinned to views which are not in his hierarchy, the following problem happens:
In the image, the scrollView cannot be scrolled upwards any more. There seem to be a space which should be filled and isn't. If I scroll down, I have the exact same problem but the empty space is below the content. In the following images you can see that the contentView is smallest than the ChatView itself:
And the same view hierarchy but with the constraints shown:
In both images the view in the background is the ChatView and the selected one is the contentView. I haven't been able to figure why the content view doesn't cover the full ChatView space.
Thanks in advance!
I finally stumbled upon the answer while searching a different problem, in another stackoverflow question. The key is to open the storyboard and set in the container view controller "AdjustsScrollViewInsets" to "NO".
In code it's simply (inside the view controller):
self.automaticallyAdjustsScrollViewInsets = NO;

Programmatic Autolayout changes behaviour of a UIView

I have a controller where I add a subview programmatically. With the configuration of the subview I add autolayout constraints programmatically. Everthing is working except that the view doesn't react on touches if I add the constraints and even the set backgroundcolor is not displayed.
The buttonView should be displayed in the lower right corner of my parent view.
Any ideas what could be wrong?
Here is how I add my constraints:
private func configureAutolayoutConstraints(buttonView: UIView, parentView: UIView){
buttonView.translatesAutoresizingMaskIntoConstraints = false
let bottomConstraint = NSLayoutConstraint(item: buttonView, attribute:
.Bottom, relatedBy: .Equal, toItem: parentView, attribute: .Bottom, multiplier: 1.0, constant: -130)
parentView.addConstraint(bottomConstraint)
let trailingConstraint = NSLayoutConstraint(item: buttonView, attribute:
.Trailing, relatedBy: .Equal, toItem: parentView, attribute: .Trailing, multiplier: 1.0, constant: -90)
parentView.addConstraint(trailingConstraint)
}
Autolayout engine needs at least 4 constraints to determine the frame of view. You have applied bottom and trailing constraints only. You need either width+height OR leading+top constraint to make it work.

Adding auto layout constraints between different view controller's view

As an academic exercise for a future UI, I am trying to add constraints between two table views, belonging to two different child view controllers of the root controller. In my RootViewController class below, tvc is displayed as expected in a 400x500 frame, but tvc2 is consuming the entire frame instead of being a 400x500 frame to the right of tvc. Basically, the constraints are apparently being ignored. I'm using an iPad sim in landscape.
override func viewDidLoad() {
super.viewDidLoad()
let v = self.view
v.backgroundColor = UIColor.greenColor()
var tvc :OrderTableViewController = OrderTableViewController(style: UITableViewStyle.Plain)
var tvc2 :OrderTableViewController = OrderTableViewController(style: UITableViewStyle.Plain)
self.addChildViewController(tvc)
self.addChildViewController(tvc2)
v.addSubview(tvc.view)
v.addSubview(tvc2.view)
tvc.didMoveToParentViewController(self)
tvc2.didMoveToParentViewController(self)
//tvc.view.setTranslatesAutoresizingMaskIntoConstraints(false)
//tvc2.view.setTranslatesAutoresizingMaskIntoConstraints(false)
//self.view.setTranslatesAutoresizingMaskIntoConstraints(false)
tvc.view.frame = CGRectMake(0, 0, 400, 500)
self.view.addConstraint(NSLayoutConstraint(
item: tvc2.view,
attribute: .Top,
relatedBy: .Equal,
toItem: tvc.view,
attribute: .Top,
multiplier: 1, constant: 0))
self.view.addConstraint(NSLayoutConstraint(
item: tvc2.view,
attribute: .Bottom,
relatedBy: .Equal,
toItem: tvc.view,
attribute: .Bottom,
multiplier: 1, constant: 0))
self.view.addConstraint(NSLayoutConstraint(
item: tvc2.view,
attribute: .Width,
relatedBy: .Equal,
toItem: nil,
attribute: .NotAnAttribute,
multiplier: 1, constant: 400))
self.view.addConstraint(NSLayoutConstraint(
item: tvc2.view,
attribute: .Left,
relatedBy: .Equal,
toItem: tvc.view,
attribute: .Right,
multiplier: 1, constant: 0))
}
This line,
tvc2.view.setTranslatesAutoresizingMaskIntoConstraints(false)
should be uncommented. When you add a view programmatically, and you're using constraints, you should always set that to false. On the other hand, you should not set that to false for controller's main view.
Also, the width constraint, since it only applies to tvc2, should be added to tvc2, not to self.view (although it should work either way).
It's also a bit odd, that you're adding one view using frames, and the other with constraints. It would be better to do both using the same paradigm.

Resources