I kind of have a weird layout here, it's kind of like this (also see pics):
-UITableViewCell 1
----UIView 2
--------UITableView 3
The controller the the UITableView (1) is like that:
//mainTableView (1) controller
var cellHeights = [CGFloat]()
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let card = CardSource.orangeCards[indexPath.row]
cell.configureCardInCell(card)
cellHeights.insert(card.frame.height + 15, at: indexPath.row)
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row]
}
but the problem is that when the screen first loads, the UIViews overlap because the cells are too small because the smaller table view (the one in the UIView) hasn't loaded yet and it's height isn't defined. Proof of this is that when I scroll to the bottom of the main table view then scroll back up cellForRowAt is called again and the two views don't overlap anymore (see pics). So what I basically want is a way to load the small table view and define it's height before the bigger table view loads (or if you have any other solutions, that'd be welcome too)
I know my question isn't very clear, I'm not really good at explaining stuff, so don't hesitate to ask me questions in the comments.
Many thanks!
When the view first loads
After scrolling down then back up
EDIT:
I found this:
static var pharmacyOrangeCard: CardView {
let view = Bundle.main.loadNibNamed("Pharmacy Orange Card", owner: self, options: nil)?.first as! PharmacyTableCardView
print(view.frame.height)
return view
}
prints the correct height. But then, when I try to access it from the controller above, it gives me a smaller number! In the meanwhile, I applied these constraints:
self.translatesAutoresizingMaskIntoConstraints = false
self.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 35).isActive = true
card.centerXAnchor.constraint(equalTo: cell.centerXAnchor).isActive = true
card.topAnchor.constraint(equalTo: cell.topAnchor).isActive = true
But I don't think that affects height, does it?
EDIT 2:
Okay, so I've changed this constraint:
self.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 35).isActive = true
to this:
card.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: 17.5).isActive = true
card.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -17.5).isActive = true
So these constraints seem to play a role because now I have this:
enter image description here
By the way, I don't know if that matters but I'm using XIB files for each of these "cards", and the height isn't constrained, so maybe that plays a role?
SOLVING EDIT:
I solved the problem by doing:
override func viewDidAppear(_ animated: Bool) {
mainTableView.reloadData()
}
Once a cell loaded on the screen, you cannot change height for that cell for better UI-Experience,
and in hierarchy heightForRowAt get called before cellForRowAt.
So you had 2 options to choose for a solution to your problem
first:: get your heights values ready before your table view try to loads cells in it (get heights array ready before setting delegate and datasource values to your tableView)
second:: whenever you need to update your tableView cells to re-established with respect to new height values, call this each time after you have updated your height values
yourTableView.reloadData()
Related
Apple doc say : reloadRowsAtIndexPaths:withRowAnimation:
:
Call this method if you want to alert the user that the value of a cell is changing. If, however, notifying the user is not important—that is, you just want to change the value that a cell is displaying—you can get the cell for a particular row and set its new value.
My problem scenario is that i want to update a label in a cell on button click and also update the layout (i.e. a new view of the cell is being added or removed on a condition applied). if reloadRowsAtindexPath is called on the button click then the tableview randomly scroll down to some other row in the tableview. If only the cell content is updated on button click then the layout is not updated properly.
If anybody has faced the same issue with the reload?
Well this turned out trickier than I expected.
Here's how I implemented it. I am not really sure if this is a good way or not so take it with a grain of salt. Find link to the project below.
You need to know two things:
The default/normal cell height (which is basically the estimated height of cell).
Increase in height of cell after the view has been added to the cell (in my example I have taken the height to be 200).
When you tap on a button which is supposed to add the subview you call a completion handler passing the indexPath and heightToBeAdded(200 in this example) as parameters.
The completion will look something like this:
self.indexPath = iPath
self.cellHeight = self.defaultCellHeight + heightToBeAdded
UIView.beginAnimations("aaa", context: nil)
UIView.setAnimationDuration(1)
self.tableView.beginUpdates()
self.tableView.reloadRows(at: [iPath], with: .none)
self.tableView.endUpdates()
UIView.commitAnimations()
if heightToBeAdded > 0.0 {
self.addCellSubviews()
}
The iPath is the same indexPath that you sent a parameter. The cell height is calculated from the two things I have described above.
When endUpdates() calls the delegate and datasource methods would encounter the following method:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
guard let iPath = self.indexPath else { return UITableViewAutomaticDimension }
if indexPath == iPath {
return cellHeight
}
else {
return UITableViewAutomaticDimension
}
}
This will increase the cell height. But what about the actual subview that needs to be added?
For that we have addCellSubviews() which is actually a completion block that is executed inside the UITableViewCell subclass. Call this only after end updates because by that time cell height will be calculated properly.
let yView = UIView()
yView.backgroundColor = .yellow
yView.translatesAutoresizingMaskIntoConstraints = false
self.addAsSubview(sView: yView)
NSLayoutConstraint.activate([
yView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 8),
yView.topAnchor.constraint(equalTo: self.topAnchor, constant: 8),
yView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -275),
yView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8)
])
UIView.animate(withDuration: 1, animations: {
self.layoutIfNeeded()
})
Here is the result -
Link to the project
Please note there are some caveats(like tapping one cell closes the other) which I am sure you will be able to work out.
This was an issue with the estimated row height. changing the value to some other estimate fixed the issue of unwanted scrolling.
Mentioned in apple docs: "Additionally, try to make the estimated row height as accurate as possible. The system calculates items such as the scroll bar heights based on these estimates. The more accurate the estimates, the more seamless the user experience becomes."
I am having trouble getting my UITableView to appear full height in my Stack View.
My view tree looks as follows:
- View
- Scroll View
- Stack View
- Table View
- Image View
- Map View
The table view is dynamically populated with data, which works fine. The issue is that only one row is visible at a time and I have to scroll through the list. What I would like to see happen is for the table view to take as much vertical room as it needs to display all the cells.
I did try adjusting table height as follows, but that just ends up with table that no longer scrolls, though even if it did work I would rather have something more dynamic:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.detailsTableView.frame.size.height = 200
}
I am suspecting that it is probably an aspect of the 'stack view' that needs adjusting, but I am not sure at this point. Can anyone suggest an appropriate way?
I had been encountering the same issue and realized you need a self sizing table view. I stumbled on this answer and created a subclass like #MuHAOS suggested. I did not encounter any issues.
final class IntrinsicTableView: UITableView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
A UIStackView will compress views wherever it can, to counteract this set a height anchor and width anchor to the UITableView or a priority for its height and width. Here is a working example of how we can be in charge of the dimensions of a table within a stack view.
An extension to instantiate and centrally position the UIStackView
First of all I've written a UIStackView extension so that I don't need to include all the code inside the view controller. Your positioning and setup will be different because you are placing your stack view inside a scroll view, but separating this code out means you can make your own adjustments.
extension UIStackView {
convenience init(axis:UILayoutConstraintAxis, spacing:CGFloat) {
self.init()
self.axis = axis
self.spacing = spacing
self.translatesAutoresizingMaskIntoConstraints = false
}
func anchorStackView(toView view:UIView, anchorX:NSLayoutXAxisAnchor, equalAnchorX:NSLayoutXAxisAnchor, anchorY:NSLayoutYAxisAnchor, equalAnchorY:NSLayoutYAxisAnchor) {
view.addSubview(self)
anchorX.constraintEqualToAnchor(equalAnchorX).active = true
anchorY.constraintEqualToAnchor(equalAnchorY).active = true
}
}
We don't set a size for the UIStackView only a position, it is the things contained within it that determine its size. Also note the setting of translatesAutoresizingMaskIntoConstraints to false in the UIStackView extension. (It is only required that we set this property for the stack view, its subviews simply inherit the behaviour.)
UITableView class with data source code
Next I've created a basic table class for demo purposes.
class MyTable: UITableView, UITableViewDataSource {
let data = ["January","February","March","April","May","June","July","August","September","October","November","December"]
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("SauceCell", forIndexPath: indexPath)
cell.textLabel?.text = data[indexPath.row]
return cell
}
}
Setup of stack view and table in view controller
Finally, the important stuff. As soon as we add our table to the stack view all the frame information is disregarded. So we need the final two lines of code to set the width and height for the table in terms that Auto Layout can understand.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let table = MyTable(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height))
table.registerClass(UITableViewCell.self, forCellReuseIdentifier: "SauceCell")
table.dataSource = table
let stack = UIStackView(axis: .Vertical, spacing: 10)
stack.anchorStackView(toView: view, anchorX: stack.centerXAnchor, equalAnchorX: view.centerXAnchor, anchorY: stack.centerYAnchor, equalAnchorY: view.centerYAnchor)
stack.addArrangedSubview(table)
table.widthAnchor.constraintEqualToAnchor(view.widthAnchor, multiplier: 1).active = true
table.heightAnchor.constraintEqualToAnchor(view.heightAnchor, multiplier: 0.5).active = true
}
}
Note that we use addArrangedSubview: not addSubview: when adding views to the stack view.
(I've written blogposts about UIStackView as well as others about Auto Layout in general that might help too.)
I have been struggling this issue for 3 days and still can not figure it out. I do hope anyone here can help me.
Currently, i have an UITableView with customized cell(subclass of UITableViewCell) on it. Within this customized cell, there are many UILabels and all of them are set with Auto Layout (pining to cell content view) properly. By doing so, the cell height could display proper height no matter the UILabel is with long or short text.
The problem is that when i try to set one of the UILabels (the bottom one) to be hidden, the content view is not adjusted height accordingly and so as cell.
What i have down is i add an Hight Constraint from Interface Builder to that bottom label with following.
Priority = 250 (Low)
Constant = 0
Multiplier = 1
This make the constrain with the dotted line. Then, in the Swift file, i put following codes.
override func viewDidLoad() {
super.viewDidLoad()
//Setup TableView
tableView.allowsMultipleSelectionDuringEditing = true
//For tableView cell resize with autolayout
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 200
}
func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
let cell = tableView.cellForRowAtIndexPath(indexPath) as! RecordTableViewCell
cell.lbLine.hidden = !cell.lbLine.hidden
if cell.lbLine.hidden != true{
//show
cell.ConstrainHeightForLine.priority = 250
}else{
//not show
cell.ConstrainHeightForLine.priority = 999
}
//tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
return indexPath
}
The tricky thing is that when i call tableView.reloadRowAtIndexPaths(), the cell would display the correct height but with a bug that it has to be trigger by double click (selecting) on the same row rather than one click.
For this, i also try following code inside the willSelectRowAtIndexPath method, but none of them is worked.
cell.contentView.setNeedsDisplay()
cell.contentView.layoutIfNeeded()
cell.contentView.setNeedsUpdateConstraints()
Currently the result is as following (with wrong cell Height):
As showed in the Figure 2, UILabel 6 could be with long text and when i hide this view, the content view is still showing as large as it before hiding.
Please do point me out where i am wrong and i will be appreciated.
I finally change the code
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
to the following
tableView.reloadData()
Then, it work perfectly.
However, i don't really know the exactly reason on it. Hope someone can still comment it out.
Update
I have revised the question completely after my latest findings.
Goal
My goal is to implement the following effect:
There is a simple table view
The user selects a row
The selected row expands, revealing another label below the original one
Please note that I am aware, that this can be achieved by inserting/deleting cells below the selected one, I already have a successful implementation using that method.
This time, I want to try to achieve this using Auto Layout constraints.
Current status
I have a sample project available for anyone to check, and also opened an issue. To summarize, here's what I've tried so far:
I have the following views as actors here:
The cell's content view
A top view, containing the main label ("main view")
A bottom view, below the main view, containing the initially hidden label ("detail view")
I have set up Auto Layout constraints within my cell the following way (please note that this is strictly pseudo-language):
mainView.top = contentView.top
mainView.leading = contentView.leading
mainView.trailing = contentView.trailing
mainView.bottom = detailView.top
detailView.leading = contentView.leading
detailView.trailing = contentView.trailing
detailView.bottom = contentView.bottom
detailView.height = 0
I have a custom UITableViewCell subclass, with multiple outlets, but the most important here is an outlet for the height constraint mentioned previously: the idea here is to set its constant to 0 by default, but when the cell is selected, set it to 44, so it becomes visible:
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
detailViewHeightConstraint.constant = selected ? detailViewDefaultHeight : 0
UIView.animateWithDuration(0.3) {
self.layoutIfNeeded()
}
}
I have the following result:
So the effect is working, but not exactly how I originally imagined. Instead of pushing the main view up, I want the cell's height to grow when the detail view is shown, and shrink back when it's hidden.
I have examined my layout hierarchy during runtime:
The initial state is OK. The height of the content view is equal to the height of my main view (in this case, it's 125 points).
When the cell is selected, the height constraint of the detail view is increased to 44 points and the two views are properly stacked vertically.But instead of the cell's content view extending, but instead, the main view shrinks.
Question
What I need is the following: the height of table view cell's content view should be equal to
the height of the main view, when the detail view's height constraint is 0 (currently this works)
main view height + detail view height when the detail view's height constraint is set properly (this does not work).
How do I have to set my constraints to achieve that?
After a significant amount of research, I think I've found the solution with the help of this great article.
Here are the steps needed to make the cell resize:
Within the Main, and Detail Views, I have originally set the labels to be horizontally and vertically centered. This isn't enough for self sizing cells. The first thing I needed is to set up my layout using vertical spacing constraints instead of simple alignment:
Additionally you should set the Main Container's vertical compression resistance to 1000.
The detail view is a bit more tricky: Apart from creating the appropriate vertical constraints, you also have to play with their priorities to reach the desired effect:
The Detail Container's Height is constrained to be 44 points, but to make it optional, set its priority to 999 (according to the docs, anything lower than "Required", will be regarded such).
Within the Detail Container, set up the vertical spacing constraints, and give them a priority of 998.
The main idea is the following:
By default, the cell is collapsed. To achieve this, we must programmatically set the constant of the Detail Container's height constraint to 0. Since its priority is higher than the vertical constraints within the cell's content view, the latter will be ignored, so the Detail Container will be hidden.
When we select the cell, we want it to expand. This means, that the vertical constraints must take control: we set the priority Detail Container's height constraint to something low (I used 250), so it will be ignored in favor of the constraints within the content view.
I had to modify my UITableViewCell subclass to support these operations:
// `showDetails` is exposed to control, whether the cell should be expanded
var showsDetails = false {
didSet {
detailViewHeightConstraint.priority = showsDetails ? lowLayoutPriority : highLayoutPriority
}
}
override func awakeFromNib() {
super.awakeFromNib()
detailViewHeightConstraint.constant = 0
}
To trigger the behavior, we must override tableView(_:didSelectRowAtIndexPath:):
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: false)
switch expandedIndexPath {
case .Some(_) where expandedIndexPath == indexPath:
expandedIndexPath = nil
case .Some(let expandedIndex) where expandedIndex != indexPath:
expandedIndexPath = nil
self.tableView(tableView, didSelectRowAtIndexPath: indexPath)
default:
expandedIndexPath = indexPath
}
}
Notice that I've introduced expandedIndexPath to keep track of our currently expanded index:
var expandedIndexPath: NSIndexPath? {
didSet {
switch expandedIndexPath {
case .Some(let index):
tableView.reloadRowsAtIndexPaths([index], withRowAnimation: UITableViewRowAnimation.Automatic)
case .None:
tableView.reloadRowsAtIndexPaths([oldValue!], withRowAnimation: UITableViewRowAnimation.Automatic)
}
}
}
Setting the property will result in the table view reloading the appropriate indexes, giving us a perfect opportunity to tell the cell, if it should expand:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! ExpandableTableViewCell
cell.mainTitle = viewModel.mainTitleForRow(indexPath.row)
cell.detailTitle = viewModel.detailTitleForRow(indexPath.row)
switch expandedIndexPath {
case .Some(let expandedIndexPath) where expandedIndexPath == indexPath:
cell.showsDetails = true
default:
cell.showsDetails = false
}
return cell
}
The last step is to enable self-sizing in viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
tableView.contentInset.top = statusbarHeight
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 125
}
Here is the result:
Cells now correctly size themselves. You may notice that the animation is still a bit weird, but fixing that does not fall into the scope of this question.
Conclusion: this was way harder than it should be. 😀 I really hope to see some improvements in the future.
This is in obj-c, but I'm sure you'll handle that:
Add in your viewDidLoad:
self.tableView.estimatedRowHeight = self.tableView.rowHeight;
self.tableView.rowHeight = UITableViewAutomaticDimension;
This will enable self sizing cells for your tableView, and should work on iOS8+
I haven't been able to find any examples of this online. How can I achieve the following effect? Instead of the standard slide-to-left effect when tapping a table row, I'd like the view controller transition animation to look like the following:
User taps a cell in the table
The cell starts growing to fill the screen, pushing other rows above and below it "offscreen".
As the cell grows, cells elements (text, images, etc.) cross-fade into the new view's contents until the new view completely fills the screen.
I'd like to also be able to interactively transition back into the table view by dragging up from the bottom edge, such that the reverse of the above is achieved. i.e. view starts shrinking back into a normal table view cell as the "offscreen" cells animate back into position.
I've thought about taking a snapshot of the table and splitting it up at the points above and below the cell, and animating these snapshots offscreen as part of a custom view controller transition. Is there a better way? Ideally I'd like to not take snapshots, as I may want to have animations, etc., still happening in the table view rows as they fade offscreen.
First thing first, i have seen your post today and his is written in swift programming language.
the follwing code is to expand the selected cell to the full screen, where the subviews will fade out and background image will expands.
First complete cellForRowAtIndexPath, then in didSelectRowAtIndexPath,
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
isSelected = true
selectedCellIndex = indexPath.row
tableView.beginUpdates()
var cellSelected: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
cellSelected.frame = tableCities.rectForRowAtIndexPath(indexPath)
println("cell frame: \(cellSelected) and center: \(cellSelected.center)")
let newCell = cellSelected.frame.origin.y - tableOffset
println("new cell origin: \(newCell)")
var tempFrame: CGRect = cellSelected.frame
variableHeight = cellSelected.frame.origin.y - tableOffset
tempFrame.size.height = self.view.frame.height
println("contentoffset: \(tableView.contentOffset)")
let offset = tableView.contentOffset.y
println("cell tag: \(cellSelected.contentView.tag)")
let viewCell: UIView? = cellSelected.contentView.viewWithTag(cellSelected.contentView.tag)
println("label: \(viewCell)")
viewCell!.alpha = 1
UIView.animateWithDuration(5.0, delay: 0.1, options: UIViewAnimationOptions.BeginFromCurrentState, animations: {
tableView.setContentOffset(CGPointMake(0, offset + self.variableHeight), animated: false)
tableView.contentInset = UIEdgeInsetsMake(0, 0, self.variableHeight, 0)
tableView.endUpdates()
cellSelected.frame = tempFrame
viewCell!.alpha = 0
println("contentoffset: \(tableView.contentOffset)")
}, completion: nil)
}
then update your heightForRowAtIndexPath, as
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if isSelected && (selectedCellIndex == indexPath.row) {
return self.view.frame.height
}else {
return 100
}
}
Ignore this if already solved. and this might be helpful to others.