0

Note, that it must work with different number of lines in UILabel - 1,2,3 etc. I've already found solution for 1 line label, where you mask UILabel's layer with CAGradientLayer, but it doesn't work for multiline labels, as it masks the whole layer and fades out all lines.

I tried to make another CALayer with position calculated to be in the position of last line with desired width and used CAGradientLayer as mask and add this layer as sublayer of UILabel, it worked for static objects, but i use this UILabel in UITableViewCell and when it's tapped - it changes color to gray and i can see my layer, because it uses background color of UILabel when view layout its subviews, and also something wrong with x position calculation:

extension UILabel {
    func fadeOutLastLineEnd() { //Call in layoutSubviews
        guard bounds.width > 0 else { return }

        lineBreakMode = .byCharWrapping
        let tmpLayer = CALayer()
        let gradientWidth: CGFloat = 32
        let numberOfLines = CGFloat(numberOfLines)
        tmpLayer.backgroundColor = UIColor.white.cgColor
        tmpLayer.frame = CGRect(x: layer.frame.width - gradientWidth,
                                y: layer.frame.height / numberOfLines,
                                width: gradientWidth,
                                height: layer.frame.height / numberOfLines)
        
        let tmpGrLayer = CAGradientLayer()

        tmpGrLayer.colors     = [UIColor.white.cgColor, UIColor.clear.cgColor]
        tmpGrLayer.startPoint = CGPoint(x: 1, y: 0)
        tmpGrLayer.endPoint   = CGPoint(x: 0, y: 0)
        tmpGrLayer.frame = tmpLayer.bounds
        
        tmpLayer.mask = tmpGrLayer
        layer.addSublayer(tmpLayer)
    }
}

So, i need UILabel:

  • which can be multiline
  • end of last line needs to be faded out (gradient?)
  • works in UITableViewCell, when the whole object changes color
burnsi
  • 6,194
  • 13
  • 17
  • 27
N. Rysin
  • 3
  • 1
  • You need to clarify... do you want the bottom-right corner to fade out? Or, do you want the *end of the text* to "fade out"? Like this: https://i.stack.imgur.com/4SgNq.png – DonMag Jun 18 '22 at 12:04
  • Hi, i need behavior of truncating bottom-right corner to fade out instead of default "text...", NOT the end of text, because text can be ended at the middle of uilabel length, as on your link You can see desired behavior on last image – N. Rysin Jun 20 '22 at 08:07

2 Answers2

1

You should create a CATextLayer with the same text properties as your UILabel.

Fill it with the end of your text you wish to fade.

Then calculate the position of this text segment in your UILabel.

Finally overlay the two.

Here are some aspect explained.

meaning-matters
  • 21,929
  • 10
  • 82
  • 142
  • I checked your answer and link, and didn't get it, it's not like gradient fading, it's overlaping 2 texts as you mention i should use same properties, so both use same color. – N. Rysin Jun 17 '22 at 12:00
1

There are various ways to do this -- here's one approach.

We can mask a view by setting the layer.mask. The opaque areas of the mask will show-through, and the transparent areas will not.

So, what we need is a custom layer subclass that will look like this:

enter image description here

This is an example that I'll call InvertedGradientLayer:

class InvertedGradientLayer: CALayer {
    
    public var lineHeight: CGFloat = 0
    public var gradWidth: CGFloat = 0
    
    override func draw(in inContext: CGContext) {
        
        // fill all but the bottom "line height" with opaque color
        inContext.setFillColor(UIColor.gray.cgColor)
        var r = self.bounds
        r.size.height -= lineHeight
        inContext.fill(r)

        // can be any color, we're going from Opaque to Clear
        let colors = [UIColor.gray.cgColor, UIColor.gray.withAlphaComponent(0.0).cgColor]
        
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        
        let colorLocations: [CGFloat] = [0.0, 1.0]
        
        let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations)!
        
        // start the gradient "grad width" from right edge
        let startPoint = CGPoint(x: bounds.maxX - gradWidth, y: 0.5)
        // end the gradient at the right edge, but
        // probably want to leave the farthest-right 1 or 2 points
        //  completely transparent
        let endPoint = CGPoint(x: bounds.maxX - 2.0, y: 0.5)

        // gradient rect starts at the bottom of the opaque rect
        r.origin.y = r.size.height - 1
        // gradient rect height can extend below the bounds, becuase it will be clipped
        r.size.height = bounds.height
        inContext.addRect(r)
        inContext.clip()
        inContext.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsBeforeStartLocation)

    }
    
}

Next, we'll make a UILabel subclass that implements that InvertedGradientLayer as a layer mask:

class CornerFadeLabel: UILabel {
    let ivgLayer = InvertedGradientLayer()
    override func layoutSubviews() {
        super.layoutSubviews()
        guard let f = self.font, let t = self.text else { return }
        // we only want to fade-out the last line if
        //  it would be clipped
        let constraintRect = CGSize(width: bounds.width, height: .greatestFiniteMagnitude)
        let boundingBox = t.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font : f], context: nil)
        if boundingBox.height <= bounds.height {
            layer.mask = nil
            return
        }
        layer.mask = ivgLayer
        ivgLayer.lineHeight = f.lineHeight
        ivgLayer.gradWidth = 60.0
        ivgLayer.frame = bounds
        ivgLayer.setNeedsDisplay()
    }
}

and here is a sample view controller showing it in use:

class FadeVC: UIViewController {
    
    let wordWrapFadeLabel: CornerFadeLabel = {
        let v = CornerFadeLabel()
        v.numberOfLines = 1
        v.lineBreakMode = .byWordWrapping
        return v
    }()
    
