tableView does not appear when centered into its parent view - ios

I have a viewController with the following (static) tableView:
class viewController: UIViewController, UITableViewDelegate {
private let tableView: UITableView = {
let tv = UITableView()
tv.separatorStyle = .singleLine
tv.allowsSelection = true
tv.isScrollEnabled = false
return tv
}()
private let tableData = ["row1", "row2", "row3", "row4"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.tableFooterView = UIView()
view.addSubview(tableView)
NSLayoutConstraints.activate([
tableView.centerXAnchor.constraints(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
tableView.centerYAnchor.constraints(equalTo: view.safeAreaLayoutGuide.centerYAnchor)
)]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
cell.textLabel!.text = tableData[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
}
When I run the app, this viewController shows a blank screen. I know that the way I am setting up the tableview's constraints is the problem because when I set up the tableView using topAnchor, bottomAnchor, leftAnchor, and rightAnchor (and with some other tweaking) the tableview appears. Any idea why the app is behaving this way?

Your table view is probably there, and centered, but you didn't define a size, so it's probably being set to zero width and height, that's why you don't see it.
You can fix this by setting a constraint on it's width and height, either to a constant or related to it's superview, depending on what you want.

The problem is this is NOT a static table view. If it were, you would not have implemented cellForRowAt. It is a normal table view and it needs a data source and delegate. Plus it needs a height and a width.

Related

Swift UITableViewCell separatorStyle breaking autolayout on iPhone MIni

I have a UITableView which has a UITableViewCell which contains a UIImageView.
The constraints are setup such that the UIImageView has padding 20 points at the top and sides, and a size ratio of 1:1, so the UIImageView will always be square regardless of the device width.
I apply a cornerRadius to the UIImageView so the image is circular.
However.... the autolayout doesn't seem to work on the first load. But after the first load, it works perfectly.
I have tried every known combination of setNeedsLayout or layoutIfNeeded - both inside the UITableViewCell and in the UITableView code. Nothing works the first time it loads.
Please help!
Code looks like this:
class CircularProfileCell: UITableViewCell {
#IBOutlet weak var circularView: UIView!
func setup() {
circularView.layer.cornerRadius = circularView.bounds.height / 2
}
}
class CircularProfileVC: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.separatorStyle = .none
self.tableView.register(UINib(nibName: "CircularProfileCell", bundle: nil), forCellReuseIdentifier: "CircularProfileCell")
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CircularProfileCell", for: indexPath) as! CircularProfileCell
cell.setup()
return cell
}
}
Setup looks like this:
Because corner radius is a layer property it does not always play well with auto layout. In addition, I guess you set it up with frame properties of the view (i.e imageView.layer.cornerRadius = imageView.bounds.height/2).
Hence you should try and set the corner radius on the layoutSubviews() function of the cell. This will make sure to render the correct size
override func layoutSubviews() {
super.layoutSubviews()
imageView.layer.cornerRadius = imageView.bounds.height/2
...
}
This only happens when the tableView.separatorStyle = .none
So to fix it I simply leave the separator on, but set the separator color to clear
self.tableView.separatorStyle = .singleLine
self.tableView.separatorColor = UIColor.clear
Thanks to #CloudBalacing for the help. More info about this problem here

UITableViewCell dynamic cell heights are incorrect until scrolled

I want my UITableViewCell to expand in size when tapped.
The layout of the cell is quite straightforward. Within the UITableViewCell is a UILabel. The UILabel is constrained to the UITableViewCell with top, bottom, left and right anchors.
I also have two stored properties. labelExpandedHeightConstraint stores the UILabel's height constraint for when the label is expanded. labelCompactHeightConstraint stores the UILabel's height constraint for when the label is compacted. Notice that labelCompactHeightConstraint is initially set to active.
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
let spacing = 8
self.addSubview(self.labelView)
self.labelView.translatesAutoresizingMaskIntoConstraints = false
self.labelView.topAnchor.constraint(equalTo: self.topAnchor, constant: spacing).isActive = true
self.labelView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -1 * spacing).isActive = true
self.labelView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
self.labelView.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -1 * spacing).isActive = true
self.labelExpandedHeightConstraint = self.labelView.heightAnchor.constraint(equalToConstant: 120)
self.labelCompactHeightConstraint = self.labelView.heightAnchor.constraint(equalToConstant: 80)
self.labelCompactHeightConstraint.isActive = true
}
The expand() function below is called whenever the user taps a UITapGestureRecognizer. This function is very simple. It expands the cell by disabling labelCompactHeightConstraint and enabling labelExpandedHeightConstraint.
#objc func expand() {
self.labelCompactHeightConstraint.isActive = false
self.labelExpandedHeightConstraint.isActive = true
self.layoutIfNeeded()
}
The problem is that when the expand() function is called, the UITableViewCell and its contents do not change in size. It is not until the user scrolls the cell off the screen, and then scrolls it back onto the screen, that the size adjusts correctly.
How can I get the cell to expand immediately when tapped? I would also like this sizing change to be animated. I would really appreciate any suggestions. Thanks!
You'll need to do it differently.
Something like:
tableView.beginUpdates()
// update data for cell, or if your cell is not dynamically created - update it directly
// Usually, you'll need to update your data structures
// Reload the cell
tableView.reloadRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
From what you wrote, the place to add this code is from:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
...
}
Also, note that in most cases, you should just change the content (ie. the text in the label) and not the constraint value.
Here is a minimal full example:
class ViewController: UITableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 100
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ident", for: indexPath) as! Cell
if selections.contains(indexPath) {
cell.height.constant = 80
} else {
cell.height.constant = 10
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.beginUpdates()
if selections.contains(indexPath){
selections.remove(indexPath)
} else {
selections.insert(indexPath)
}
tableView.reloadRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
var selections = Set<IndexPath>()
}
class Cell : UITableViewCell {
#IBOutlet var height : NSLayoutConstraint!
}
Could use the table view's multiple selection but wanted to demonstrate usage of app specific data.

