iOS Swift: create a mask from a UILabel - ios

Hi I'm trying to use a label as a mask for a particle emitter layer.
My particles emitter is already set up, but I'm having a problem to get a mask from a label, this is my code that doesn't work so well.
func emitter() {
// define emitter layer as centered w 80% of smallest dimension
let image = emitterImage
let origin = CGPoint(x: view.bounds.midX - view.bounds.width / 2, y: view.bounds.midY - view.bounds.height / 2)
let center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let size = CGSize(width: view.bounds.width, height: view.bounds.height)
let rect = CGRect(origin: origin, size: size)
let emitterLayer = CAEmitterLayer()
emitterLayer.emitterShape = CAEmitterLayerEmitterShape.rectangle
emitterLayer.emitterSize = rect.size
emitterLayer.emitterPosition = center
// define cells
let cell = CAEmitterCell()
cell.birthRate = Float(size.width * size.height / 10)
cell.lifetime = 1
cell.velocity = 10
cell.scale = 0.1
cell.scaleSpeed = -0.1
cell.emissionRange = .pi * 2
cell.contents = image.cgImage
emitterLayer.emitterCells = [cell]
// add the layer
view.layer.addSublayer(emitterLayer)
// mask
let font = UIFont(name: "HelveticaNeue", size: 64)!
var unichars = [UniChar]("Text".utf16)
var glyphs = [CGGlyph](repeating: 0, count: unichars.count)
let gotGlyphs = CTFontGetGlyphsForCharacters(font, &unichars, &glyphs, unichars.count)
if gotGlyphs {
let cgpath = CTFontCreatePathForGlyph(font, glyphs[0], nil)!
let path = UIBezierPath(cgPath: cgpath)
let mask = CAShapeLayer()
mask.frame = CGRect(x: 100, y: 100, width: 200, height: 200)
mask.fillColor = UIColor.clear.cgColor
mask.strokeColor = UIColor.white.cgColor
mask.lineWidth = 10.0
mask.path = path.cgPath
emitterLayer.mask = mask
}
}
Problem 1: I got just the first letter ("T") how could I attach all the characters path in one?
if gotGlyphs {
var paths: [UIBezierPath] = []
for glyph in glyphs {
let cgpath = CTFontCreatePathForGlyph(font, glyph, nil)!
let path = UIBezierPath(cgPath: cgpath)
paths.append(path)
}
In this way I got an array of all chars path, but how can I attach them??
Problem 2: The path is rotated by 180 degrees (why???)

Solved Using CATextLayer to create the mask from a label.
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: 0, y: view.bounds.height / 6 * 1, width: yourLabel.frame.width, height: yourLabel.frame.height)
textLayer.rasterizationScale = UIScreen.main.scale
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = CATextLayerAlignmentMode.center
textLayer.fontSize = 30
textLayer.font = yourLabel.font
textLayer.isWrapped = true
textLayer.truncationMode = CATextLayerTruncationMode.end
textLayer.string = yourLabel.text
emitterLayer.mask = textLayer

Related

How to measure character bounds within a text shown by CATextLayer?

I am fresh to IOS development. I am looking for a way to retrieve detailed text layout information (such as ascent, descent, advance width etc).
In android, I am able to do this through Paint.getTextWidths. Then I am able to draw bounds or do hit test on individual character.
like this:
On IOS, I am using CATextLayer to manage text layers, but I could only find out the layer.frame, which gives me the bounds of the whole text block.
like this:
Any equivalence to easily do this on IOS?
I finally achieved this by using CTLine and CTRun to manually measure the text.
let layer = CATextLayer()
layer.string = self.text
layer.fontSize = CGFloat(self.fontSize)
layer.font = CTFontCreateWithName("Helvetica" as CFString, CGFloat(self.fontSize), nil)
layer.truncationMode = .end
layer.allowsFontSubpixelQuantization = false
layer.contentsScale = UIScreen.main.scale
layer.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: layer.preferredFrameSize())
let attrs = [ NSAttributedString.Key.font: UIFont(name: "Helvetica", size: 20.0)! ]
let nsText = NSAttributedString(string: self.text, attributes: attrs)
let nsLine = CTLineCreateWithAttributedString(nsText)
let runs = CTLineGetGlyphRuns(nsLine) as? Array<CTRun>
// measure text
var widths = Array<CGFloat>()
for run in runs! {
let glyphCount = CTRunGetGlyphCount(run)
var cgSizes = Array(repeating: CGSize(), count: glyphCount)
let _ = CTRunGetAdvances(run, CFRangeMake(0, glyphCount), &cgSizes)
for cgSize in cgSizes {
widths.append(cgSize.width)
}
}
// draw bounding box
let height = layer.frame.height
print("advance widths: \(widths), height: \(height)")
var xOffset = CGFloat()
for width in widths {
let path = UIBezierPath()
path.move(to: CGPoint(x: xOffset, y: 0.0))
path.addLine(to: CGPoint(x: xOffset + width, y: 0.0 ))
path.addLine(to: CGPoint(x: xOffset + width, y: height))
path.addLine(to: CGPoint(x: xOffset, y: height))
path.close()
let boundsLayer = CAShapeLayer()
boundsLayer.path = path.cgPath
boundsLayer.lineWidth = 1
boundsLayer.strokeColor = UIColor.blue.cgColor
boundsLayer.fillColor = CGColor(gray: 0, alpha: 0)
layer.addSublayer(boundsLayer)
xOffset += width
}
layer.borderWidth = 1
layer.borderColor = UIColor.red.cgColor

