In an UITableViewCell, I have an UIView container, which contains a UIImageView on top and an UILabel on the bottom. When the UILabel does not contain text, I want to remove the UILabel and resize the UImageView to the bottom of my UIView container.
In my TableViewCell, I have this:
let x = imageViewPostPreview.bottomAnchor.constraint(equalTo: viewPreviewOverlay.topAnchor, constant: 0)
if postText == "" {
x.isActive = true
imageViewPostPreview.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner, .layerMinXMaxYCorner]
self.layoutSubviews()
self.layoutIfNeeded()
} else if postText != "" {
x.isActive = false
imageViewPostPreview.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
self.layoutSubviews()
self.layoutIfNeeded()
}
}
This piece of code works on when the initial UITableViewCell is loaded, but when I scroll my UITableView, the cell loses the initial layout.
How do I setup the layout correctly, so the layout is kept on scroll?
Edit:
Initial cell, which is correct:
After scrolling, incorrect layout:
Add this to prepareToReuse method
override func prepareForReuse() {
super.prepareForReuse()
let x = imageViewPostPreview.bottomAnchor.constraint(equalTo: viewPreviewOverlay.topAnchor, constant: 0)
if postText.isEmpty {
x.isActive = true
imageViewPostPreview.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner, .layerMinXMaxYCorner]
} else {
x.isActive = false
imageViewPostPreview.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
}
self.layoutIfNeeded()
}
Note: Dont call layoutSubview directly ... always try to call layoutifNeeded or setNeeedsLayout
A way you can have the cell calculate its size automatically, is to have constraints set inside the cell view for all directions on vertical axis. So, in your case the image view will have top, leading and trailing set to superview, and bottom to the label. The label will have leading and bottom to superview, and the top will be to the image view you set. It's important to set for vertical axis, since for table view we need it to calculate the height. This way the table view will know what to expect and can set height automatically.
After that you set tableView.rowHeight = UITableView.automaticDimension. Or you can also set in the delegate method.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
After all this is in place, you will only need to give the right values to the cell subviews(in this case set image and text). You can do this inside cell for row function. A sample code will look something like this.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath)
let model = self.models[indexPath.row]
cell.imageViewPostPreview.image = model.image ?? nil
cell.postText.text = model.text ?? nil
return cell
}
This should work without any further need to calculate constraints and calling layouts to update. Hope this helps! You can also check this question.
Related
I am trying to figure out why my custom styling for table cells disappears after scrolling down in a table view and them back up. What do I need to do to have the style persist?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : CustomTrendingCell = trendingTableView.dequeueReusableCell(withIdentifier: "cell") as! CustomTrendingCell
cell.selectionStyle = .none
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(searchForTrendingTerm(sender:)))
cell.iconView_search.isUserInteractionEnabled = true
cell.iconView_search.tag = indexPath.row
cell.iconView_search.addGestureRecognizer(tapGesture)
cell.trendingLabel.text = trendingSearchTerms[indexPath.row]
cell.elevate(elevation: 4.0) //where my style is being set
return cell
}
extension UIView {
func elevate(elevation: Double) {
self.layer.masksToBounds = false
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: elevation)
self.layer.shadowRadius = abs(CGFloat(elevation))
self.layer.shadowOpacity = 0.4
}
}
The top 2 items in the screenshot below have been scrolled down and up. The drop shadow styling has been removed. The bottom 2 have the correct styling and have not been scrolled down.
Screenshot example
One possible solution here is to explicitly specify the zPosition of each cell's layer.
You would want to ensure that the upper cell has the higher position so that it's content (shadow) lies over the lower cell.
In your cell for row at function add:
cell.layer.zPosition = CGFloat(numberOfRows - indexPath.row)
This is my code but not work!
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyChatCell
var frame = cell.frame
if(indexPath%2 == 0){
frame.size.width = CGFloat(200)
} else {
frame.size.width = CGFloat(100)
}
cell.frame = frame
return cell
}
I want change cell width to 100 or 200 per cell
But frame.size.width not work
You can't change that, because all cells in a table view must have the same width, but can have different heights if needed.
However, you can try to make the table view's .backgroundColor transparent, and maybe the content view of the cell, than add another "wrapper view" on the cell's content view and make it have different widths. It will create the "visual impression" that cells indeed have different widths.
I made a UITableView with cells that expand when you tap on them. It is modeled off the following project: https://github.com/rushisangani/TableViewCellExpand
Basically, the cell's container view has two subviews which act as containers for the expanded/contracted states - "front" and "back", each of which is constrained to the top, bottom, leading, and trailing edges of the cell's main content view. To make the cell expand or contract, I just toggle the isActive property on the bottom constraint of the front and back views. This works, but only if I reload the cell when it is tapped. If I just change the constraints and then try to call invalidateIntrinsicContentSize(), nothing happens.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
// Expand/contract the cell and invalidate size (doesn't work)
// let cell = tableView.cellForRow(at: indexPath) as! ExpandingCell
// cell.tap()
// cell.invalidateIntrinsicContentSize()
// Keep track of the selected index and configure the expand/contract state when the cell is remade
tableView.deselectRow(at: indexPath, animated: false)
expandedIndexPath = indexPath
if(!expandedIndexPathArray.contains(indexPath)){
expandedIndexPathArray.append(indexPath)
}
else{
expandedIndexPathArray = expandedIndexPathArray.filter({$0 != indexPath})
}
// Whenever a cell's intrinsicContentSize changes, it must be reloaded
tableView.reloadRows(at: [indexPath], with: .none)
}
What's going on behind the scenes? Why can't the cell recalculate its size without being reloaded?
If you want to contract/expand cells based on their intrinsic content size, you need to do the following 3 steps:
Configure the table view:
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44
For static table view controller, you will need to implement heightForRowAtIndexPath and return UITableViewAutomaticDimension.
Update cell constraints to reflect contracted/expanded state
Ask the table view to reflect changes:
tableView.beginUpdates()
tableView.endUpdates()
I have a tableView with two custom cells. One for the header and one for the cells inside the tableView.
The Header cell looks like this:
Label Label ScrollView(inside scrollView is an imageView)
The other cell looks like this:
Label TextField ScrollView(inside scrollView is an imageView)
When I scroll one cell horizontally all other cells (including header cell) will get the same contentOffset. This works like a charm.
I added another function which adds another imageView inside the scrollView on top of the existing imageView for all cells except the header cel. Inside the new imageView I add a line which is draggable. This is realized by a longPressureGesture. Inside this gesture I need to do a tableView.reloadData() so that each time the line moves it will be updated for all cells. LongPressureGesture allows me to do this without loosing the control of the line while doing a reloadData().
But here is my problem:
The contentOffset of the scrollView inside the headerCell is reseted after calling reloadData() inside the longPressureGesture. But the contentOffset for all other scrollViews in the cells are still the same.
I tried to add the contentOffset in the headerCell so that each time the reloadData is called the contentOffset will be set. But this is not working because the contentOffset will be called to early and has no effect.
If I add a delay and then set the contentOffset again it is working. But this ends up in flickering which is not good.
Edit: Tried to use only one cell (used cell for header instead of specific header cell). Ended up with the same result. So this is an general issue for headers in tableView?
My code for the cells is:
/*
* This method fills all the given information for each signal to a custom cell by type SignalCell.
* Each cell has a signalName, a value at specific time and a wave image.
*/
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! SignalCell
cell.delegate = self
// Get the signal for this row
let signal = VCDHelper.sharedInstance.signals[indexPath.row] as! VCDSignal
// Set the name of the signal to the label
cell.signalLabel.text = signal.name
cell.name = signal.name
// cellContainer is set inside renderWave every time a new Cell appears
renderTimePicker(cell, signal: signal, indexPath: indexPath)
renderWave(cell, signal: signal, indexPath: indexPath)
cellContainer.setObject(cell, forKey: signal.name)
return cell
}
Code for header:
// Display header cell
override func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCellWithIdentifier(cellHeaderIdentifier) as! HeaderCell
// Add object to renderer dictionary
var headerRenderer = headerToRender.objectForKey("header") as? TimeRenderer
// Render waves
if let headerRenderer = headerRenderer {
cell.timeImageView.image = headerRenderer.renderedTime()
} else {
// There is no renderer yet
headerRenderer = TimeRenderer()
headerToRender.setObject(headerRenderer!, forKey: "header")
// Creates and returns an NSBlockOperation object with block
let operation = NSBlockOperation(block: { () -> Void in
let renderedHeaderImage = headerRenderer!.renderedTime()
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
cell.timeImageView.image = renderedHeaderImage
cell.cellContainer = self.cellContainer
})
})
queue.addOperation(operation)
headerToRenderOperations.setObject(operation, forKey: "header")
}
cellContainer.setObject(cell, forKey: "header")
return cell
}
And here the code for saving/setting the contentOffset for each Cell realized in SignalCell class:
/*
* This method allows only horizontal scrolling.
* It also handles that scrolling out of bounds is not allowed
*/
func scrollViewDidScroll(scrollView: UIScrollView) {
// Set contentOffsetX to minimum bound value because we are out of range
if scrollView.contentOffset.x < 0 {
scrollCells(scrollView, contentOffsetX: 0.0)
VCDHelper.sharedInstance.waveScrollContentOffsetX = 0.0
}
if scrollView.contentOffset.x > 0 {
// ContentOffsetX is the point at the beginning of the scrollView
// We know the total size of the image and need to subtract the size of the scrollView
// This will result in the max contentOffset.x for scrolling
if scrollView.contentOffset.x <= CGFloat(VCDHelper.sharedInstance.imageWidth) - scrollView.bounds.width {
scrollCells(scrollView, contentOffsetX: scrollView.contentOffset.x)
VCDHelper.sharedInstance.waveScrollContentOffsetX = Float(scrollView.contentOffset.x)
} else {
// Set contentOffsetX to maximal bound value because we are out of range
scrollCells(scrollView, contentOffsetX: CGFloat(VCDHelper.sharedInstance.imageWidth) - scrollView.bounds.width)
}
}
}
/*
* This method scrolls all visible images inside the scrollView at once
*/
func scrollCells(scrollView: UIScrollView, contentOffsetX: CGFloat) {
for (key, cell) in self.cellContainer {
if key as! String == "header" {
let headerCell = cell as! HeaderCell
let scrollContentOffsetY = headerCell.timeScrollView.contentOffset.y
// Dont use setContentOffset because this will call scrollViewDidScroll each time
headerCell.timeScrollView.contentOffset = CGPoint(x: contentOffsetX, y: scrollContentOffsetY)
} else {
let signalCell = cell as! SignalCell
let scrollContentOffsetY = signalCell.signalScrollView.contentOffset.y
// Dont use setContentOffset because this will call scrollViewDidScroll each time
signalCell.signalScrollView.contentOffset = CGPoint(x: contentOffsetX, y: scrollContentOffsetY)
}
}
}
Here's what my application looks like currently.
You'll see that when the subtitle text label is only one line is resizes correctly, but when there's multiple lines it gets all messed up. I think this has to do with the constraints possibly? Right now I'm using the auto-layout constraints. You can see them in the screenshot. Here's the code that creates my cells.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> DealCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Deal Cell", forIndexPath: indexPath) as DealCell
let currentBar = bars[indexPath.row] as BarAnnotation
cell.barName.text = currentBar.name
cell.deal.text = currentBar.deal
cell.distanceToBar.text = String(format: "%.3f mi", currentBar.distance)
// Set the height of the table view cells
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0;
return cell
}
Any ideas?
Use tableView:heightForRowAtIndexPath: function in the UITableViewDelegate.
Using this you can individually set the height of each row separately.
For more information : https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDelegate_Protocol/index.html#//apple_ref/occ/intfm/UITableViewDelegate/tableView:heightForRowAtIndexPath:
Or you can use auto layout settings like in this article.
http://useyourloaf.com/blog/2014/02/14/table-view-cells-with-varying-row-heights.html