1

I have a UITableView whose cells contain a subview on which I need to perform three things:

  1. change its width constraint at runtime depending on a value inside an object specific to this cell
  2. change its background color depending on that same value
  3. round the top left and bottom left corners of the view but keep the corners on the right hand side as they are (so layer.cornerRadius is not an option)

I use the following code inside my custom UITableViewCell subclass to achieve the rounded corner effect on one side of the view only, which I call from tableView:cellForRowAt::

func roundLeadingEdgesOfBar() {
    let roundedLayer = CAShapeLayer()
    roundedLayer.bounds = viewInQuestion.frame
    roundedLayer.position = viewInQuestion.center
    roundedLayer.path = UIBezierPath(roundedRect: viewInQuestion.bounds,
                                     byRoundingCorners: [.topLeft, .bottomLeft],
                                     cornerRadii: CGSize(width: 2, height: 2)).cgPath
    viewInQuestion.layer.mask = roundedLayer
    print("frame: \(viewInQuestion.frame)")
}

However what I see when I run this code is an effect like this: enter image description here

The print statement in the code above produces the following output, indicating that viewInQuestion has the same frame every time when clearly on the screen it hasn't:

frame: (175.0, 139.5, 200.0, 5.0)
frame: (175.0, 139.5, 200.0, 5.0)
frame: (175.0, 139.5, 200.0, 5.0)
frame: (175.0, 139.5, 200.0, 5.0)

So I assume the width constraint on the view has not been rendered by the time I call this function. When I scroll the entire table view up until all cells are out of view, and then scroll them back into view, everything looks correct and the printed frames are all different, like I would expect:

frame: (136.5, 79.5, 238.5, 5.0)
frame: (169.5, 79.5, 205.5, 5.0)
frame: (226.0, 79.5, 149.0, 5.0)
frame: (247.5, 79.5, 127.5, 5.0)

I've read several times on SO to execute code that is dependent on constraints having been applied from within layoutSubviews, but that gave me the same result. I even tried calling roundLeadingEdgesOfBar from within tableView:willDisplay:forRowAt:, to no avail.

I also found this response to a similar problem which suggests putting the mask layer code inside drawRect:. This actually fixes 99% of the problem for me (leaving performance issues aside), but there are still corner cases (no pun intended) left for very long table views where I still see the wrong behavior.

My last resort was to call my rounding function via performSelector with a 0.00001 delay, which works in the sense that you see the bug for about a second on screen before it then disappears - still far from ideal behavior, let alone the awful code I had to write for it.

Is there any way to reliably apply the shape layer on the view inside a UITableViewCell using its correct runtime frame?

Richard
  • 479
  • 1
  • 8
  • 19
  • Is the view you are having trouble with a subview of your cell? If so, if you *don't* set the layer mask, is the size/position correct? – DonMag Oct 17 '17 at 12:24
  • @DonMag Yes, it is a subview and no, if I _don't_ set the layer mask, the output frame is the same incorrect one for all cells (but on screen everything is correct) – Richard Oct 17 '17 at 12:38
  • OK - see my answer, and see if that will work for you. – DonMag Oct 17 '17 at 12:44

2 Answers2

2

I think what you should be doing is that,

  1. Set the properties related to your current object in cellForRowAtIndexPath, try setting constraints in the setter of that particular value.
  2. Call setNeedsLayout which will make a future call to layoutIfNeeded -> layoutSubviews
  3. In your layoutSubviews, set the rounded corners.

I hope this will help you.

  • ```setNeedsLayout ``` sets a future call to ```layoutSubviews```, Try calling ```layoutSubviews``` explicitly from the point you're setting the properties. – Muhammad Abdul Subhan Oct 17 '17 at 11:25
  • I already tried calling `layoutSubviews` explicitly (no success), I guess it's because at the point where I'm configuring the cell the constraints aren't rendered, so calling `layoutSubviews` there would be pointless – Richard Oct 17 '17 at 11:28
  • There must be some issue which i'm unable to find out. I guess try moving your code to ```updateConstraints()``` and call ```setNeedsUpdateConstraints() ``` – Muhammad Abdul Subhan Oct 17 '17 at 11:36
2

Instead of calling a function to "round the edges," I suggest creating a UIView subclass and let it handle the rounding.

For example:

class BulletBar: UIView {

    override func layoutSubviews() {

        let roundedLayer = CAShapeLayer()

        roundedLayer.frame = bounds

        roundedLayer.path = UIBezierPath(roundedRect: bounds,
                                         byRoundingCorners: [.topLeft, .bottomLeft],
                                         cornerRadii: CGSize(width: 2, height: 2)).cgPath
        layer.mask = roundedLayer

    }

}

Now, set the class of your "bar" subview in the cell to BulletBar. Use constraints to pin it to the right and bottom and to constrain the height and width. Create an IBOutlet for the width constraint, and then set the barWidthConstraint.constant as desired.

The class itself will handle rounding the corners.

Result:

enter image description here

DonMag
  • 69,424
  • 5
  • 50
  • 86