Mask over the image view Swift 5

I have been trying to implement this answer from this question using Swift 5.2
I only see the blurred image and the mask is not appearing. Can someone point me what I am missing there or how to convert it to Swift 5 from Swift 3?
This is a playground code.
import UIKit
import PlaygroundSupport
let generalFrame = CGRect(x: 0, y: 0, width: 500, height: 500)
let containerView = UIView(frame: generalFrame)
containerView.backgroundColor = UIColor.black;
PlaygroundPage.current.liveView = containerView
let parentView = UIView(frame: generalFrame)
containerView.addSubview(parentView)
let url = URL(string: "https://static.pexels.com/photos/168066/pexels-photo-168066-large.jpeg")
let data = try Data(contentsOf: url!);
let imageView = UIImageView(frame:parentView.bounds)
imageView.image = UIImage(data: data)
imageView.contentMode = .scaleAspectFill
let maskView = UIView(frame:parentView.bounds)
maskView.backgroundColor = UIColor.black
maskView.layer.mask = {() -> CALayer in
var roundedRect = CGRect (
x: 0.0,
y: 0.0,
width: parentView.bounds.size.width * 0.5,
height: parentView.bounds.size.width * 0.5
);
roundedRect.origin.x = parentView.frame.size.width / 2 - roundedRect.size.width / 2;
roundedRect.origin.y = parentView.frame.size.height / 2 - roundedRect.size.height / 2;
let cornerRadius = roundedRect.size.height / 2.0;
let path = UIBezierPath(rect:parentView.bounds)
let croppedPath = UIBezierPath(roundedRect: roundedRect, cornerRadius: cornerRadius)
path.append(croppedPath)
path.usesEvenOddFillRule = true
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath;
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
return maskLayer
}()
let blurView = UIBlurEffect(style: .light)
let effectView = UIVisualEffectView(effect: blurView)
effectView.frame = generalFrame
effectView.mask = maskView
parentView.addSubview(imageView)
parentView.addSubview(effectView)
I managed to get this working. The fix was to apply mask to UIVisualEffectView:
let maskLayer = CAShapeLayer()
effectView.layer.mask = maskLayer
import UIKit
import PlaygroundSupport
let generalFrame = CGRect(x: 0, y: 0, width: 500, height: 500)
let containerView = UIView(frame: generalFrame)
containerView.backgroundColor = UIColor.black;
PlaygroundPage.current.liveView = containerView
let parentView = UIView(frame: generalFrame)
containerView.addSubview(parentView)
let url = URL(string: "https://static.pexels.com/photos/168066/pexels-photo-168066-large.jpeg")
let data = try Data(contentsOf: url!);
let imageView = UIImageView(frame:parentView.bounds)
imageView.image = UIImage(data: data)
imageView.contentMode = .scaleAspectFill
var roundedRect = CGRect (
x: 0.0,
y: 0.0,
width: parentView.bounds.size.width * 0.5,
height: parentView.bounds.size.width * 0.5
);
roundedRect.origin.x = parentView.frame.size.width / 2 - roundedRect.size.width / 2;
roundedRect.origin.y = parentView.frame.size.height / 2 - roundedRect.size.height / 2;
let cornerRadius = roundedRect.size.height / 2.0;
let path = UIBezierPath(rect:parentView.bounds)
let croppedPath = UIBezierPath(roundedRect: roundedRect, cornerRadius: cornerRadius)
path.append(croppedPath)
path.usesEvenOddFillRule = true
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath;
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
let blurView = UIBlurEffect(style: .light)
let effectView = UIVisualEffectView(effect: blurView)
effectView.frame = generalFrame
effectView.layer.mask = maskLayer
parentView.addSubview(imageView)
parentView.addSubview(effectView)

