UITableView not drawing cells properly on initial load - Swift - ios

I have a table view that's in a nib file and it loads the data properly. It loads the required number of cells and fills in the data correctly. The cell is a xib file as well that includes multiple views. Each cell has a custom height. I managed to set the right height per cell using:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return (data[indexPath.row].height + 200)
}
The problem is that on initial load the cells don't get drawn properly like so:
Initial load image
The description area doesn't move where I made it move by changing its X and Y coordinates.
This gets fixed however when I scroll the table view beyond the initially loaded cells and go back to them, like so:
After scrolling
Upon loading the cell I move the description area below the image. This works for cells but not on the initially loaded ones. It only gets fixed once I scroll the broken cells out of view and go back to them. How do I fix this? How can I make it so that the cells get drawn properly on the initial load?
Edit: To clarify: The cells get loaded properly but aren't drawn correctly. I have to scroll out of the first few cells and scroll back to them for the cells to be drawn right.
Any help is appreciated. Thank you!

I can see that the image you'r trying to load is of larger dimension than that of the image container view.
You can try giving clipsToBound = True to the imageView.
This would most probably resolve your issue.
Thanks.

you want to increase UITableViewCell height based on image inside it, right?
Here is tableview datasource and delegate.
extension MyDataSource: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = self.rowHeights[indexPath.row] {
return height
} else {
return 100
}
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = self.rowHeights[indexPath.row] {
return height
} else {
return 100
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return mydatas.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let mydata = mydatas[indexPath.row]
var cell: UITableViewCell
cell = tableView.dequeueReusableCell(withIdentifier: 'myCell.cellIdentify()')!
cell.contentView.backgroundColor = UIColor.clear
cell.backgroundColor = UIColor.clear
cell.tag = indexPath.row
cell.selectionStyle = .none
let image = UIImage(named: promotion)
(cell as! myCell).configureCell(downloadImage: image!)
let aspectRatio = (image?.size.height)!/(image?.size.width)!
let imageHeight = (cell.contentView.frame.width * aspectRatio) + 16
UIView.performWithoutAnimation {
self.myTableView?.beginUpdates()
self.rowHeights[indexPath.row] = imageHeight
self.myTableView?.endUpdates()
}
return cell
}
}
here is UITableViewCell custom class.
myCell
import UIKit
class myCell: UITableViewCell {
#IBOutlet weak var imageHolder: WMView!
#IBOutlet weak var actionImage: UIImageView!
#IBOutlet weak var loaderIndicator: UIActivityIndicatorView!
override func prepareForReuse() {
super.prepareForReuse()
self.loaderIndicator.isHidden = false
self.loaderIndicator.startAnimating()
self.actionImage.image = nil
}
static func cellHeight() -> CGFloat {
return 73.0
}
static func cellIdentify() ->String {
return "myCell"
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func configureCell(downloadImage: UIImage) {
self.imageHolder.layer.borderWidth = 1
self.imageHolder.layer.cornerRadius = 5
self.imageHolder.layer.borderColor = UIColor.clear.cgColor
self.imageHolder.layer.masksToBounds = true
self.imageHolder.clipsToBounds = true
//ImageHolder
self.actionImage.image = downloadImage
self.loaderIndicator.isHidden = true
}
}

Related

layoutSubviews() is ran every time the cell is tapped

I'm developing an iOS app in Swift and I have a UITableView with custom UITableViewCells that have spacing at the left and right of the cell by overriding layoutSubviews().
This works by subtracting 40 to self.bounds.size.width.
As I'm not really good at explaining, here you have an image:
But the problem is that when I click on a cell, the overridden layoutSubviews() function is run again, so the cell is shrunken again as it subtracts 40 again to the already original self.bounds.size.width - 40.
How can I avoid layoutSubviews() running every time the cell is clicked and make it run only once when the view is load?
I do not know why the layoutSubviews is run again, as it is loaded in my custom UITableViewCell class.
Here you have my code:
class TBRepoTableViewCell: UITableViewCell {
#IBOutlet weak var repoLabel: UILabel!
#IBOutlet weak var urlLabel: UILabel!
#IBOutlet weak var repoIcon: UIImageView!
override func layoutSubviews() {
// Set the width of the cell
self.bounds = CGRect(self.bounds.origin.x, self.bounds.origin.y, self.bounds.size.width - 40, self.bounds.size.height)
super.layoutSubviews()
}
}
class SourcesViewController: UITableViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// note that indexPath.section is used rather than indexPath.row
print("You tapped cell number \(indexPath.row).")
}
override func viewWillAppear(_ animated: Bool) {
setEditing(false, animated: true)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TBRepoTableViewCell
let repoArray = array[indexPath.row]
cell.repoLabel.text = repoArray.repoName
cell.urlLabel.text = repoArray.repoURL
cell.repoIcon.image = repoArray.icon
self.tableView.separatorStyle = .none
// add borders
cell.layer.cornerRadius = 10
cell.clipsToBounds = true
cell.layer.masksToBounds = true
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
array.remove(at: indexPath.row)
tableView.reloadData()
}
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
tableView.reloadData()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return array.count
}
}
Never modify the view's frame or bounds in layoutSubviews. The only thing you should do there is update the frame or bounds of the view's subviews based on the current size of the view.
What you should do is modify your cell view class such that it has a view that shows the indent and the rounded corners. Let's call this a "background view". Add the other subviews of the cell to this background view. Then add the background view to the cell's contentView.
Your cellForRowAt should not be changing the cell's layer. That logic belongs in the custom cell class.

