For whatever reason, I can't get this to work with a CATextLayer. I suspect it's totally obvious, and I can't see the forest for the trees, but what I need to do, is use a CATextLayer to mask a "hole" into a CAGradientLayer (so the effect is a gradient, with text "cut out" of it).
I have this working fine, the other way, but I am coming up snake eyes, trying to mask the text from the gradient.
Here's the code that I'm using (It's a UIButton class, and this is the layoutSubviews() override):
override func layoutSubviews() {
super.layoutSubviews()
layer.borderColor = UIColor.clear.cgColor
if let text = titleLabel?.text,
var dynFont = titleLabel?.font {
let minimumFontSizeInPoints = (dynFont.pointSize * 0.5)
let scalingStep = 0.025
while dynFont.pointSize >= minimumFontSizeInPoints {
let calcString = NSAttributedString(string: text, attributes: [.font: dynFont])
let cropRect = calcString.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
if bounds.size.width >= cropRect.size.width {
break
}
guard let tempDynFont = UIFont(name: dynFont.fontName, size: dynFont.pointSize - (dynFont.pointSize * scalingStep)) else { break }
dynFont = tempDynFont
}
titleLabel?.font = dynFont
}
if let titleLabel = titleLabel,
let font = titleLabel.font,
let text = titleLabel.text {
let textLayer = CATextLayer()
textLayer.frame = titleLabel.frame
textLayer.rasterizationScale = UIScreen.main.scale
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = .left
textLayer.fontSize = font.pointSize
textLayer.font = font
textLayer.isWrapped = true
textLayer.truncationMode = .none
textLayer.string = text
self.textLayer = textLayer
titleLabel.textColor = .clear
let gradient = CAGradientLayer()
gradient.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
gradient.startPoint = CGPoint(x: 0.5, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
var layerFrame = textLayer.frame
if !reversed {
if 0 < layer.borderWidth {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
outlineLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
outlineLayer.lineWidth = layer.borderWidth
outlineLayer.strokeColor = UIColor.white.cgColor
outlineLayer.fillColor = UIColor.clear.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CIAdditionCompositing") {
textLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
} else {
layer.mask = textLayer
}
} else {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
textLayer.foregroundColor = UIColor.white.cgColor
outlineLayer.backgroundColor = UIColor.white.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
outlineLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
}
gradient.frame = layerFrame
layer.addSublayer(gradient)
}
}
The problem is in this part of the code:
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
textLayer.foregroundColor = UIColor.white.cgColor
outlineLayer.backgroundColor = UIColor.white.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
outlineLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
The !reversed part works fine. I get a gradient masked through text, and, possibly, an outline.
What I need, is to get the gradient to fill the button, with the text "cut out," so the background shows through.
Like I said, this seems deeply obvious, and I seem to have a block.
Are there any suggestions as to what I might be screwing up?
I could probably break this into a playground, but maybe this is enough.
Thanks!
UPDATE:
Here it is as a playground:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
@IBDesignable
class Rcvrr_GradientTextMaskButton: UIButton {
/* ################################################################## */
/**
This contains our text
*/
var textLayer: CALayer?
/* ################################################################## */
/**
The starting color for the gradient.
*/
@IBInspectable var gradientStartColor: UIColor = .white
/* ################################################################## */
/**
The ending color.
*/
@IBInspectable var gradientEndColor: UIColor = .black
/* ################################################################## */
/**
The angle of the gradient. 0 (default) is top-to-bottom.
*/
@IBInspectable var gradientAngleInDegrees: CGFloat = 0
/* ################################################################## */
/**
If true, then the label is reversed, so the background is "cut out" of the foreground.
*/
@IBInspectable var reversed: Bool = false
}
/* ###################################################################################################################################### */
// MARK: Base Class Overrides
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
/* ################################################################## */
/**
If the button is "standard" (the text is filled with the gradient), then this method takes care of that.
*/
override func layoutSubviews() {
super.layoutSubviews()
layer.borderColor = UIColor.clear.cgColor
if let text = titleLabel?.text,
var dynFont = titleLabel?.font {
let minimumFontSizeInPoints = (dynFont.pointSize * 0.5)
let scalingStep = 0.025
while dynFont.pointSize >= minimumFontSizeInPoints {
let calcString = NSAttributedString(string: text, attributes: [.font: dynFont])
let cropRect = calcString.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
if bounds.size.width >= cropRect.size.width {
break
}
guard let tempDynFont = UIFont(name: dynFont.fontName, size: dynFont.pointSize - (dynFont.pointSize * scalingStep)) else { break }
dynFont = tempDynFont
}
titleLabel?.font = dynFont
}
if let titleLabel = titleLabel,
let font = titleLabel.font,
let text = titleLabel.text {
let textLayer = CATextLayer()
textLayer.frame = titleLabel.frame
textLayer.rasterizationScale = UIScreen.main.scale
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = .left
textLayer.fontSize = font.pointSize
textLayer.font = font
textLayer.isWrapped = true
textLayer.truncationMode = .none
textLayer.string = text
self.textLayer = textLayer
titleLabel.textColor = .clear
let gradient = CAGradientLayer()
gradient.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
gradient.startPoint = CGPoint(x: 0.5, y: 0)
gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
var layerFrame = textLayer.frame
if !reversed {
if 0 < layer.borderWidth {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
outlineLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
outlineLayer.lineWidth = layer.borderWidth
outlineLayer.strokeColor = UIColor.white.cgColor
outlineLayer.fillColor = UIColor.clear.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CIAdditionCompositing") {
textLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
} else {
layer.mask = textLayer
}
} else {
let outlineLayer = CAShapeLayer()
outlineLayer.frame = bounds
textLayer.foregroundColor = UIColor.white.cgColor
outlineLayer.backgroundColor = UIColor.white.cgColor
layerFrame = bounds
textLayer.masksToBounds = false
if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
outlineLayer.compositingFilter = compositingFilter
outlineLayer.addSublayer(textLayer)
}
layer.mask = outlineLayer
}
gradient.frame = layerFrame
layer.addSublayer(gradient)
}
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .yellow
let button = Rcvrr_GradientTextMaskButton()
button.frame = CGRect(x: 10, y: 200, width: 300, height: 50)
button.setTitle("HI", for: .normal)
button.gradientStartColor = .green
button.gradientEndColor = .blue
button.reversed = true
view.addSubview(button)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()