How to dynamically resize custom UITableViewCells in UITableView?

I built a generic UITableView to which I can pass a model and a custom UITableViewCell and it's working well except for two things: the cells height and the UITableView height.
I guess these two issues are related.
I'm trying multiple things I found on SO to help with it but nothing worked out.
Here is a sample of my generic UITableView:
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(Cell.self, forCellReuseIdentifier: "Cell")
tableView.dataSource = self
tableView.delegate = self
tableView.separatorStyle = .none
loadList(appendItems: true)
initActions()
initLayout()
}
func initLayout() {
self.view.addSubview(tableView)
self.view.addSubview(emptyView)
self.view.addSubview(loadMoreButton)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.snp.makeConstraints { (make) -> Void in
make.left.right.leading.trailing.top.equalToSuperview()
}
tableView.isScrollEnabled = false
emptyView.translatesAutoresizingMaskIntoConstraints = false
emptyView.isHidden = true
emptyView.textAlignment = .center
emptyView.snp.makeConstraints { (make) -> Void in
make.left.right.leading.trailing.equalToSuperview()
make.top.equalToSuperview().offset(8)
}
self.loadMoreButton.isHidden = true
if(self.hasLoader == true) {
self.loadMoreButton.isHidden = false
loadMoreButton.translatesAutoresizingMaskIntoConstraints = false
loadMoreButton.snp.makeConstraints { (make) -> Void in
make.centerX.equalToSuperview()
make.top.equalTo(tableView.snp.bottom)
}
}
}
Then, when my data is loaded I do:
self.tableView.reloadData()
self.tableView.snp.remakeConstraints { (make) -> Void in
make.left.right.leading.trailing.top.equalToSuperview()
if(self.hasLoader == true) {
make.bottom.equalTo(self.loadMoreButton.snp.top)
}
make.height.equalTo(self.tableView.contentSize.height)
}
Then, my tableView funcs:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return resourceList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! Cell
let currentResource = resourceList[indexPath.row]
cell.setResource(resource: currentResource) // Here I initialize my labels, UIImage, etc.
cell.isUserInteractionEnabled = true
self.cellHeight = cell.contentView.frame.size.height // Something I tried
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tableView.deselectRow(at: indexPath, animated: true)
let resource = resourceList[indexPath.row]
if (resource.getRouteParam() != "") {
router.setRoute(routeName: resource.getRouteName(), routeParam: resource.getRouteParam())
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return self.cellHeight ?? 200 // Here I'm trying to return an *automatic* height of my custom cell
}
To complete I can say that my cells have different sizes due to different labels length so I can't define a static height.
I also tried to add estimatedHeightForRowAt in my viewDidLoad method but it didn't work either.
When I use UITableView.automaticDimension my cells have always a height of 44.0
1. UITableView configuration
To be able to have automatically sized cells in UITableView you will need to do set the following properties on your table view:
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 200 // or other value
2. Proper AutoLayout inside of UITableViewCell
Make sure yo have auto layout setup correctly in your cells.
3. (Optional) UITableViewDelegate and estimatedHeightForRowAt
Implement this method of UITableViewDelegate to provide the table with with more accurate size, so it will correctly handle fast scrolling.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 200 // try to calculate more accurate size
}
Some stuff I noticed
This will not really work how you expect it to work so don't bother with it. Here the cells views are not even correctly laid out, so there is a very high chance you will get problematic values (like 0)
self.cellHeight = cell.contentView.frame.size.height
There should be no need to resetting the constraints after you reload data. Setting them in the viewDidLoad should be enough.
self.tableView.reloadData()
self.tableView.snp.remakeConstraints
Resources
How to make UITableViewCells auto resize to their content
Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

