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

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

Related

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)
}
}
}

Placing UILabel along drawn line

I want to place UILabel along drawn line with CAShapeLayer.
Like that:
Currently I can't understand how to calculate X and Y points to place the label at center of the line taking into calculated angle.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let linePath = UIBezierPath()
linePath.move(to: CGPoint(x: initialPoint.x, y: initialPoint.y))
linePath.addLine(to: CGPoint(x: finalPoint.x, y: finalPoint.y))
let lineLayer = CAShapeLayer()
lineLayer.lineWidth = lineWidth
lineLayer.strokeColor = UIColor.white.cgColor
lineLayer.lineCap = .round
lineLayer.path = linePath.cgPath
view.layer.insertSublayer(lineLayer, at: 0)
// X & Y don't place correctly
let label = UILabel(frame: CGRect(x: (initialPoint.x - finalPoint.x) / 2, y: (initialPoint.y - finalPoint.y) / 2, width: view.frame.width, height: view.frame.height))
let angle = atan2(finalPoint.x - initialPoint.x, finalPoint.y - initialPoint.y + navigationHeight)
label.textColor = .white
label.font = label.font.withSize(24.0)
label.text = "Text here"
label.transform = CGAffineTransform(rotationAngle: angle)
view.addSubview(label)
}

iOS Swift: create a mask from a UILabel

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

How can i add 3 background colour in single UIView??

I Need to add three colour in single background colour
Without using 3 UIView or image.
Create custom UIView and override the draw(_:) function. Then use the current CGContext and draw according to your preferred size. Example based on the alignment from the given image is shown below:
class CustomView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
let firstRect = CGRect(origin: CGPoint(x: rect.origin.x, y: rect.origin.y), size: CGSize(width: rect.size.width / 3, height: rect.size.height))
let middleRect = CGRect(origin: CGPoint(x: firstRect.maxX, y: rect.origin.y), size: CGSize(width: rect.size.width / 3, height: rect.size.height))
let lastRect = CGRect(origin: CGPoint(x: middleRect.maxX, y: rect.origin.y), size: CGSize(width: rect.size.width / 3, height: rect.size.height))
let arrayTuple: [(rect: CGRect, color: CGColor)] = [(firstRect, UIColor.red.cgColor), (middleRect, UIColor.green.cgColor), (lastRect, UIColor.blue.cgColor)]
for tuple in arrayTuple {
context.setFillColor(tuple.color)
context.fill(tuple.rect)
}
}
}
Use this below func
func addSublayers (_ viewCustom : UIView){
let layer1 = CAShapeLayer()
let layer2 = CAShapeLayer()
let layer3 = CAShapeLayer()
layer1.frame = CGRect(origin: viewCustom.bounds.origin,
size: CGSize(width: viewCustom.frame.size.width/3,
height: viewCustom.frame.size.height))
layer2.frame = CGRect(x: layer1.frame.size.width,
y: layer1.frame.origin.y,
width: viewCustom.frame.size.width/3,
height: viewCustom.frame.size.height)
layer3.frame = CGRect(x: layer2.frame.size.width + layer2.frame.origin.x,
y: layer2.frame.origin.y,
width: viewCustom.frame.size.width/3,
height: viewCustom.frame.size.height)
layer1.backgroundColor = UIColor.red.cgColor
layer2.backgroundColor = UIColor.green.cgColor
layer3.backgroundColor = UIColor.blue.cgColor
viewCustom.layer.addSublayer(layer1)
viewCustom.layer.addSublayer(layer2)
viewCustom.layer.addSublayer(layer3)
}
Output:
extension UIView {
func addMultipleColorsHorizontal(colors: [UIColor]) {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
var colorsArray: [CGColor] = []
var locationsArray: [NSNumber] = []
for (index, color) in colors.enumerated() {
colorsArray.append(color.cgColor)
colorsArray.append(color.cgColor)
locationsArray.append(NSNumber(value: (1.0 / Double(colors.count)) * Double(index)))
locationsArray.append(NSNumber(value: (1.0 / Double(colors.count)) * Double(index + 1)))
}
gradientLayer.colors = colorsArray
gradientLayer.locations = locationsArray
gradientLayer.startPoint = CGPoint(x: 0, y: 1)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
self.backgroundColor = .clear
self.layer.addSublayer(gradientLayer)
}
}

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