iosswiftuitableviewimage-caching

TableView Cells not formatting correctly unless image is cached


I am having an issue with my tableview, where the cells don't orient correctly before an image is cached, and only once I return back to a page and my image is cached do they orient correctly. Here is an example of what I am talking about, the first image is when I first go to the page, and I have a function which stores images in an imagecache, and the second picture is when I return to that page after navigating somewhere else, and the images are cached.

Incorrectly Formatted:

enter image description here

Correctly Formatted:

enter image description here

I know it is a very subtle difference with the way it looks but I'd love to get that figured out and understand why this is happening

My cells constant height is 85, and here is the relevant code:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//Indexing part I left out dw about this



        let cell = discountTableView.dequeueReusableCell(withIdentifier: self.discountCellId, for: indexPath) as! DiscountCell
        cell.textLabel?.text = discount.retailerName
        cell.detailTextLabel?.text = discount.matchedFirstName + " " + discount.matchedLastName
        cell.profileImageView.image = UIImage.gif(name: "imageLoading")!
        grabImageWithCache(discount.retailerImageLink) { (profilePic) in
            cell.profileImageView.image = profilePic
        }
        //Adding second Image
        cell.matchImageView.image = UIImage.gif(name: "imageLoading")!
        grabImageWithCache(discount.matchedProfileImage) { (matchProfilepic) in
            cell.matchImageView.image = matchProfilepic
        }
        //declare no selection style for cell (avoid gray highlight)
        cell.selectionStyle = .none
        //format the cell's curvature
        cell.layer.cornerRadius = 38
        return cell
    }

**I am curious as to why the height of the cells seem to change when the image is cached, because the height is set to a constant number, so I have no idea why it is changing. When I print the height of the cells it says it is 85 both times as well...

UITableViewCell Class:

class DiscountCell: UITableViewCell {

    override func layoutSubviews() {
        super.layoutSubviews()
        //vertical spacing between cells
        let padding = UIEdgeInsets(top: 0, left: 0, bottom: 7, right: 0)
        bounds = bounds.inset(by: padding)
        
        textLabel?.frame = CGRect(x: 120, y: textLabel!.frame.origin.y-10, width: textLabel!.frame.width, height: textLabel!.frame.height)
        detailTextLabel?.frame = CGRect(x: 120, y: detailTextLabel!.frame.origin.y, width: detailTextLabel!.frame.width, height: detailTextLabel!.frame.height)
        //spacing between cells
        let margins = UIEdgeInsets(top: 0, left: 0, bottom: 85, right: 0)
        contentView.frame = contentView.frame.inset(by: margins)

    }
    
    //sets a placeholder imageview
    let profileImageView: UIImageView = {
        let imageView = UIImageView ()
        imageView.image = UIImage(named: "failed")
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.layer.cornerRadius = 35
        imageView.layer.masksToBounds = true
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.layer.borderWidth = 2
        imageView.layer.borderColor = #colorLiteral(red: 0.9725490196, green: 0.9725490196, blue: 0.9725490196, alpha: 1)
        return imageView
    }()
    //PlaceHolder imageview for match
    let matchImageView:UIImageView = {
        let imageView = UIImageView ()
        imageView.image = UIImage(named: "failed")
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.layer.cornerRadius = 35
        imageView.layer.masksToBounds = true
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        return imageView
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
        
        addSubview(matchImageView)
        addSubview(profileImageView)
        //Setting Profile Pic anchors
        profileImageView.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 5).isActive = true
        profileImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
        profileImageView.widthAnchor.constraint(equalToConstant: 70).isActive = true
        profileImageView.heightAnchor.constraint(equalToConstant: 70).isActive = true
        //Setting Match Anchors
        matchImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
        matchImageView.widthAnchor.constraint(equalToConstant: 70).isActive = true
        matchImageView.heightAnchor.constraint(equalToConstant: 70).isActive = true
        matchImageView.leftAnchor.constraint(equalTo: profileImageView.leftAnchor,constant: 35).isActive = true
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

I believe I have narrowed in on the issue, I noticed that the spacing between the cells was inconsistent both times, so I think the issue is with these lines of code

        //vertical spacing between cells
        let padding = UIEdgeInsets(top: 0, left: 0, bottom: 7, right: 0)
        bounds = bounds.inset(by: padding)

I am not sure what to do differently, as I watched tutorials saying to change the contentviews insets but when I do, it has no effect, and I see things saying iOS 11 changed the way you do this, but haven't been able to find how to actually go about fixing this.


Solution