UITableView changing image and title position cell by cell

I was wondering if there any possible way to create a table view with this style:
I have a dictionary contains title and image values, I need to create a cell one Image-Right / Title-Left and next vice versa. How can achieve something like this?
You can do it by setAffineTransform in this way:
• build up your tableView with one prototype cell that has an image in left and a label in right
• then do this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "YourCellIdentifier", for: indexPath) as! YourTableViewCell
if (indexPath.row % 2 == 0) {
cell.contentView.layer.setAffineTransform(CGAffineTransform(scaleX: -1, y: 1))
cell.YourImage.layer.setAffineTransform(CGAffineTransform(scaleX: -1, y: 1))
cell.YourLabel.layer.setAffineTransform(CGAffineTransform(scaleX: -1, y: 1))
}
// do what ever you want ...
return cell
}
also the best solution is defining 2 prototype cells but in your case this is a tricky and fast way to achieve your goal.
Yes, you can use a table view to achieve your requirement. you will need to follow the following steps.
Method 1:
Create two table view cell XIB's one with left side label and right side image, the second one is with left side image and right side image.
Keep same class of both the XIB's you have created but with different identifiers.
In your Table view cellForRowAtIndexPath method implement following logic.
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return datasourceArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if(indexPath.row % 0 == 0) {
let cell = tableView.dequeueReusableCell(withIdentifier: "RightLabelTableViewCell", for: indexPath) as! CustomTablViewCell
cell.model = datasourceArray[indexPath.row]
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "LeftLabelTableViewCell", for: indexPath) as! CustomTablViewCell
cell.model = datasourceArray[indexPath.row]
return cell
}
}
}
Note: You can use one class for TableViewCell with a different
identifier and design your xib's accordingly.
Method 2:
Flip your table view cell's content view in a such a way that they will swap in your UI.
add the following code into your cellForRowAtIndexPath and also add else part of it because cell for a row may behave weirdly because of dequeing:
extension UIView {
/// Flip view horizontally.
func flipX() {
transform = CGAffineTransform(scaleX: -transform.a, y: transform.d)
}
}
Usage:
cell.contentView.flipX()
cell.yourImage.flipX()
cell.youImageName.flipX()
Don't forget to add else part in cellForRowAt method.
There are actually many ways of doing this:
Create 2 cells. Have 2 cells like OddTableViewCell and EvenTableViewCell. You can choose with index path which to use in cellForRow method like:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if(indexPath.row%0 == 0) {
let cell = tableView.dequeueReusableCell(withIdentifier: "EvenTableViewCell", for: indexPath) as! EvenTableViewCell
cell.model = dataArray[indexPath.row]
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "OddTableViewCell", for: indexPath) as! OddTableViewCell
cell.model = dataArray[indexPath.row]
return cell
}
}
}
Have a single cell but duplicate views so you have 2 labels and 2 image views. Then hide them as you need to:
class MyCell: UITableViewCell {
#IBOutlet private var leftImageView: UIImageView?
#IBOutlet private var rightImageView: UIImageView?
#IBOutlet private var leftLabel: UILabel?
#IBOutlet private var rightLabel: UILabel?
var userImage: UIImage? {
didSet {
refresh()
}
}
var userName: String? {
didSet {
refresh()
}
}
var imageOnLeft: Bool = false {
didSet {
refresh()
}
}
func refresh() {
leftImageView?.image = imageOnLeft ? userImage : nil
leftImageView?.isHidden = !imageOnLeft
rightImageView?.image = imageOnLeft ? nil : userImage
rightImageView?.isHidden = imageOnLeft
leftLabel?.text = imageOnLeft ? nil : userName
leftLabel?.isHidden = imageOnLeft
rightLabel?.text = imageOnLeft ? userName : nil
rightLabel?.isHidden = !imageOnLeft
}
}
Have a single cell with stack view. Add a label and image view onto the stack view. You can change order of items in stack view. Some promising answer can be found here. The rest should be pretty similar to the second solution.
(4.) Also you could just use a collection view and have a label cell and an image cell.
Create one cell with 2 image and 2 label left and right
when you went to left side image that time hide right side image same as in label.
cell
import UIKit
class TestTableViewCell: UITableViewCell {
#IBOutlet weak var lbl_left: UILabel!
#IBOutlet weak var lbl_right: UILabel!
#IBOutlet weak var img_right: UIImageView!
#IBOutlet weak var img_left: UIImageView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
func configure_cell(left:Bool)
{
if left{
img_left.isHidden = true
img_right.isHidden = false
lbl_left.isHidden = false
lbl_right.isHidden = true
self.img_right.image = UIImage(named: "testimg")
}else{
img_left.isHidden = false
img_right.isHidden = true
lbl_left.isHidden = true
lbl_right.isHidden = false
self.img_left.image = UIImage(named: "testimg")
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
ViewController
extension ViewController:UITableViewDataSource,UITableViewDelegate
{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 120
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 120
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "TestTableViewCell", for: indexPath) as? TestTableViewCell
if (indexPath.row + 1) % 2 == 0 {
cell?.configure_cell(left: true)
} else {
cell?.configure_cell(left: false)
}
return cell!
}
}

Multiple UICollectionView in a UITableView are intertwined in scrolling

I am totally new to iOS development, so my problem might be a noob question but I have no clue how to make my UICollectionViews scroll separately from each other. When I scroll my top UICollectionView, scroll down, my other UICollectionView is also scrolled without touching it.
The UICollectionView is configured to scroll horizontally. What I want to achieve is something like Netflix has done. With the user being able to scroll horizontally, while the list of items is shown horizontally. So far everything works, except the scrolling of one list makes the other scrolls too when they are redrawn (I guess, since I can only see it happen on lists that are not yet shown on the device.)
I hope I have described my problem properly, might you be missing some information to help me solve this I am of course happy to provide.
So my UITableController looks like this:
class MoviesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var containerView: UIView!
#IBOutlet weak var tableView: UITableView!
let categories = ["In theaters", "Popular", "Upcoming", "Top rated"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
containerView.backgroundColor = UIColor.init(hex: "232637")
tableView.backgroundColor = UIColor.init(hex: "232637")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func numberOfSections(in tableView: UITableView) -> Int {
return categories.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
tableView.rowHeight = 332;
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath) as! TableViewCell
return cell
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 42;
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return categories[section]
}
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int){
view.tintColor = UIColor.init(hex: "232637")
let header = view as! UITableViewHeaderFooterView
header.textLabel?.textColor = UIColor.init(hex: "ff5959")
}
}
my UITableViewCell looks like this:
class TableViewCell: UITableViewCell, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var collectionView: UICollectionView!
let imageNames = //Some filler data
let gameNames = //Some filler data
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
collectionView.backgroundColor = UIColor.init(hex: "232637")
collectionView.contentInset = UIEdgeInsets(top: 0,left: 0,bottom: 32,right: 0)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageNames.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCell", for: indexPath) as! MovieCell
//cell.imageView.imageFromURL(urlString: imageNames[indexPath.row])
let image = UIImageView(image: UIImage(named: "img-logo"))
image.contentMode = .scaleAspectFit
image.frame = cell.containerView.bounds
image.layer.cornerRadius = 10;
image.clipsToBounds = true;
image.imageFromURL(urlString: imageNames[indexPath.row])
cell.containerView.addSubview(image)
cell.containerView.backgroundColor = UIColor.init(hex: "232637")
return cell
}
}
And my UICollectionViewCell looks like this:
class MovieCell: UICollectionViewCell {
#IBOutlet weak var containerView: UIView!
}
My Views look like this:
It sounds like you are failing to take into account the fact that cells (table view cells as well as collection view cells) are reused. Thus, if a table view contains a collection view and you scroll that collection view and then you scroll the table view, the table view cell is reused in a new row and the previously scrolled collection view inside it remains scrolled to where you put it previously. If that's not what you want, it's up to you to reset the scroll position of the collection view when you discover that the cell is being reused.
What gave me a clue that you might be not be understanding cell reuse is this line:
cell.containerView.addSubview(image)
That's wrong, because you're doing it even if the cell is reused, meaning that some of your cells will end up with dozens of image views overlaying one another, slowing things down and eventually perhaps causing you to run out of memory. That's not the problem you asked about, but it is a sign that you are not aware of the implications of cell reuse.

