Placing UILabel along drawn line - ios

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

Related

iOS - How to set a CATextLayer inside a CAShapeLayer which is drawn with a custom UIBezierPath?

I want to program a custom pie menu. In the code below you see how I create a pie menu with two items. My structure is the following: I'm using a rectengular UIBezierPath with a CAShapeLayer as the context as my circular background. Inside my circular background I've got a child, the inner small circle (also UIBezierPath with CAShapeLayer). The other childs of my circular background layer are the items, which are also a CAShapeLayer with using a custom UIBezierPath (I draw my items depends on the number of items (different degrees and so on)). Now I want to add inside every item layer a CATextLayer ("Item 1", "Item 2" and so on). My problem is, that I don't know how to set the frame of my specific item layers and how I can add the specific CATextLayer in the way that the text is dynamically inside the parent item layer. In my case the CATextLayer depends on the frame of the menu background layer.
func setMenuBackgroundLayer() {
//Draw a circle background with UIBezierPath for the static pie menu
let path = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height / 2), radius: menuRadius, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2), clockwise: true)
menuBackgroundLayer = CAShapeLayer()
menuBackgroundLayer.path = path.cgPath
menuBackgroundLayer.fillColor = menuBackgroundLayerColor.cgColor
menuBackgroundLayer.frame = self.bounds
menuBackgroundLayer.zPosition = 1
self.layer.addSublayer(menuBackgroundLayer)
//Draw the inner circle (back button)
let pathInner = UIBezierPath(arcCenter: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2), radius: innerCircleRadius, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2), clockwise: true)
innerCircleLayer = CAShapeLayer()
innerCircleLayer.path = pathInner.cgPath
innerCircleLayer.fillColor = menuBackgroundLayerColor.cgColor
innerCircleLayer.strokeColor = UIColor.black.cgColor
innerCircleLayer.lineWidth = 1
innerCircleLayer.frame = menuBackgroundLayer.frame
menuBackgroundLayer.addSublayer(innerCircleLayer)
//Set the inner circle above all other menu items
innerCircleLayer.zPosition = 100
//Add the arrow image inside the inner circle
//addBackImage()
}
func insertMenuItems() {
//Compare which item has to get inserted and insert it
if numberOfItems == 1 {
let path = UIBezierPath(arcCenter: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2), radius: menuRadius, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2), clockwise: true)
item1Layer = CAShapeLayer()
item1Layer.path = path.cgPath
item1Layer.fillColor = menuBackgroundLayerColor.cgColor
item1Layer.strokeColor = UIColor.black.cgColor
item1Layer.lineWidth = 1
item1Layer.frame = menuBackgroundLayer.bounds
menuBackgroundLayer.addSublayer(item1Layer)
item1Layer.zPosition = 2
let textLayer = CATextLayer()
textLayer.string = "ITEM 1"
textLayer.foregroundColor = UIColor.white.cgColor
textLayer.font = UIFont(name: "Avenir", size: 15.0)
textLayer.fontSize = 15.0
textLayer.alignmentMode = CATextLayerAlignmentMode.center
textLayer.zPosition = 3
textLayer.frame = item1Layer.bounds
textLayer.position = CGPoint(x: item1Layer.position.x, y: item1Layer.position.y + 20.0)
textLayer.contentsScale = UIScreen.main.scale
item1Layer.addSublayer(textLayer)
}
else if numberOfItems == 2 {
//Item 1
let path1 = UIBezierPath()
path1.move(to: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2))
path1.addArc(withCenter: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2), radius: menuRadius, startAngle: rad2deg(180.0), endAngle: rad2deg(0.0), clockwise: true)
path1.close()
item1Layer = CAShapeLayer()
item1Layer.path = path1.cgPath
item1Layer.fillColor = menuBackgroundLayerColor.cgColor
item1Layer.strokeColor = UIColor.black.cgColor
item1Layer.lineWidth = 1
item1Layer.frame = menuBackgroundLayer.bounds
menuBackgroundLayer.addSublayer(item1Layer)
item1Layer.zPosition = 2
let textLayer1 = CATextLayer()
textLayer1.string = "ITEM 1"
textLayer1.foregroundColor = UIColor.white.cgColor
textLayer1.font = UIFont(name: "Avenir", size: 15.0)
textLayer1.fontSize = 15.0
textLayer1.alignmentMode = CATextLayerAlignmentMode.center
textLayer1.zPosition = 3
textLayer1.frame = item1Layer.bounds
textLayer1.position = CGPoint(x: item1Layer.position.x, y: item1Layer.position.y + 20.0)
textLayer1.contentsScale = UIScreen.main.scale
item1Layer.addSublayer(textLayer1)
//Item 2
let path2 = UIBezierPath()
path2.move(to: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2))
path2.addArc(withCenter: CGPoint(x: menuBackgroundLayer.frame.size.width / 2, y: menuBackgroundLayer.frame.size.height / 2), radius: menuRadius, startAngle: rad2deg(0.0), endAngle: rad2deg(180.0), clockwise: true)
path2.close()
item2Layer = CAShapeLayer()
item2Layer.path = path2.cgPath
item2Layer.fillColor = menuBackgroundLayerColor.cgColor
item2Layer.strokeColor = UIColor.black.cgColor
item2Layer.lineWidth = 1
item2Layer.frame = menuBackgroundLayer.bounds
menuBackgroundLayer.addSublayer(item2Layer)
item2Layer.zPosition = 2
let textLayer2 = CATextLayer()
textLayer2.string = "ITEM 2"
textLayer2.foregroundColor = UIColor.white.cgColor
textLayer2.font = UIFont(name: "Avenir", size: 15.0)
textLayer2.fontSize = 15.0
textLayer2.alignmentMode = CATextLayerAlignmentMode.center
textLayer2.zPosition = 3
textLayer2.frame = item2Layer.bounds
textLayer2.position = CGPoint(x: item2Layer.position.x, y: item2Layer.position.y + 20.0)
textLayer2.contentsScale = UIScreen.main.scale
item2Layer.addSublayer(textLayer2)
}
and so on...
}
So, here's a rough prototype which does the stuff you need, but not very precise.
If you want to rotate the text, this can be achieved with CATransform.
You can play with the code here: https://github.com/gatamar/stackoverflow_answers/tree/master/so64348954
Or I can make it more precise, if this is almost what you need.
The code for Pie Menu:
import Foundation
import UIKit
class HackLinesView: UIView {
init(frame: CGRect, partsCount parts: Int) {
super.init(frame: frame)
backgroundColor = .clear
let side = frame.width/2
// add lines
for part in 0..<parts {
let angle = CGFloat(part)/CGFloat(parts) * 2 * .pi
let lineLayer = CAShapeLayer()
lineLayer.backgroundColor = UIColor.black.cgColor
let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 1, height: side))
lineLayer.path = path.cgPath
lineLayer.transform = CATransform3DMakeRotation(angle, 0, 0, 1)
layer.addSublayer(lineLayer)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class PieMenuView: UIView {
init(frame: CGRect, partsCount parts: Int) {
assert( abs(frame.width-frame.height) < 0.001)
super.init(frame: frame)
setupLayers(parts)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupLayers(_ parts: Int) {
let side = bounds.width
let outerRadius = side * 0.5
let innerRadius = side * 0.2
// add outer circle
let outerCircleLayer = CAShapeLayer()
outerCircleLayer.frame = bounds
outerCircleLayer.cornerRadius = outerRadius
outerCircleLayer.backgroundColor = UIColor.orange.cgColor
layer.addSublayer(outerCircleLayer)
// add inner circle
let innerCircleLayer = CAShapeLayer()
innerCircleLayer.frame = CGRect(x: side/2-innerRadius, y: side/2-innerRadius, width: innerRadius*2, height: innerRadius*2)
innerCircleLayer.cornerRadius = innerRadius
innerCircleLayer.backgroundColor = UIColor.yellow.cgColor
layer.addSublayer(innerCircleLayer)
let linesView = HackLinesView(frame: CGRect(x: side/2, y: side/2, width: side, height: side), partsCount: parts)
addSubview(linesView)
// add text
for part in 0..<parts {
let angle = CGFloat(part)/CGFloat(parts) * 2 * .pi
let textLayer = CATextLayer()
textLayer.string = String(format: "%d", part)
textLayer.foregroundColor = UIColor.blue.cgColor
// calc the center for text layer
let x1 = side/2
let y1 = side/2
let x2 = x1 + cos(angle)*outerRadius
let y2 = y1 + sin(angle)*outerRadius
let textCenterX = (x1 + x2)/2, textCenterY = (y1 + y2)/2
let textLayerSide: CGFloat = 50
textLayer.frame = CGRect(x: textCenterX-textLayerSide/2, y: textCenterY-textLayerSide/2, width: textLayerSide, height: textLayerSide)
layer.addSublayer(textLayer)
}
}
}

How to set CAShapeLayer 's strokeColor in swift

I want draw diagonal view.
This is my code.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let path = UIBezierPath()
path.move(to: CGPoint.zero)
path.addLine(to: CGPoint(x: 50, y: 50))
let view = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
view.backgroundColor = UIColor.blue
let mask = CAShapeLayer()
mask.frame = view.bounds
mask.path = path.cgPath
mask.strokeColor = UIColor.orange.cgColor
mask.lineWidth = 25
view.layer.mask = mask
self.view.addSubview(view)
}
I set strokeColor to orange. But color is blue backgroundColor
Why does this happen?
Here is the code snippet:
let path = UIBezierPath()
path.move(to: CGPoint.zero)
path.addLine(to: CGPoint(x: 50, y: 50))
let view = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
view.backgroundColor = UIColor.green
let diagonal = CAShapeLayer()
diagonal.frame = view.bounds
diagonal.path = path.cgPath
diagonal.strokeColor = UIColor.orange.cgColor
diagonal.lineWidth = 15
diagonal.backgroundColor = UIColor.blue.cgColor
// view.layer.mask = mask
view.layer.addSublayer(diagonal)
self.view.addSubview(view)
This results in:

Corner radius image Swift

I'm trying to make this corner radius image...it's not exactly the same shape of the image..any easy answer instead of trying random numbers of width and height ?
thanks alot
let rectShape = CAShapeLayer()
rectShape.bounds = self.mainImg.frame
rectShape.position = self.mainImg.center
rectShape.path = UIBezierPath(roundedRect: self.mainImg.bounds, byRoundingCorners: [.bottomLeft , .bottomRight ], cornerRadii: CGSize(width: 50, height: 4)).cgPath
You can use QuadCurve to get the design you want.
Here is a Swift #IBDesignable class that lets you specify the image and the "height" of the rounding in Storyboard / Interface Builder:
#IBDesignable
class RoundedBottomImageView: UIView {
var imageView: UIImageView!
#IBInspectable var image: UIImage? {
didSet { self.imageView.image = image }
}
#IBInspectable var roundingValue: CGFloat = 0.0 {
didSet {
self.setNeedsLayout()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
doMyInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
doMyInit()
}
func doMyInit() {
imageView = UIImageView()
imageView.backgroundColor = UIColor.red
imageView.contentMode = UIViewContentMode.scaleAspectFill
addSubview(imageView)
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = self.bounds
let rect = self.bounds
let y:CGFloat = rect.size.height - roundingValue
let curveTo:CGFloat = rect.size.height + roundingValue
let myBezier = UIBezierPath()
myBezier.move(to: CGPoint(x: 0, y: y))
myBezier.addQuadCurve(to: CGPoint(x: rect.width, y: y), controlPoint: CGPoint(x: rect.width / 2, y: curveTo))
myBezier.addLine(to: CGPoint(x: rect.width, y: 0))
myBezier.addLine(to: CGPoint(x: 0, y: 0))
myBezier.close()
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
Result with 300 x 200 image view, rounding set to 40:
Edit - (3.5 years later)...
To answer #MiteshDobareeya comment, we can switch the rounded edge from Bottom to Top by transforming the bezier path:
let c = CGAffineTransform(scaleX: 1, y: -1).concatenating(CGAffineTransform(translationX: 0, y: bounds.size.height))
myBezier.apply(c)
It's been quite a while since this answer was originally posted, so a few changes:
subclass UIImageView directly - no need to make it a UIView with an embedded UIImageView
add a Bool roundTop var
if set to False (the default), we round the Bottom
if set to True, we round the Top
re-order and "name" our path points for clarity
So, the basic principle:
We create a UIBezierPath and:
move to pt1
add a line to pt2
add a line to pt3
add a quad-curve to pt4 with controlPoint
close the path
use that path for a CAShapeLayer mask
the result:
If we want to round the Top, after closing the path we can apply apply a scale transform using -1 as the y value to vertically mirror it. Because that transform mirror it at "y-zero" we also apply a translate transform to move it back down into place.
That gives us:
Here's the updated class:
#IBDesignable
class RoundedTopBottomImageView: UIImageView {
#IBInspectable var roundingValue: CGFloat = 0.0 {
didSet {
self.setNeedsLayout()
}
}
#IBInspectable var roundTop: Bool = false {
didSet {
self.setNeedsLayout()
}
}
override func layoutSubviews() {
super.layoutSubviews()
let r = bounds
let myBezier = UIBezierPath()
let pt1: CGPoint = CGPoint(x: r.minX, y: r.minY)
let pt2: CGPoint = CGPoint(x: r.maxX, y: r.minY)
let pt3: CGPoint = CGPoint(x: r.maxX, y: r.maxY - roundingValue)
let pt4: CGPoint = CGPoint(x: r.minX, y: r.maxY - roundingValue)
let controlPoint: CGPoint = CGPoint(x: r.midX, y: r.maxY + roundingValue)
myBezier.move(to: pt1)
myBezier.addLine(to: pt2)
myBezier.addLine(to: pt3)
myBezier.addQuadCurve(to: pt4, controlPoint: controlPoint)
myBezier.close()
if roundTop {
// if we want to round the Top instead of the bottom,
// flip the path vertically
let c = CGAffineTransform(scaleX: 1, y: -1) //.concatenating(CGAffineTransform(translationX: 0, y: bounds.size.height))
myBezier.apply(c)
}
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
You can try with UIView extension. as
extension UIView {
func setBottomCurve(){
let offset = CGFloat(self.frame.size.height + self.frame.size.height/1.8)
let bounds = self.bounds
let rectBounds = CGRect(x: bounds.origin.x,
y: bounds.origin.y ,
width: bounds.size.width,
height: bounds.size.height / 2)
let rectPath = UIBezierPath(rect: rectBounds)
let ovalBounds = CGRect(x: bounds.origin.x - offset / 2,
y: bounds.origin.y ,
width: bounds.size.width + offset,
height: bounds.size.height)
let ovalPath = UIBezierPath(ovalIn: ovalBounds)
rectPath.append(ovalPath)
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = rectPath.cgPath
self.layer.mask = maskLayer
}
}
& use it in viewWillAppear like methods where you can get actual frame of UIImageView.
Usage:
override func viewWillAppear(_ animated: Bool) {
//use it in viewWillAppear like methods where you can get actual frame of UIImageView
myImageView.setBottomCurve()
}

iOS Gradient not perpendicular to gradient vector

I have added a gradient on the component, it is at an angle. This gradient is not perpendicular to the gradient vector. Device iPhone 6s, for convenience, have set the size constant. What could be the problem?
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let topPoint = CGPoint(x: 33, y: -55)
let botPoint = CGPoint(x: 217, y: 469)
let iPhoneSize = CGSize(width: 375, height: 667)
let additionalLayer = CAGradientLayer()
additionalLayer.frame = view.bounds
additionalLayer.startPoint = CGPoint(x: topPoint.x / iPhoneSize.width, y: topPoint.y / iPhoneSize.height)
additionalLayer.endPoint = CGPoint(x: botPoint.x / iPhoneSize.width, y: botPoint.y / iPhoneSize.height)
additionalLayer.colors = [UIColor.white.cgColor, UIColor.darkGray.cgColor, UIColor.black.cgColor]
additionalLayer.locations = [0.0, 0.468, 1.0]
drawLine(onLayer: additionalLayer, fromPoint: topPoint, toPoint: botPoint)
view.layer.addSublayer(additionalLayer)
}
func drawLine(onLayer layer: CALayer, fromPoint start: CGPoint, toPoint end: CGPoint) {
let line = CAShapeLayer()
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
line.path = linePath.cgPath
line.fillColor = nil
line.opacity = 1.0
line.strokeColor = UIColor.red.cgColor
layer.addSublayer(line)
}
}
P.S. I've tried add this code to viewDidLayoutSubviews()
P.P.S. Also have added screenshot.