  • Couple notes...

    Not a good idea to modify a cell's bounds or its contentView. UIKit uses those for a lot of things (such as adjusting contents when implementing table editing).

    Cell subviews should be added to the cell's contentView, not to the cell itself. Again, due to how table views rely on that hierarchy.

    You can use UIView and UIImageView subclasses to handle the "rounding" for you. For example:

    class RoundImageView: UIImageView {
        override func layoutSubviews() {
            layer.cornerRadius = bounds.size.height * 0.5
        }
    }
    class RoundEndView: UIView {
        override func layoutSubviews() {
            layer.cornerRadius = bounds.size.height * 0.5
        }
    }
    

    Here is a complete implementation:

    class RoundImageView: UIImageView {
        override func layoutSubviews() {
            layer.cornerRadius = bounds.size.height * 0.5
        }
    }
    class RoundEndView: UIView {
        override func layoutSubviews() {
            layer.cornerRadius = bounds.size.height * 0.5
        }
    }
    
    class DiscountCell: UITableViewCell {
        
        // "Holder" view... contains all other UI elements
        // use RoundEndView so it handles the corner radius by itself
        let holderView: RoundEndView = {
            let v = RoundEndView()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = .white
            return v
        }()
        
        //sets a placeholder imageview
        // use RoundImageView so it handles the corner radius by itself
        let profileImageView: RoundImageView = {
            let imageView = RoundImageView()
            imageView.image = UIImage(named: "failed")
            imageView.translatesAutoresizingMaskIntoConstraints = false
            imageView.layer.masksToBounds = true
            imageView.contentMode = .scaleAspectFill
            imageView.clipsToBounds = true
            imageView.layer.borderWidth = 2
            imageView.layer.borderColor = #colorLiteral(red: 0.9725490196, green: 0.9725490196, blue: 0.9725490196, alpha: 1)
            return imageView
        }()
        
        //PlaceHolder imageview for match
        // use RoundImageView so it handles the corner radius by itself
        let matchImageView: RoundImageView = {
            let imageView = RoundImageView()
            imageView.image = UIImage(named: "failed")
            imageView.translatesAutoresizingMaskIntoConstraints = false
            imageView.layer.masksToBounds = true
            imageView.contentMode = .scaleAspectFill
            imageView.clipsToBounds = true
            return imageView
        }()
        
        let mainLabel: UILabel = {
            let v = UILabel()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.font = UIFont.systemFont(ofSize: 17.0, weight: .regular)
            return v
        }()
        
        let subLabel: UILabel = {
            let v = UILabel()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.font = UIFont.systemFont(ofSize: 14.0, weight: .regular)
            return v
        }()
        
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
            commonInit()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        
        func commonInit() -> Void {
            
            backgroundColor = .clear
            
            contentView.addSubview(holderView)
            
            holderView.addSubview(matchImageView)
            holderView.addSubview(profileImageView)
            holderView.addSubview(mainLabel)
            holderView.addSubview(subLabel)
            
            NSLayoutConstraint.activate([
    
                // holder view constraints Top 5 pts from contentView Top
                holderView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5.0),
                // Leading 12 pts from contentView Leading
                holderView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12.0),
                // Trailing 5 pts from contentView Trailing
                holderView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5.0),
                // 5 pts from contentView Bottom (use lessThanOrEqualTo to avoid auto-layout warnings)
                holderView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -5.0),
    
                //Setting Profile Pic anchors - Top and Leading 5 pts from Top and Leading of Holder view
                profileImageView.topAnchor.constraint(equalTo: holderView.topAnchor, constant: 5),
                profileImageView.leadingAnchor.constraint(equalTo: holderView.leadingAnchor, constant: 5),
                // Botom 5 pts from contentView Bottom - this will determine the height of the Holder view
                profileImageView.bottomAnchor.constraint(equalTo: holderView.bottomAnchor, constant: -5),
                // width 70 pts, height equal to width (keeps it "round")
                profileImageView.widthAnchor.constraint(equalToConstant: 70.0),
                profileImageView.heightAnchor.constraint(equalTo: profileImageView.widthAnchor),
            