Dynamically alter the height of UITableViewCell

I put UITableViewCell by xib file.
And, I want to change the height of Cell depending on the height of UITextView put in Cell.
I found an online article.
I think that it went according to this article.
But TableViewCell does not change depending on the height of TextView.
Please tell me where is wrong.
I will detail in detail what I did below.
1. I created UITableView and Cell (Xib) as shown below.
TableView.swift
import UIKit
class TableView: UIView, UITableViewDelegate, UITableViewDataSource, UITextViewDelegate {
#IBOutlet weak var newImage: UIImageView!
//etc...
class func instance() -> TableView {
return UINib(nibName: "TableView", bundle: nil).instantiate(withOwner: self, options: nil)[0] as! TableView
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = newTableView.dequeueReusableCell(withIdentifier: "NewCell", for: indexPath) as! NewCellTableViewCell
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 64
}
}
Cell.swift
import UIKit
class NewCellTableViewCell: UITableViewCell {
let bottomBorder = CALayer()
let vArchColor = archColor()
#IBOutlet weak var textView: UITextView!
override func awakeFromNib() {
super.awakeFromNib()
bottomBorder.frame = CGRect(x: 20, y: self.frame.size.height - 1.0, width: self.frame.width*2, height: 1.0)
bottomBorder.backgroundColor = vArchColor.black01.cgColor
self.layer.addSublayer(bottomBorder)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
print("Selected")
}
}
I set several necessary values in UITableView.
// in TableView.swift
func textViewDidChange(_ textView: UITextView) {
newTableView.beginUpdates()
newTableView.endUpdates()
}
// in TableView.swift
func initSomething(){
newTableView.rowHeight = UITableViewAutomaticDimension
newTableView.estimatedRowHeight = 10000
}
I used Storyboard in Cell (XibFile) and set constraints.
Of course, I also unchecked scrollenabled.
Image of Cell.xib
You've provided an implementation for heightForRowAtIndexPath: which will override on a per row basis any constant value you have set in tableView.rowHeight = .... Remove this method from your TableViewController and you should be golden, provided the constraints of your text view aren't ambiguous. Make sure it's pinned on all four sides and has a height constraint.

