0

I am trying programmatically create a layer with transparent text. Everything I try doesn't seem to work. My end goal is to create an inner shadow on text.

Instead of a circle as in the code below I want text.

let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
view.backgroundColor = .white

// MASK
let blackSquare = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
blackSquare.backgroundColor = .black
view.addSubview(blackSquare)

let maskLayer = CAShapeLayer()

// Create a path with the rectangle in it.
let path = CGMutablePath()
path.addArc(center: CGPoint(x: 100, y: 100), radius: 50, startAngle: 0.0, endAngle: 2.0 * .pi, clockwise: false)
path.addRect(CGRect(x: 0, y: 0, width: blackSquare.frame.width, height: blackSquare.frame.height))
maskLayer.backgroundColor = UIColor.black.cgColor

maskLayer.path = path;
maskLayer.fillRule = kCAFillRuleEvenOdd

blackSquare.layer.mask = maskLayer

maskLayer.masksToBounds = false
maskLayer.shadowRadius = 4
maskLayer.shadowOpacity = 0.5
maskLayer.shadowOffset = CGSize(width: 5, height: 5)

view.addSubview(blackSquare)

I'm also able to use text as a mask but I'm unable to invert the mask. Any help is appreciated.

EDIT:

I figured it out based on Rob's answer as suggested by Josh. Here's my playground code.

import Foundation
import UIKit
import PlaygroundSupport

// view
let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
view.backgroundColor = .black

// button
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
button.setTitle("120", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = UIFont(name: "AvenirNextCondensed-UltraLight", size: 200)

view.addSubview(button)

addInnerShadow(button: button)

func addInnerShadow(button: UIButton) {

// text
let text = button.titleLabel!.text!

// get context
UIGraphicsBeginImageContextWithOptions(button.bounds.size, true, 0)
let context = UIGraphicsGetCurrentContext()

context?.scaleBy(x: 1, y: -1)
context?.translateBy(x: 0, y: -button.bounds.size.height)

let font = button.titleLabel!.font!

// draw the text
let attributes = [
    NSAttributedStringKey.font: font,
    NSAttributedStringKey.foregroundColor: UIColor.white
]

let size = text.size(withAttributes: attributes)
let point = CGPoint(x: (button.bounds.size.width - size.width) / 2.0, y: (button.bounds.size.height - size.height) / 2.0)
text.draw(at: point, withAttributes: attributes)

// capture the image and end context
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

// create image mask
let cgimage = image?.cgImage!
let bytesPerRow = cgimage?.bytesPerRow
let dataProvider = cgimage?.dataProvider!
let bitsPerPixel = cgimage?.bitsPerPixel
let width = cgimage?.width
let height = cgimage?.height
let bitsPerComponent = cgimage?.bitsPerComponent
let mask = CGImage(maskWidth: width!, height: height!, bitsPerComponent: bitsPerComponent!, bitsPerPixel: bitsPerPixel!, bytesPerRow: bytesPerRow!, provider: dataProvider!, decode: nil, shouldInterpolate: false)

// create background
UIGraphicsBeginImageContextWithOptions(button.bounds.size, false, 0)
UIGraphicsGetCurrentContext()!.clip(to: button.bounds, mask: mask!)
view.backgroundColor!.setFill()
UIBezierPath(rect: button.bounds).fill()
let background = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

let backgroundView = UIImageView(image: background)

// add shadows
backgroundView.layer.shadowOffset = CGSize(width: 2, height: 2)
backgroundView.layer.shadowRadius = 2
backgroundView.layer.shadowOpacity = 0.75

button.addSubview(backgroundView)

}

PlaygroundPage.current.liveView = view
2
  • Should we close as duplicate? Commented Mar 3, 2018 at 9:25
  • The APIs and syntax have changed quite significantly since the previous answer. You decide. Commented Mar 3, 2018 at 20:24

2 Answers 2

1

Whilst not exactly the same, please refer to the answer provided here by Rob who answered a similar question:

How do I style a button to have transparent text?

This should get you started at the very least...

Sign up to request clarification or add additional context in comments.

1 Comment

Rob's answer definitely helped with a number of changes.
0

Stumbled on a possible solution that I've updated to proper syntax:

func mask(withRect rect: CGRect, inverse: Bool = false) {
    let path = UIBezierPath(rect: rect)
    let maskLayer = CAShapeLayer()

    if inverse {
        path.append(UIBezierPath(rect: self.view.bounds))
        maskLayer.fillRule = kCAFillRuleEvenOdd
    }

    maskLayer.path = path.cgPath

    self.view.layer.mask = maskLayer
}

You'll obviously need to pick parts out to see if it works for you.

2 Comments

I'll be honest, I'm not a UI guy so let me know how that goes.
Seems that the fillRule doesn't work with text. I found a solution based of Rob's post as per Josh's recommendation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.