    let charWrapFadeLabel: CornerFadeLabel = {
        let v = CornerFadeLabel()
        v.numberOfLines = 1
        v.lineBreakMode = .byCharWrapping
        return v
    }()
    
    let normalLabel: UILabel = {
        let v = UILabel()
        v.numberOfLines = 1
        return v
    }()
    
    let numLinesLabel: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        return v
    }()
    
    var numLines: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        let sampleText = "This is some example text that will wrap onto multiple lines and fade-out the bottom-right corner instead of truncating or clipping a last line."
        wordWrapFadeLabel.text = sampleText
        charWrapFadeLabel.text = sampleText
        normalLabel.text = sampleText
        
        let stack: UIStackView = {
            let v = UIStackView()
            v.axis = .vertical
            v.spacing = 8
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        let bStack: UIStackView = {
            let v = UIStackView()
            v.axis = .horizontal
            v.spacing = 8
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        let btnUP: UIButton = {
            let v = UIButton()
            let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
            let img = UIImage(systemName: "chevron.up.circle.fill", withConfiguration: cfg)
            v.setImage(img, for: [])
            v.tintColor = .systemGreen
            v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
            v.addTarget(self, action: #selector(btnUpTapped), for: .touchUpInside)
            return v
        }()
        
        let btnDown: UIButton = {
            let v = UIButton()
            let cfg = UIImage.SymbolConfiguration(pointSize: 28.0, weight: .bold, scale: .large)
            let img = UIImage(systemName: "chevron.down.circle.fill", withConfiguration: cfg)
            v.setImage(img, for: [])
            v.tintColor = .systemGreen
            v.widthAnchor.constraint(equalTo: v.heightAnchor).isActive = true
            v.addTarget(self, action: #selector(btnDownTapped), for: .touchUpInside)
            return v
        }()
        
        bStack.addArrangedSubview(btnUP)
        bStack.addArrangedSubview(numLinesLabel)
        bStack.addArrangedSubview(btnDown)
        
        let v1 = UILabel()
        v1.text = "Word-wrapping"
        v1.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        let v2 = UILabel()
        v2.text = "Character-wrapping"
        v2.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        let v3 = UILabel()
        v3.text = "Normal Label (Truncate Tail)"
        v3.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        
        stack.addArrangedSubview(bStack)
        stack.addArrangedSubview(v1)
        stack.addArrangedSubview(wordWrapFadeLabel)
        stack.addArrangedSubview(v2)
        stack.addArrangedSubview(charWrapFadeLabel)
        stack.addArrangedSubview(v3)
        stack.addArrangedSubview(normalLabel)

        stack.setCustomSpacing(20, after: bStack)
        stack.setCustomSpacing(20, after: wordWrapFadeLabel)
        stack.setCustomSpacing(20, after: charWrapFadeLabel)

        view.addSubview(stack)
        
        // dashed border views so we can see the lable frames
        let wordBorderView = DashedView()
        let charBorderView = DashedView()
        let normalBorderView = DashedView()
        wordBorderView.translatesAutoresizingMaskIntoConstraints = false
        charBorderView.translatesAutoresizingMaskIntoConstraints = false
        normalBorderView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(wordBorderView)
        view.addSubview(charBorderView)
        view.addSubview(normalBorderView)

        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            stack.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 60.0),
            stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -60.0),
            
            wordBorderView.topAnchor.constraint(equalTo: wordWrapFadeLabel.topAnchor, constant: 0.0),
            wordBorderView.leadingAnchor.constraint(equalTo: wordWrapFadeLabel.leadingAnchor, constant: 0.0),
            wordBorderView.trailingAnchor.constraint(equalTo: wordWrapFadeLabel.trailingAnchor, constant: 0.0),
            wordBorderView.bottomAnchor.constraint(equalTo: wordWrapFadeLabel.bottomAnchor, constant: 0.0),
            
            charBorderView.topAnchor.constraint(equalTo: charWrapFadeLabel.topAnchor, constant: 0.0),
            charBorderView.leadingAnchor.constraint(equalTo: charWrapFadeLabel.leadingAnchor, constant: 0.0),
            charBorderView.trailingAnchor.constraint(equalTo: charWrapFadeLabel.trailingAnchor, constant: 0.0),
            charBorderView.bottomAnchor.constraint(equalTo: charWrapFadeLabel.bottomAnchor, constant: 0.0),
            
            normalBorderView.topAnchor.constraint(equalTo: normalLabel.topAnchor, constant: 0.0),
            normalBorderView.leadingAnchor.constraint(equalTo: normalLabel.leadingAnchor, constant: 0.0),
            normalBorderView.trailingAnchor.constraint(equalTo: normalLabel.trailingAnchor, constant: 0.0),
            normalBorderView.bottomAnchor.constraint(equalTo: normalLabel.bottomAnchor, constant: 0.0),
            
        ])
        
        // set initial number of lines to 1
        btnUpTapped()
        
    }
    @objc func btnUpTapped() {
        numLines += 1
        numLinesLabel.text = "Num Lines: \(numLines)"
        wordWrapFadeLabel.numberOfLines = numLines
        charWrapFadeLabel.numberOfLines = numLines
        normalLabel.numberOfLines = numLines
    }
    @objc func btnDownTapped() {
        if numLines == 1 { return }
        numLines -= 1
        numLinesLabel.text = "Num Lines: \(numLines)"
        wordWrapFadeLabel.numberOfLines = numLines
        charWrapFadeLabel.numberOfLines = numLines
        normalLabel.numberOfLines = numLines
    }
}

When running, it looks like this:

enter image description here

The red dashed borders are there just so we can see the frames of the labels. Tapping the up/down arrows will increment/decrement the max number of lines to show in each label.

DonMag
  • 69,424
  • 5
  • 50
  • 86