Self sizing tableview inside self sizing tableview cell

Let's say I have hierarchy like this:
*TableViewCell
**TableView
***TableViewCell
and all of them should be resizable. Did someone face this kind of problem? In past I've used many workarounds like systemLayoutSizeFitting or precalculation of height in heightForRowAt, but it always breaks some constraints, because TableViewCell has height constraint equal to estimated row height and there appear some kinds of magic behavior. Any ways to make this live?
Current workaround:
class SportCenterReviewsTableCell: UITableViewCell, MVVMView {
var tableView: SelfSizedTableView = {
let view = SelfSizedTableView(frame: .zero)
view.clipsToBounds = true
view.tableFooterView = UIView()
view.separatorStyle = .none
view.isScrollEnabled = false
view.showsVerticalScrollIndicator = false
view.estimatedRowHeight = 0
if #available(iOS 11.0, *) {
view.contentInsetAdjustmentBehavior = .never
} else {
// Fallback on earlier versions
}
return view
}()
private func markup() {
contentView.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.register(ReviewsTableViewCell.self, forCellReuseIdentifier: "Cell")
tableView.snp.makeConstraints() { make in
make.top.equalTo(seeAllButton.snp.bottom).offset(12)
make.left.equalTo(contentView.snp.left)
make.right.equalTo(contentView.snp.right)
make.bottom.lessThanOrEqualTo(contentView.snp.bottom)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ReviewsTableViewCell
cell.viewModel = viewModel.cellViewModels[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! ReviewsTableViewCell
cell.viewModel = viewModel.cellViewModels[indexPath.row]
cell.setNeedsLayout()
cell.layoutIfNeeded()
let size = cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultLow)
return size.height
}
}
Self sizing tableView class:
class SelfSizedTableView: UITableView {
override func reloadData() {
super.reloadData()
self.invalidateIntrinsicContentSize()
self.layoutIfNeeded()
}
override var intrinsicContentSize: CGSize {
self.setNeedsLayout()
self.layoutIfNeeded()
return contentSize
}
}
This is actually not an answer to the question, but just an explanation.
(Wrote here because of the character count limitation for the comments).
The thing is that you're trying to insert a vertically scrollable view inside another vertically scrollable view. If you don't disable the nested tableview's scroll ability, you will have a glitch while scrolling, because the system wouldn't know to whom pass the scroll event (to the nested tableview, or to the parent tableview).
So in our case, you'll have to disable the "scrollable" property for the nested tableviews, hence you'll have to set the height of the nested tableview to be equal to its content size. But this way you will lose the advantages of tableview (i.e. cell reusing advantage) and it will be the same as using an actual UIScrollView. But, on the other hand, as you'll have to set the height to be equal to its content size, then there is no reason to use UIScrollView at all, you can add your nested cells to a UIStackView, and you tableview will have this hierarchy:
*TableView
**TableViewCell
***StackView
****Items
****Items
****Items
****Items
But again, the right solution is using multi-sectional tableview. Let your cells be section headers of the tableview, and let inner cells be the rows of the tableview.
here is an example of how to make a tableview inside a table view cell with automatic height for the cells.
You should use the 'ContentSizedTableView' class for the inner tableViews.
class ViewController: UIViewController {
#IBOutlet weak var outerTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
outerTableView.rowHeight = UITableView.automaticDimension
outerTableView.estimatedRowHeight = UITableView.automaticDimension
outerTableView.delegate = self
outerTableView.dataSource = self
}
}
final class ContentSizedTableView: UITableView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
sizeToFit()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? TableTableViewCell
return cell!
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
}
Use xib files to simplify the hierarchy.
Get a tableView on your storyboard, and create a nib file for your tableViewCell(say CustomTableViewCell). Inside it create a tableView and again create one more tableViewCell xib file. Now, no need of setting labels into your xib file,(if you want only labels in cells and nothing else, if not, there is another way of adding constraints)
Say you have an array of text, some strings are long and some are short.
register nib file in CustomTableViewCell and extend it to use Delegate and DataSource.
register this CustomTableViewCell in ViewController.
While declaring a cell in CustomTableViewCell, just do=
cell.textLabel?.text = content
cell.textLabel?.numberOfLines = 0
Use heightForRowAt to set outer tableViewCell's height, and let the inner tableView to scroll inside.

