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()
The immediate problem is that you are putting the gradient layer in front of the layer that is being masked. Your masking is working, but you are covering it up! It is not
self.layer
you want to mask if you want to see something happening here; it'sgradient
. Changelayer.mask =
togradient.mask =
everywhere, and you will see an actual visible result.You will then probably realize that your mask itself is faulty, but at least you won't just be looking at the unadulterated gradient wondering where the mask went!