iOS Radar Chart with 3D Effect

I would like to replicate this graph in my app.
I tried to search online but I found only pods, specifically Charts.
I tried to customize it but I was unable to give it the 3d effect by assigning each "triangle" a different color shade.
How can I replicate it?
Uibezierpath or something else?
Just have fun.
class GraphView: UIView {
let cirleSegnaposto:CGFloat = 20.0
let labelSize:Double = 50
let spacingGraphLabel:Double = 0
let widthOfZero:Double = 30
let labels = ["Label 1", "Label 2", "Label 3", "Label 4", "Label 5"]
let firstColors:[UIColor] = [.darkGray, .black, .darkGray, .lightGray, .white]
let secondColors:[UIColor] = [.orange, .brown, .orange, .yellow, .red]
var values: [Int]? = nil
var secondValues: [Int]? = nil
override func draw(_ rect: CGRect) {
for i in 0 ..< 4 {
let cirleLayer = CAShapeLayer()
let delta = Double(15 * i) + labelSize
let path = UIBezierPath(ovalIn: CGRect(x: delta,
y: delta,
width: Double(rect.width) - delta * 2,
height: Double(rect.width) - delta * 2))
cirleLayer.path = path.cgPath
cirleLayer.lineWidth = 1
cirleLayer.strokeColor = UIColor.lightGray.cgColor
cirleLayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(cirleLayer)
}
let radius:Double = Double(rect.width/2) - (labelSize - spacingGraphLabel)
let labelRadius:Double = Double(rect.width/2) + (spacingGraphLabel)
let origin = CGPoint(x: rect.width/2, y: rect.height/2)
for i in 0..<5 {
let cirleLayer = CAShapeLayer()
let angle:Double = Double(i)/5.0 * (2 * .pi)
let centerX = Double(origin.x) + radius * cos(angle)
let centerY = Double(origin.y) - radius * sin(angle)
let path = UIBezierPath(ovalIn: CGRect(x: CGFloat(centerX) - cirleSegnaposto/2,
y: CGFloat(centerY) - cirleSegnaposto/2,
width: cirleSegnaposto,
height: cirleSegnaposto))
cirleLayer.path = path.cgPath
cirleLayer.fillColor = UIColor.lightGray.cgColor
cirleLayer.lineWidth = 0.5
cirleLayer.strokeColor = UIColor.black.cgColor
self.layer.addSublayer(cirleLayer)
let label = UILabel(frame: .zero)
label.font = UIFont.systemFont(ofSize: 12)
label.text = labels[i]
label.frame.size = CGSize(width: labelSize, height: labelSize/2)
let labelCenterX = Double(origin.x) + labelRadius * cos(angle)
let labelCenterY = Double(origin.y) - labelRadius * sin(angle)
label.center = CGPoint(x: labelCenterX, y: labelCenterY)
label.transform = label.transform.rotated(by: .pi/2)
self.addSubview(label)
}
if let values = secondValues {
drawGraph(values: values, center: origin, maxValue: radius, colors: secondColors.map({$0.cgColor}))
}
if let values = values {
drawGraph(values: values, center: origin, maxValue: radius, colors: firstColors.map({$0.cgColor}))
}
}
func drawGraph(values: [Int], center: CGPoint, maxValue: Double, colors: [CGColor]) {
var points = [CGPoint]()
for i in 0 ..< values.count {
let radius = Double(values[i])/10.0 * (maxValue - widthOfZero) + widthOfZero
let angle:Double = Double(i)/5.0 * (2 * .pi)
let x = Double(center.x) + radius * cos(angle)
let y = Double(center.y) - radius * sin(angle)
let point = CGPoint(x: x, y: y)
points.append(point)
}
for (i, point) in points.enumerated() {
let secondPoint = point == points.last ? points[0] : points[i+1]
let path = UIBezierPath()
path.move(to: center)
path.addLine(to: point)
path.addLine(to: secondPoint)
path.close()
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.fillColor = colors[i]
layer.lineWidth = 1
layer.lineJoin = .round
layer.strokeColor = UIColor.black.cgColor
self.layer.addSublayer(layer)
}
}
}

CAKeyframeAnimation rotation don't draw real shadow

