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:

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:

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.