                //Setting Match Pic Anchors - 35 pts from Profile Leading, centerY to Profile
                matchImageView.leadingAnchor.constraint(equalTo: profileImageView.leadingAnchor, constant: 35),
                matchImageView.centerYAnchor.constraint(equalTo: profileImageView.centerYAnchor, constant: 0.0),
                // same width and height as Profile
                matchImageView.widthAnchor.constraint(equalTo: profileImageView.widthAnchor),
                matchImageView.heightAnchor.constraint(equalTo: profileImageView.heightAnchor),
                
                //Setting Main Label Anchors - Top equal to Top of Match image
                mainLabel.topAnchor.constraint(equalTo: matchImageView.topAnchor, constant: 0.0),
                // Leading 12 pts from Match image Trailing
                mainLabel.leadingAnchor.constraint(equalTo: matchImageView.trailingAnchor, constant: 12.0),
                // prevent Trailing from going past holder view Trailing
                mainLabel.trailingAnchor.constraint(equalTo: holderView.trailingAnchor, constant: -16.0),
                
                //Setting Sub Label Anchors - Top 8 pts from Main label Bottom
                subLabel.topAnchor.constraint(equalTo: mainLabel.bottomAnchor, constant: 8.0),
                // Leading and Trailing equal to Main label Leading / Trailing
                subLabel.leadingAnchor.constraint(equalTo: mainLabel.leadingAnchor, constant: 0.0),
                subLabel.trailingAnchor.constraint(equalTo: mainLabel.trailingAnchor, constant: 0.0),
                
            ])
            
        }
    }
    
    // example Discount object struct
    struct Discount {
        var retailerName: String = ""
        var matchedFirstName: String = ""
        var matchedLastName: String = ""
        var matchedProfileImage: String = ""
        var retailerImageLink: String = ""
    }
    
    class DiscountViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
        
        let discountTableView = UITableView()
        
        let discountCellId = "dcID"
        
        var myData: [Discount] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // a little sample data
            var d = Discount(retailerName: "Cup & Cone", matchedFirstName: "Kara", matchedLastName: "Tomlinson", matchedProfileImage: "pro1", retailerImageLink: "")
            myData.append(d)
            d = Discount(retailerName: "Cup & Cone", matchedFirstName: "Sophie", matchedLastName: "Turner", matchedProfileImage: "pro2", retailerImageLink: "")
            myData.append(d)
            d = Discount(retailerName: "Another Retailer", matchedFirstName: "WanaB3", matchedLastName: "Nerd", matchedProfileImage: "pro3", retailerImageLink: "")
            myData.append(d)
    
            
            discountTableView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(discountTableView)
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                discountTableView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                discountTableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                discountTableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                discountTableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            ])
            
            view.backgroundColor = UIColor(red: 1.0, green: 0.9, blue: 0.7, alpha: 1.0)
            discountTableView.backgroundColor = UIColor(red: 0.66, green: 0.66, blue: 0.9, alpha: 1.0)
            discountTableView.separatorStyle = .none
    
            discountTableView.delegate = self
            discountTableView.dataSource = self
            
            discountTableView.register(DiscountCell.self, forCellReuseIdentifier: discountCellId)
            
        }
        
        func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return myData.count
        }
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            //Indexing part I left out dw about this
            
            let discount = myData[indexPath.row]
            
            let cell = discountTableView.dequeueReusableCell(withIdentifier: self.discountCellId, for: indexPath) as! DiscountCell
            
            cell.mainLabel.text = discount.retailerName
            cell.subLabel.text = discount.matchedFirstName + " " + discount.matchedLastName
    
            // add first image
            if let img = UIImage(named: "imageLoading") {
                cell.profileImageView.image = img
            }
            // use your custom image funcs
            //cell.profileImageView.image = UIImage.gif(name: "imageLoading")!
            //grabImageWithCache(discount.retailerImageLink) { (profilePic) in
            //  cell.profileImageView.image = profilePic
            //}
    
            //Adding second Image
            if let img = UIImage(named: discount.matchedProfileImage) {
                cell.matchImageView.image = img
            }
            // use your custom image funcs
            //cell.matchImageView.image = UIImage.gif(name: "imageLoading")!
            //grabImageWithCache(discount.matchedProfileImage) { (matchProfilepic) in
            //  cell.matchImageView.image = matchProfilepic
            //}
            
            //declare no selection style for cell (avoid gray highlight)
            cell.selectionStyle = .none
    
            return cell
        }
        
    }
    

    Results:

    enter image description here

    enter image description here