UILabel won't center in UIView

I'm trying to create a custom UIView that can be used as a progress bar or similar (#IBDesignable with IBInspectables). I want a centered (primarily X-axis, but for now Y as well) UILabel in this view.
private func addLabel()
{
let label = UILabel(frame: CGRectMake(0, 0, self.frame.width / 4, self.frame.height / 4))
label.center = self.center
label.textAlignment = .Center
label.text = "5"
//Color just to see the whole label
label.backgroundColor = UIColor.redColor().colorWithAlphaComponent(0.5)
self.addSubview(label)
}
In Interfacebuilder the label centers just fine:
However, when running it on my device (iPhone 5S) the label is aligned slightly to the right (picture below). I've tried different approaches (like making the label frame self.frame) but it's still not centered correctly. What am I missing?
override func drawRect(rect: CGRect) {
// Drawing code
let center = CGPoint(x:bounds.width/2, y: bounds.height)
let radius: CGFloat = max(bounds.width, bounds.height)
let startAngle: CGFloat = π
let endAngle: CGFloat = 2 * π
path = UIBezierPath(arcCenter: center, radius: radius / 2 - arcWidth / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.lineWidth = arcWidth
arcBackgroundColor.setStroke()
path.stroke()
self.addLayer()
}
private func addLayer()
{
progressLayer = CAShapeLayer()
progressLayer.path = self.path.CGPath
progressLayer.strokeColor = self.progressColor.CGColor
progressLayer.fillColor = UIColor.clearColor().CGColor
progressLayer.lineWidth = self.arcWidth
if animatesOnDraw && progress > 0 {
self.layer.addSublayer(progressLayer)
self.animateProgress(0)
} else {
progressLayer.strokeEnd = CGFloat(self.progress / self.maxValue)
progressLayer.opacity = Float(self.progressStrokeEndValue)
self.layer.addSublayer(progressLayer)
}
addLabel() //as shown above
}
The problem is label.center = self.center, because the center of the label and the center of the superview are in different coordinate systems so they might not be the same. Use instead
label.center = CGPoint(x: self.view.frame.bounds.size.width / 2.0, y:self.view.frame.bounds.size.height / 2.0)
Try this:
self.label.center = view.center
Add the UILabel programatically and Modify the UILabel frame from
let label = UILabel(frame: CGRectMake(0, 0, self.frame.width / 4, self.frame.height / 4))
To
let label = UILabel(frame: CGRectMake(self.frame.width/2 - (self.frame.height/4)/2, 0, self.frame.width / 4, self.frame.height / 4))
ELSE
Implement Autolayout
A possible solution can be:-
#IBOutlet customView : UIView!
//Let customView be the outlet of your custom view
func addLabel () {
let CenterY = customView.center.y
let CenterX = customView.center.x
let heightOfCustom = customView.frame.size.height
let widthOfCustom = customView.frame.size.width
// If you want a label with height and width 1/4th of the width of the custom view
let label = UILabel(frame : CGRectMake(CenterX - widthOfCustom/8, CenterY - heightOfCustom/8, widthOfCustom/4, heightOfCustom/4))
label.text = "5"
label.textAlignment = .Center
label.backgroundColor = UIColor.redColor().colorWithAlphaComponent(0.5)
self.addSubview(label)
}

Resources