Resizing a tableView full screen when clicked

I am working on the following :
Screenshot
Basically it's one UIImageView inside of another UIImageView at the top (60% of the screen) and a UITableView that takes 40% of the screen
and code (see below) :
What I am trying to achieve is to resize the cells of the UITableView full screen when my customCell is clicked on and unsize it to it's original state when unclicked.
Is there any way to make this work?
Thanks in advance.
//Label outlets
#IBOutlet weak var tableView: UITableView!
var selectedIndexPath : IndexPath? = nil
override func viewDidLoad() {
super.viewDidLoad()
//MARK Start of CustomViewCell Code
tableView.register(UINib(nibName: "CustomViewCell", bundle: nil), forCellReuseIdentifier: "Cell")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! CustomViewCell
cell.clipsToBounds = true
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let index = indexPath
if selectedIndexPath != nil {
if index == selectedIndexPath {
return 667
} else {
return 62
}
} else {
return 62}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch selectedIndexPath {
case nil:
selectedIndexPath = indexPath
default:
if selectedIndexPath! == indexPath {
selectedIndexPath = nil
} else {
selectedIndexPath = indexPath
}
}
tableView.reloadRows(at: [indexPath], with: .automatic)
}
//MARK End of CustomViewCell Code
Besides reloading the cells with the new heights, you also need to change the frame of the table view. If you want to make the table occupy the whole screen, make sure you create an IBOutlet for it, say myTableView, then, after:
tableView.reloadRows(at: [indexPath], with: .automatic)
Add this code:
myTableView.frame = view.bounds
You may also create a Bool variable to keep track of normal and full-screen modes of the table view, and some CGRect variable to keep the initial frame. You may need something like this when you want the table view to go back to its initial frame:
myTableView.frame = initialTableViewFrame
Hope you get the idea.
Here is a pure auto-layout approach:
1.In your interface builder, create a UIImageView with constraints [Trailing, Leading, Top , Bottom, Height], make the height >= and priority is 999. You will use this height later in code for the expand/collapse logic.
2.Create a dummy UIView whose height will be programmatically set later, this UIView will 'push' / 'stretch' your cell height when you create the expand effect to your imageview. Set it's visibility to hidden since you don't need to show this to your user.
3.In your custom cell, create two NSLayoutConstraint instances as #IBoutlets, one for UIImageView height and one for the dummy UIView height then connect them later in your interface file.
let lowPriorityHeight : Float = 250;
let highPriorityHeight : Float = 999;
class CustomCell: UITableViewCell {
#IBOutlet weak var imgVwHeight: NSLayoutConstraint!
#IBOutlet weak var tempHeight : NSLayoutConstraint!
var isExpanded = false{
didSet{
imgVwHeight.priority = (isExpanded) ? lowPriorityHeight : highPriorityHeight;
if isExpanded == true{
tempHeight.constant = ExpandedHeight
}else{
tempHeight.constant = NormalHeight
}
}
}
4.In your main class, set the isExpanded to true or false to create the effect.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath) as! CustomCell
cell.isExpanded = !cell.isExpanded
tableView.reloadData()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : CustomCell = tableView.dequeueReusableCell(withIdentifier: "kCustomCell", for: indexPath) as! CustomCell
return cell
}
You may download my sample implementation HERE

Resources