I want to rotate an UIImageView with CAKeyframeAnimation. The problem is that the shadow rotates with the object, making a non-real effect... The shadow must remain under the image, not rotating around it
This is my code. Very simple:
let imagen:UIImageView = UIImageView(image:UIImage(named: "material6.png"))
self.view.addSubview(imagen)
imagen.center = CGPoint(x: 100, y: 100)
imagen.layer.shadowOpacity = 0.75
imagen.layer.shadowOffset = CGSize(width: 0, height: 15)
//Animación del ángulo
let animacionAngulo = CAKeyframeAnimation(keyPath: "transform.rotation.z")
var valoresAngulo = [NSNumber]()
let degrees:Float = 360
let radians = degrees * Float.pi / Float(180.0)
valoresAngulo.append(NSNumber(value: 0))
valoresAngulo.append(NSNumber(value: radians))
animacionAngulo.values = valoresAngulo
//Permite añadir varias animaciones y gestionarlas a la vez
animacionAngulo.repeatCount = Float.infinity
animacionAngulo.duration = 2.0
imagen.layer.add(animacionAngulo, forKey: nil)
Any solution?
If the shadow is rotating along with the image. You can solve it by adding an UIView with same dimensions of your UIImageView below your UIImageView. If you add shadow to your UIView instead of UIImageView, you can achieve the desired effect in this case.
let imagen:UIImageView = UIImageView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
imagen.center = CGPoint(x: 100, y: 100)
imagen.backgroundColor = UIColor.red
let belowView: UIView = UIView(frame: imagen.frame)
belowView.backgroundColor = UIColor.white
belowView.layer.shadowOpacity = 0.75
belowView.layer.shadowOffset = CGSize(width: 0, height: 15)
self.view.addSubview(belowView)
self.view.addSubview(imagen)
//Animación del ángulo
let animacionAngulo = CAKeyframeAnimation(keyPath: "transform.rotation.z")
var valoresAngulo = [NSNumber]()
let degrees:Float = 360
let radians = degrees * Float.pi / Float(180.0)
valoresAngulo.append(NSNumber(value: 0))
valoresAngulo.append(NSNumber(value: radians))
animacionAngulo.values = valoresAngulo
//Permite añadir varias animaciones y gestionarlas a la vez
animacionAngulo.repeatCount = Float.infinity
animacionAngulo.duration = 2.0
imagen.layer.add(animacionAngulo, forKey: nil)

position of a CALayer after change bounds size

I have a CATextLayer with some CGAffineTransform. The bounds is same to parent bounds.
If I change the bounds size to text size, then position of a layer is also changed.
The red text is the layer without changing bounds.
How to calculating the position after changing bounds size? (green text)
Here is code from Playground:
import Cocoa
let frame = CGRect(origin: .zero, size: CGSize(width: 1200, height: 800))
let transform = CGAffineTransform(a: 0.811281, b: 0.584656, c: -0.484656, d: 0.611281, tx: 420.768, ty: -170.049)
func text(with color: NSColor, apply: Bool = false) -> CATextLayer {
let layer = CATextLayer()
let text = NSAttributedString(string: "valami", attributes: [NSForegroundColorAttributeName : color , NSFontAttributeName : NSFont.boldSystemFont(ofSize: 180)])
layer.string = text
layer.frame = frame
layer.bounds = frame
if apply {
layer.bounds.size = text.size()
// let translate = CGAffineTransform(translationX: -408.5, y: -12.8)
// let concate = transform.concatenating(translate)
// layer.setAffineTransform(concate)
layer.setAffineTransform(transform)
}
let border = CAShapeLayer()
border.path = CGPath(rect: layer.bounds, transform: nil)
border.fillColor = nil
border.strokeColor = color.cgColor
layer.addSublayer(border)
return layer
}
let background = CAShapeLayer()
background.path = CGPath(rect: frame, transform: nil)
background.fillColor = NSColor.white.cgColor
background.strokeColor = nil
let textLayer1 = text(with: .red)
let textLayer2 = text(with: NSColor.green.withAlphaComponent(0.5), apply: true)
let group = CALayer()
group.addSublayer(textLayer1)
group.bounds = frame
group.frame = frame
group.setAffineTransform(transform)
let view = NSView(frame: frame)
view.wantsLayer = true
view.layer = CALayer()
view.layer?.addSublayer(background)
view.layer?.addSublayer(group)
view.layer?.addSublayer(textLayer2)
view
OK, I figured out!
This is the working apply block:
if apply {
let textSize = text.size()
let widthChange = layer.bounds.width - textSize.width
let heightChange = layer.bounds.height - textSize.height
let anx = ((transform.c / 2) * heightChange) - ((transform.a / 2) * widthChange)
let any = ((transform.d / 2) * heightChange) - ((transform.b / 2) * widthChange)
let translate = CGAffineTransform(translationX: anx, y: any)
let concate = transform.concatenating(translate)
layer.setAffineTransform(concate)
layer.bounds.size = textSize
}

Resources