How to add spacing between UITableViewCells - Swift

I have a table with some customizations.
Here is my code:
import UIKit
class ViewController: UIViewController, UITableViewDelegate {
var exercises : [String] = ["Swimming", "Running", "Weight Lifting", "Biking", "Climbing"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
//Necessary for basic tableView setup. Defines number of rows in a specific section.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
//Setting the amount of rows to the number of elements in exercises. This function returns that.
tableView.backgroundColor = UIColor.clearColor()
return exercises.count
}
//Necessary for basic tableView setup. Helps us out content for every cell in the index path. Runs = rows
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
tableView.separatorColor = UIColor.clearColor()
//Setting the footer to default so the extra junk does not show
tableView.tableFooterView = UIView()
//This will be returned. This automatically creates a prototype cell
var cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
//Setting every cell to the respective item in exercises
cell.textLabel?.text = exercises[indexPath.row]
cell.textLabel?.font = UIFont(name: "Avenir-Light", size: 17)
cell.textLabel?.textColor = UIColor.whiteColor()
cell.textLabel?.textAlignment = .Center
//Border Code
cell.layer.borderWidth = 2.0
cell.layer.borderColor = UIColor.whiteColor().CGColor
//Round Corners
cell.layer.cornerRadius = 20
return cell
}
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
cell.backgroundColor = UIColor.clearColor()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I want to have some spacing between each UITableViewCell. I have already tried the following:
Change the height of each row. This option does not work because I have borders. Adding more height just makes each row look larger.
Convert each row into a section and then use heightForHeader in section.The post. I want to avoid this option because I would have to convert all my rows to sections.
Add a transparent UIView within each row. Again, this option does not work because I have borders.
Is there any other alternative?
Thanks
First of all, you should move tableView related code out of tableView:cellForRowAtIndexPath, preferably to viewDidLoad:
override func viewDidLoad {
super.viewDidLoad()
tableView.separatorColor = UIColor.clearColor()
tableView.tableFooterView = UIView()
}
Secondly, UITableViewCells are reusable objects so they are dequeued by the tableView when required:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Cell")
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
}
...
}
As for your problem, you should either set rowHeight on tableView
override func viewDidLoad {
super.viewDidLoad()
...
tableView.rowHeight = 100.0
}
or implement tableView:heightForRowAtIndexPath: instead:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 100.0
}
You should also update textLabel's border and corner radius value instead of the cell:
//Border Code
cell.textLabel.layer.borderWidth = 2.0
cell.textLabel.layer.borderColor = UIColor.whiteColor().CGColor
//Round Corners
cell.textLabel.layer.cornerRadius = 20
I tried ozgur's method but it didn't work because I had borders between my table view cells. Eventually, I used the answer from this post. Hope it helps
You can add spacing by creating an extra view inside the cell that contains the content of the cell that has spacing between the top and the bottom. Make the background color of the cell translucent and it'll appear as though the cell has spacing above and below it

Resources