July 10, 2019

Custom UITableViewCell with Xcode assets image alignment

2 minutes read



Requirement

We have a design requirement to build a tableview with custom cells. Each section is supposed to be like a “box” with a ridiculous shadow on all sides. No problem! - we think to ourselves. But then we realise, that it can get messy pretty easy. There are 4 types of background images and depending on type, we might need to adjust cell height, to compensate for shadow.

Solution

First we need to export background images, for all cell types.

Each of these images needs slicing (So that they would keep corners as they are, and strech the middle part). That can be easily done in the Xcode assets, and later on when using these images in code (or storyboards / interface builder), we don’t need to worry about them - they will strech correctly.

For the actual cell, we will use custom class with .xib. In the .xib file, we put all the items but we will position them from code, using NSLayoutAnchors.

enum CellBackgroundType: Int {
	case top = 0
	case middle = 1
	case bottom = 2
	case single = 3
}

class KazooTableViewCell: UITableViewCell {

    public var cellBgType: CellBackgroundType = .single
	
    @IBOutlet weak var backgroundImageView: UIImageView!
    @IBOutlet weak var iconImageView: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var descriptionLabel: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
        
        backgroundImageView.translatesAutoresizingMaskIntoConstraints = false
        iconImageView.translatesAutoresizingMaskIntoConstraints = false
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
        
        // We set background image equal to contentView frame anchors.
        backgroundImageView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
        backgroundImageView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
        backgroundImageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        backgroundImageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
		
        // Then we position all other elements based on background image view.
        iconImageView.leftAnchor.constraint(equalTo: backgroundImageView.leftAnchor, constant: 10).isActive = true
        iconImageView.topAnchor.constraint(equalTo: backgroundImageView.topAnchor, constant: 10).isActive = true
        iconImageView.widthAnchor.constraint(equalToConstant: 30).isActive = true
        iconImageView.heightAnchor.constraint(equalToConstant: 30).isActive = true
		
        titleLabel.topAnchor.constraint(equalTo: backgroundImageView.topAnchor, constant: 5).isActive = true
        titleLabel.leftAnchor.constraint(equalTo: iconImageView.rightAnchor, constant: 10).isActive = true
        titleLabel.rightAnchor.constraint(equalTo: backgroundImageView.rightAnchor, constant: -10).isActive = true
		
        descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true
        descriptionLabel.leftAnchor.constraint(equalTo: iconImageView.rightAnchor, constant: 10).isActive = true
        descriptionLabel.rightAnchor.constraint(equalTo: backgroundImageView.rightAnchor, constant: -10).isActive = true
        descriptionLabel.bottomAnchor.constraint(equalTo: backgroundImageView.bottomAnchor, constant: -5).isActive = true
    }
    
    // Using this function, we can set apropriate cell type, and change background image.
    func setAsCellType(cellType: CellBackgroundType) {
        switch cellType {
            case .top:
                backgroundImageView.image = UIImage.init(named: "cell_bg_top")
            case .bottom:
                backgroundImageView.image = UIImage.init(named: "cell_bg_bottom")
            case .middle:
                backgroundImageView.image = UIImage.init(named: "cell_bg_middle")
            case .single:
                backgroundImageView.image = UIImage.init(named: "cell_bg_single")
		}
	}
}

This then results into this:

Well, it is not incorrect.. per se.. Background images are correct at correct times, shadows look good, but we can see that first/last/single cell should be higher, to compensate for the shadow. Also, inner content should be left-offset, to appear within the box.

To deal with it, we will use xcode assets alignment.

Basically, for our background images, we can provide top/bottom/left/right alignment values, that would compensate for the shadows. The value is pixels count. (Literarly - in actual image) So, for @2x image, it’s 57 pixels shadow (I counted). For @3x images it’s 86 pixels.

To demonstrate, see this top-cell imageview: (black frame is just to mark Image View frame)

But when we apply alignment top: 57, left: 57, right: 57, bottom: 0, then the actual frame will grow larger with given offset (black frame), but when we will use it in our cell with NSLayoutAnchors, it will use the red frame of image view.

Ok, now, without changing the code, we see such result:

So, what’s left is - just add offset for backgroundImageView, so that we can see shadows! To do it, we just need to add left/right constant offset:

// We use 28.5, because shadow is 57 for @2x. 
// So, it would be 28.5 pixels, to see whole shadow on both sides
backgroundImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: 28.5).isActive = true
backgroundImageView.rightAnchor.constraint(equalTo: rightAnchor, constant: -28.5).isActive = true

And then store top and bottom constraints, to change their constant, based on cell type:

var bgViewBottomAnchorConstraint: NSLayoutConstraint?
var bgViewTopAnchorConstraint: NSLayoutConstraint?
	
bgViewBottomAnchorConstraint = backgroundImageView.bottomAnchor.constraint(equalTo: bottomAnchor)
bgViewBottomAnchorConstraint?.isActive = true
bgViewTopAnchorConstraint = backgroundImageView.topAnchor.constraint(equalTo: topAnchor)
bgViewTopAnchorConstraint?.isActive = true

Lastly, we need to adjust our setAsCellType(cellType: cellBackgroundType) function, so that it would adjust top and bottom constraint constant, to compensate for the shadow, when necessary:

func setAsCellType(cellType: cellBackgroundType) {
    switch cellType {
        case .top:
            self.bgViewTopAnchorConstraint?.constant = 28.5
            self.bgViewBottomAnchorConstraint?.constant = 0
            backgroundImageView.image = UIImage.init(named: "cell_bg_top")
        case .bottom:
            self.bgViewTopAnchorConstraint?.constant = 0
            self.bgViewBottomAnchorConstraint?.constant = -28.5
            backgroundImageView.image = UIImage.init(named: "cell_bg_bottom")
        case .middle:
            self.bgViewTopAnchorConstraint?.constant = 0
            self.bgViewBottomAnchorConstraint?.constant = 0
            backgroundImageView.image = UIImage.init(named: "cell_bg_middle")
        case .single:
            self.bgViewTopAnchorConstraint?.constant = 28.5
            self.bgViewBottomAnchorConstraint?.constant = -28.5
            backgroundImageView.image = UIImage.init(named: "cell_bg_single")
    }
}

And then - without changing any constraints for icon and labels, we get such result:

P.S.

Full code / demo, can be found here