position of a CALayer after change bounds size - ios

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
}

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

How can I rotate a CATextLayer properly in Swift?

I'm currently facing an issue that when rotating a CATextLayer the frame size would be transformed unexpectedly at the same time. I expect the text can be rotated at some angle without changing its frame size. If anyone could enlighten me about this, thanks in advance.
What I have tried:
var someAngle: CGFloat = 0 {
didSet {
drawText()
}
}
func drawText() {
for layer in someView.layer.sublayers ?? [] where layer is CATextLayer {
layer.removeFromSuperlayer()
}
let textLayer = CATextLayer()
textLayer.fontSize = 26
textLayer.backgroundColor = UIColor.lightGray.cgColor
textLayer.string = "Hello world"
textLayer.alignmentMode = .center
textLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
textLayer.transform = CATransform3DMakeRotation(someAngle, 0.0, 0.0, 1.0)
let textSize = calculateTextSize(text: "Hello world", font: UIFont.systemFont(ofSize: 26))
let origin = CGPoint(x: someView.bounds.midX - textSize.width/2, y: someView.bounds.midY - textSize.height/2)
textLayer.frame = CGRect(origin: origin, size: textSize)
someView.layer.addSublayer(textLayer)
}
However, the result looks like this:
And my expectation is similar to this:
solved this by adding a textLayer property outside the function.
var textLayer = CATextLayer()

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

Custom View Drawing - Hole inside a View

