0
votes

I'm developing an app which shows an avatar picture and some profile info inside a tableViewCell. The same avatar picture is set to the cell background, and then I have another UIView above it with a gradient, which is created using CAGradientLayer.

The problem is, when my cell is displayed, the gradient does not have the correct frame size (which should be the size of the cell) but rather leaves an empty space to the right. When I scroll the tableView, the gradient resizes correctly.

Here's how I created the gradient:

func awakeFromNib() {
    let startColor = UIColor.gradientOverlay.cgColor //Defined elsewhere
    let endColor = UIColor.black.cgColor

    gradientLayer.frame = gradientView.frame
    gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
    gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
    gradientLayer.colors = [startColor, endColor]
    self.gradientView.layer.addSublayer(gradientLayer)
}

Also, I have overriden the layoutSubviews method like this:

override func layoutSubviews() {
    super.layoutSubviews()

    gradientLayer.frame = gradientView.frame
    self.layoutIfNeeded()
}

The screen looks like this when the view is rendered (I've removed sensitive info):

tableViewCell

Notice the color suffers a change right after the labels, on the right. The last color column is only the border of the simulator screen.

Do you guys happen to know how to solve this?

EDIT: Corrected typo on title.

2
Not sure if it'll help, but it might be worth trying a call to self.layer.layoutIfNeeded() or self.layer.layoutSublayers() at the end of layoutSubviews() to adapt the layer geometry to any revised view geometry. - flanker

2 Answers

2
votes

Gradient is natively using CAGradientLayer witch is a CALayer not an UIView. So view layouts are not affecting it.

A custom class

You can use this class I wrote:

class GradientView: UIView {

    var colors: [UIColor]? { didSet { syncColors() } }

    var direction: GradientDirection = .topToBottom {
        didSet {
            layer.startPoint = direction.startPoint
            layer.endPoint = direction.endPoint
        }
    }

    enum GradientDirection {
        case rightToLeft
        case leftToRight
        case bottomToTop
        case topToBottom
        case custom(startPoint: CGPoint,endPoint: CGPoint)

        var startPoint: CGPoint {
            switch self {
            case .rightToLeft: return CGPoint(x: 1, y: 0)
            case .leftToRight: return .zero
            case .bottomToTop: return CGPoint(x: 0, y: 1)
            case .topToBottom: return .zero
            case .custom(let startPoint, _): return startPoint
            }
        }

        var endPoint: CGPoint {
            switch self {
            case .rightToLeft: return .zero
            case .leftToRight: return CGPoint(x: 1, y: 0)
            case .bottomToTop: return .zero
            case .topToBottom: return CGPoint(x: 0, y: 1)
            case .custom(_, let endPoint): return endPoint
            }
        }

        static var interfaceDirection: GradientDirection {
            switch UIApplication.shared.userInterfaceLayoutDirection {
            case .leftToRight: return .leftToRight
            case .rightToLeft: return .rightToLeft
            }
        }
    }

    override class var layerClass: AnyClass { return CAGradientLayer.self }

    override var layer: CAGradientLayer { return super.layer as! CAGradientLayer }

    override func layoutSubviews() { syncColors() }

    private func syncColors() { layer.colors = colors?.map() { $0.cgColor } }
}

Simplifier class

With the help of this simple class:

enum GradientDirection {
    case rightToLeft
    case leftToRight
    case bottomToTop
    case topToBottom
    case custom(startPoint: CGPoint,endPoint: CGPoint)

    var startPoint: CGPoint {
        switch self {
        case .rightToLeft: return CGPoint(x: 1, y: 0)
        case .leftToRight: return .zero
        case .bottomToTop: return CGPoint(x: 0, y: 1)
        case .topToBottom: return .zero
        case .custom(let startPoint, _): return startPoint
        }
    }

    var endPoint: CGPoint {
        switch self {
        case .rightToLeft: return .zero
        case .leftToRight: return CGPoint(x: 1, y: 0)
        case .bottomToTop: return .zero
        case .topToBottom: return CGPoint(x: 0, y: 1)
        case .custom(_, let endPoint): return endPoint
        }
    }

    static var interfaceDirection: GradientDirection {
        switch UIApplication.shared.userInterfaceLayoutDirection {
        case .leftToRight: return .leftToRight
        case .rightToLeft: return .rightToLeft
        }
    }
}

So then you have a normal view that reacts to any layout notifications from the super view.

Usage

Just add subview and set the class to the GradientView and layout with autolayout or any other layout you want.

or with code:

    let gradientView: GradientView = {
        let gradientView = GradientView(frame: self.frame)
        gradientView.colors = [
            UIColor.black.withAlphaComponent(1),
            UIColor.black.withAlphaComponent(0)
        ]
        gradientView.direction = .bottomToTop

        return gradientView
    }()

Note that you need to remove the original backgroundColor of the view if you want it to have some transparency.

-1
votes

I've found a workaround for it. As the problem lied in the layer's width, I initialized its width using UIScreen.main.bounds.width.