How to draw View like this.
After research I got context.fillRects method can be used. But how to find the exact rects for this.
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.red.cgColor)
context?.setAlpha(0.5)
context?.fill([<#T##rects: [CGRect]##[CGRect]#>])
How to achieve this result?
Background: Blue.
Overlay(Purple): 50% opacity that contains square hole in the center
First create your view and then draw everything with two UIBezierPaths: one is describing the inside rect (the hole) and the other one runs along the borders on your screen (externalPath). This way of drawing ensures that the blue rect in the middle is a true hole and not drawn on top of the purple view.
let holeWidth: CGFloat = 200
let hollowedView = UIView(frame: view.frame)
hollowedView.backgroundColor = UIColor.clear
//Initialise the layer
let hollowedLayer = CAShapeLayer()
//Draw your two paths and append one to the other
let holePath = UIBezierPath(rect: CGRect(origin: CGPoint(x: (view.frame.width - holeWidth) / 2, y: (view.frame.height - holeWidth) / 2), size: CGSize(width: holeWidth, height: holeWidth)))
let externalPath = UIBezierPath(rect: hollowedView.frame).reversing()
holePath.append(externalPath)
holePath.usesEvenOddFillRule = true
//Assign your path to the path property of your layer
hollowedLayer.path = holePath.cgPath
hollowedLayer.fillColor = UIColor.purple.cgColor
hollowedLayer.opacity = 0.5
//Add your hollowedLayer to the layer of your hollowedView
hollowedView.layer.addSublayer(hollowedLayer)
view.addSubview(hollowedView)
The result looks like this :
Create a custom UIView with background color blue.
class CustomView: UIView {
// Try adding a rect and fill color.
override func draw(_ rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
ctx!.beginPath()
//Choose the size based on the size required.
ctx?.addRect(CGRect(x: 20, y: 20, width: rect.maxX - 40, height: rect.maxY - 40))
ctx!.closePath()
ctx?.setFillColor(UIColor.red.cgColor)
ctx!.fillPath()
}
}
I just ended up with this.
Code:
createHoleOnView()
let blurView = createBlurEffect(style: style)
self.addSubview(blurView)
Method Create Hole:
private func createHoleOnView() {
let maskView = UIView(frame: self.frame)
maskView.clipsToBounds = true;
maskView.backgroundColor = UIColor.clear
func holeRect() -> CGRect {
var holeRect = CGRect(x: 0, y: 0, width: scanViewSize.rawValue.width, height: scanViewSize.rawValue.height)
let midX = holeRect.midX
let midY = holeRect.midY
holeRect.origin.x = maskView.frame.midX - midX
holeRect.origin.y = maskView.frame.midY - midY
self.holeRect = holeRect
return holeRect
}
let outerbezierPath = UIBezierPath.init(roundedRect: self.bounds, cornerRadius: 0)
let holePath = UIBezierPath(roundedRect: holeRect(), cornerRadius: holeCornerRadius)
outerbezierPath.append(holePath)
outerbezierPath.usesEvenOddFillRule = true
let hollowedLayer = CAShapeLayer()
hollowedLayer.fillRule = kCAFillRuleEvenOdd
hollowedLayer.fillColor = outerColor.cgColor
hollowedLayer.path = outerbezierPath.cgPath
if self.holeStyle == .none {
hollowedLayer.opacity = 0.8
}
maskView.layer.addSublayer(hollowedLayer)
switch self.holeStyle {
case .none:
self.addSubview(maskView)
break
case .blur(_):
self.mask = maskView;
break
}
}
UIView's Extension function for Create Blur:
internal func createBlurEffect(style: UIBlurEffectStyle = .extraLight) -> UIView {
let blurEffect = UIBlurEffect(style: style)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = self.bounds
return blurEffectView
}

Adding border to mask layer

I'm trying to make a custom shape UIButton using mask layers and I was successful
extension UIButton {
func mask(withImage image : UIImage , frame : CGRect ){
let maskingLayer = CAShapeLayer()
maskingLayer.frame = frame
maskingLayer.contents = image.cgImage
self.layer.mask = maskingLayer
}
}
But I want to add a border (stroke) to the mask layer to indicate that this button was chosen.
I tried adding a sub layer to the mask layer
button.mask?.layer.borderColor = UIColor.black.cgColor
button.mask?.layer.borderWidth = 4
but didn't work since the button shape is still rectangle.
I know I can use CGMutablePath to define the shape
func mask(withPath path: CGMutablePath , frame : CGRect , color : UIColor) {
let mask = CAShapeLayer()
mask.frame = frame
mask.path = path
self.layer.mask = mask
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.path = path
shape.lineWidth = 3.0
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = color.cgColor
self.layer.insertSublayer(shape, at: 0)
// self.layer.sublayers![0].masksToBounds = true
}
but drawing such complex shape using paths is extremly hard
Any help would be appreciated.
i was able to get CGPath from an image(.svg) using PocketSVG
but i faced another problem that the scale of the path is equal to the original SVG so i managed to scale the path to fit in frame , here is the full code :-
extension UIView {
func mask(withSvgName ImageName : String , frame : CGRect , color : UIColor){
let svgutils = SvgUtils()
let paths = svgutils.getLayerFromSVG(withImageName: ImageName)
let mask = CAShapeLayer()
mask.frame = frame
let newPath = svgutils.resizepath(Fitin: frame, path: paths[0].cgPath)
mask.path = newPath
self.layer.mask = mask
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.path = newPath
shape.lineWidth = 2.0
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = color.cgColor
self.layer.insertSublayer(shape, at: 0)
}
}
and the utilities class
import Foundation
import PocketSVG
class SvgUtils{
func getLayerFromSVG(withImageName ImageName : String ) -> [SVGBezierPath]{
let url = Bundle.main.url(forResource: ImageName, withExtension: "svg")!
var paths = [SVGBezierPath]()
for path in SVGBezierPath.pathsFromSVG(at: url) {
paths.append(path)
}
return paths
}
func resizepath(Fitin frame : CGRect , path : CGPath) -> CGPath{
let boundingBox = path.boundingBox
let boundingBoxAspectRatio = boundingBox.width / boundingBox.height
let viewAspectRatio = frame.width / frame.height
var scaleFactor : CGFloat = 1.0
if (boundingBoxAspectRatio > viewAspectRatio) {
// Width is limiting factor
scaleFactor = frame.width / boundingBox.width
} else {
// Height is limiting factor
scaleFactor = frame.height / boundingBox.height
}
var scaleTransform = CGAffineTransform.identity
scaleTransform = scaleTransform.scaledBy(x: scaleFactor, y: scaleFactor)
scaleTransform.translatedBy(x: -boundingBox.minX, y: -boundingBox.minY)
let scaledSize = boundingBox.size.applying(CGAffineTransform (scaleX: scaleFactor, y: scaleFactor))
let centerOffset = CGSize(width: (frame.width - scaledSize.width ) / scaleFactor * 2.0, height: (frame.height - scaledSize.height) / scaleFactor * 2.0 )
scaleTransform = scaleTransform.translatedBy(x: centerOffset.width, y: centerOffset.height)
//CGPathCreateCopyByTransformingPath(path, &scaleTransform)
let scaledPath = path.copy(using: &scaleTransform)
return scaledPath!
}
}
and simply use it like this
button.mask(withSvgName: "your_svg_fileName", frame: button.bounds, color: UIColor.green)

Resources