Wrong position of CAShapelayer in sublayers of UIView - ios

I'm trying to draw: O-O-O in the midY of UIView but get:
Here is a func for calculating position for CAShapeLayer.frame. Even indices are dots and odd indices are lines. Number of lines is always minus one number of dots.
func frameForShape(at index: Int) -> CGRect {
let dotWidth = dotRadius * 2
let lineWidth = (view.bounds.width - (CGFloat(numberOfDots) * dotWidth)) / CGFloat(numberOfLines)
if index % 2 == 0 {
let count = CGFloat(index) / 2
let x = (lineWidth * count) + (dotWidth * count)
return CGRect(x: x, y: 0, width: dotWidth, height: view.bounds.height)
} else {
let count = Double(index) / 2
let x = (lineWidth * CGFloat(floor(count))) + (dotWidth * CGFloat(ceil(count)))
return CGRect(x: x, y: 0, width: lineWidth, height: view.bounds.height)
}
}
func setFrame() {
for (index, shape) in shapes.enumerated() {
shape.frame = frameForShape(at: index)
}
}
frameForShape(at:) correctly return frame position and size
This funcs draw a path in the center of given frame
func linePath(rect: CGRect) -> UIBezierPath {
let newRect = CGRect(x: rect.minX, y: rect.midY - (lineHeight / 2), width: rect.width, height: lineHeight)
let path = UIBezierPath(rect: newRect)
return path
}
func dotPath(rect: CGRect) -> UIBezierPath {
let newRect = CGRect(x: rect.midX - dotRadius, y: rect.midY - dotRadius, width: dotRadius * 2, height: dotRadius * 2)
let path = UIBezierPath(ovalIn: newRect)
return path
}
Here, I'm adding CAShapeLayers to yellow UIView
override func awakeFromNib() {
super.awakeFromNib()
for shape in shapes {
view.layer.addSublayer(shape)
}
}
override func layoutSubviews() {
super.layoutSubviews()
layoutIfNeeded()
setFrame()
build()
}
func build() {
for (index, shape) in shapes.enumerated() {
if index % 2 == 0 {
shape.path = dotPath(rect: shape.frame).cgPath
} else {
shape.path = linePath(rect: shape.frame).cgPath
}
}
}
Frames are correct so why dots and lines are misplaced?
I found this answer: https://stackoverflow.com/a/40194019/
incorrect position of your layer - frame should be equal to bounds of parent layer in parentView
But how draw path at specific position if UIBezierPath(ovalIn:) and UIBezierPath(rect:) draw all inside frame?

As the answer says, simply change shape.frame to shape.bounds in your call to build().
Remember that the coordinates of your shape layer are relative to the containing view, so bounds needs to be used.

Related

Animate tab bar shape created using UIBezierPath() based on selected index

I have created a tab Bar shape using UIBezierPath(). Im new to creating shapes so in the end I got the desired shape closer to what I wanted, not perfect but it works. It's a shape which has a wave like top, so on the left side there is a crest and second half has a trough. This is the code that I used to create the shape:
func createPath() -> CGPath {
let height: CGFloat = 40.0 // Height of the wave-like curve
let extraHeight: CGFloat = -20.0 // Additional height for top left and top right corners
let path = UIBezierPath()
let width = self.frame.width
// Creating a wave-like top edge for tab bar starting from left side
path.move(to: CGPoint(x: 0, y: extraHeight)) // Start at top left corner with extra height
path.addQuadCurve(to: CGPoint(x: width/2, y: extraHeight), controlPoint: CGPoint(x: width/4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width*3/4, y: extraHeight + height))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}
Above im using -20 so that shape stays above bounds of tab bar and second wave's trough stays above the icons of tab bar. Here is the desired result:
This was fine until I was asked to animate the shape on pressing tab bar items. So if I press second item, the crest should be above second item and if fourth, then it should be above fourth item. So I created a function called updateShape(with selectedIndex: Int) and called it in didSelect method of my TabBarController. In that im passing index of the selected tab and based on that creating new path and removing old and replacing with new one. Here is how im doing it:
func updateShape(with selectedIndex: Int) {
let shapeLayer = CAShapeLayer()
let height: CGFloat = 40.0
let extraHeight: CGFloat = -20.0
let width = self.frame.width
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: extraHeight))
if selectedIndex == 0 {
path.addQuadCurve(to: CGPoint(x: width / 2, y: extraHeight), controlPoint: CGPoint(x: width / 4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width / 2 + width / 4, y: extraHeight + height))
}
else if selectedIndex == 1 {
path.addQuadCurve(to: CGPoint(x: width / 2 + width / 4, y: extraHeight), controlPoint: CGPoint(x: width / 4 + width / 4, y: extraHeight - height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width * 3 / 4 + width / 4, y: extraHeight + height))
}
else if selectedIndex == 2 {
let xShift = width / 4
path.addQuadCurve(to: CGPoint(x: width / 2 + xShift, y: extraHeight), controlPoint: CGPoint(x: width / 8 + xShift, y: extraHeight + height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width * 7 / 8 + xShift, y: extraHeight - height))
}
else {
path.addQuadCurve(to: CGPoint(x: width / 2, y: extraHeight), controlPoint: CGPoint(x: width / 4, y: extraHeight + height))
path.addQuadCurve(to: CGPoint(x: width, y: extraHeight), controlPoint: CGPoint(x: width / 2 + width / 4, y: extraHeight - height))
}
path.addLine(to: CGPoint(x: width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.secondarySystemBackground.cgColor
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
And calling it like this:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBar = tabBarController.tabBar as! CustomTabBar
guard let index = viewControllers?.firstIndex(of: viewController) else {
return
}
tabBar.updateShape(with: index)
}
This is working fine as you can see below but the problem is I learned creating shapes just now and creating that wave based on width of screen so the crest and trough are exactly half the width of frame so I was able to do it for FourthViewController too and got this:
But the problem arises for remaining 2 indices. Im not able to create same wave which looks like the crest is moving above second or third item instead I get something like this:
It doesn't look like other to waves showing the hump over third item. Also my code is strictly based on 4 items and was wondering if Im asked to add 1 more item so tab bar has 5 or 6 items, it would be trouble. Is there any way to update my function that creates new shapes based on index of tab bar or can anyone help me just create shapes for remaining two items? The shape should look same just the hump should exactly be over the selected item.
So you want something like this:
Here's how I did it. First, I made a WaveView that draws this curve:
Notice that the major peak exactly in the center. Here's the source code:
class WaveView: UIView {
var trough: CGFloat {
didSet { setNeedsDisplay() }
}
var color: UIColor {
didSet { setNeedsDisplay() }
}
init(trough: CGFloat, color: UIColor = .white) {
self.trough = trough
self.color = color
super.init(frame: .zero)
contentMode = .redraw
isOpaque = false
}
override func draw(_ rect: CGRect) {
guard let gc = UIGraphicsGetCurrentContext() else { return }
let bounds = self.bounds
let size = bounds.size
gc.translateBy(x: bounds.origin.x, y: bounds.origin.y)
gc.move(to: .init(x: 0, y: size.height))
gc.saveGState(); do {
// Transform the geometry so the bounding box of the curve
// is (-1, 0) to (+1, +1), with the y axis going up.
gc.translateBy(x: bounds.midX, y: trough)
gc.scaleBy(x: bounds.size.width * 0.5, y: -trough)
// Now draw the curve.
for x in stride(from: -1, through: 1, by: 2 / size.width) {
let y = (cos(2.5 * .pi * x) + 1) / 2 * (1 - x * x)
gc.addLine(to: .init(x: x, y: y))
}
}; gc.restoreGState()
// The geometry is restored.
gc.addLine(to: .init(x: size.width, y: size.height))
gc.closePath()
gc.setFillColor(UIColor.white.cgColor)
gc.fillPath()
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
Then, I made a subclass of UITabBarController named WaveTabBarController that creates a WaveView and puts it behind the tab bar. It makes the WaveView exactly twice the width of the tab bar, and sets the x coordinate of the WaveView's frame such that the peak is above the selected tab item. Here's the source code:
class WaveTabBarController: UITabBarController {
let waveView = WaveView(trough: 20)
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if superclass!.instancesRespond(to: #selector(UITabBarDelegate.tabBar(_:didSelect:))) {
super.tabBar(tabBar, didSelect: item)
}
view.setNeedsLayout()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let shouldAnimate: Bool
if waveView.superview != view {
view.insertSubview(waveView, at: 0)
shouldAnimate = false
} else {
shouldAnimate = true
}
let tabBar = self.tabBar
let w: CGFloat
if let selected = tabBar.selectedItem, let items = tabBar.items {
w = (CGFloat(items.firstIndex(of: selected) ?? 0) + 0.5) / CGFloat(items.count) - 1
} else {
w = -1
}
let trough = waveView.trough
let tabBarFrame = view.convert(tabBar.bounds, from: tabBar)
let waveFrame = CGRect(
x: tabBarFrame.origin.x + tabBarFrame.size.width * w,
y: tabBarFrame.origin.y - trough,
width: 2 * tabBarFrame.size.width,
height: tabBarFrame.size.height + trough
)
guard waveFrame != waveView.frame else {
return
}
if shouldAnimate {
// Don't animate during the layout pass.
DispatchQueue.main.async { [waveView] in
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
waveView.frame = waveFrame
}
}
} else {
waveView.frame = waveFrame
}
}
}

UIView.draw(rect:) is called but not drawing its graphic context

I'm fairly new to ios programming
I try to draw some arcs for my circular progress bar in UIView.draw(rect:) method.
I'm using UIGraphicsGetCurrentContext to get the context and add arc path and draw in the method of custom UIView called MyProgressView
So I made an object of my custom view and it worked fine
But if I make a couple of more objects, they don't work
UIView.draw(rect:) is called every time whenever it needs to be.
the first one is drawing properly and the rest are not
class MyProgressView: UIView {
var startAngle: Double = 0.0
var endAngle: Double = 0.0 {
didSet{
self.setNeedsDisplay()
}
}
func radian(_ degree: Double) -> Double {
return (degree - 90) * Double.pi / 180.0
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setLineWidth(10)
context.setStrokeColor(UIColor.white.cgColor)
context.addArc(center: self.center, radius: 30, startAngle: radian(startAngle), endAngle: radian(endAngle), clockwise: false)
context.strokePath()
}
}
I put self.setNeedsDisplay() in didSet of a variable called endAngle
and inside of view controller's viewDidLoad,
let progressBar = MyProgressView()
progressBar.frame = CGRect(x: 0, y: 0, width: view.frame.width / 2, height:view.frame.height / 2)
progressBar.backgroundColor = .systemMint
progressBar.startAngle = 0
progressBar.endAngle = 180
view.addSubview(progressBar)
and it looks fine
but if I add two more, they don't work
let progressBar2 = MyProgressView()
progressBar2.frame = CGRect(x: 150, y: 0, width: view.frame.width / 2, height: view.frame.height / 2)
progressBar2.backgroundColor = .systemPink
progressBar2.startAngle = 90
progressBar2.endAngle = 270
view.addSubview(progressBar2)
let progressBar3 = MyProgressView()
progressBar3.frame = CGRect(x: 0, y: 150, width: view.frame.width / 2, height: view.frame.height / 2)
progressBar3.backgroundColor = .systemIndigo
progressBar3.startAngle = 90
progressBar3.endAngle = 270
view.addSubview(progressBar3)
Can anybody explain what's missing here?
I made it and run in playground, iOS version is 15.4
Thanks
You have 2 problems
1- The method that converts to radians isn't correct
2- You need to use a zero based center point
class MyProgressView: UIView {
var startAngle: Double = 0.0
var endAngle: Double = 0.0
func radian(_ number: Double) -> CGFloat {
return CGFloat(number * .pi / 180)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setLineWidth(10)
context.setStrokeColor(UIColor.white.cgColor)
context.addArc(center: CGPoint(x:rect.size.width / 2, y:rect.size.height / 2), radius: 30, startAngle: radian(startAngle), endAngle: radian(endAngle), clockwise: false)
context.strokePath()
}
}
Full demo
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let progressBar = MyProgressView()
progressBar.frame = CGRect(x: 0, y: 0, width: view.frame.width / 2, height:view.frame.height / 2)
progressBar.backgroundColor = .systemMint
progressBar.startAngle = 0
progressBar.endAngle = 180
view.addSubview(progressBar)
let progressBar2 = MyProgressView()
progressBar2.frame = CGRect(x: 150, y: 0, width: view.frame.width / 2, height: view.frame.height / 2)
progressBar2.backgroundColor = .systemPink
progressBar2.startAngle = 90
progressBar2.endAngle = 270
view.addSubview(progressBar2)
let progressBar3 = MyProgressView()
progressBar3.frame = CGRect(x: 0, y: 300, width: view.frame.width / 2, height: view.frame.height / 2)
progressBar3.backgroundColor = .systemIndigo
progressBar3.startAngle = 90
progressBar3.endAngle = 270
view.addSubview(progressBar3)
}
}
class MyProgressView: UIView {
var startAngle: Double = 0.0
var endAngle: Double = 0.0
func radian(_ number: Double) -> CGFloat {
return CGFloat(number * .pi / 180)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setLineWidth(10)
context.setStrokeColor(UIColor.white.cgColor)
context.addArc(center: CGPoint(x:rect.size.width / 2, y:rect.size.height / 2), radius: 30, startAngle: radian(startAngle), endAngle: radian(endAngle), clockwise: false)
context.strokePath()
}
}

Add subview on a parent drawn through UIBezierPath

I've a custom UITabBar. Its bar has a simple but customised shape: its height is bigger then default one, has rounded corners and (important) a shadow layer on the top.
The result is this:
Now I've to add an element that shows the selected section on the top of the bar, to achieve this:
The problem is that no matter the way I choose to add this element (add a subview to the bar or add a new sublayer) but the new element will always be drawn outside the corners. I suppose this is because I can't enable the clipping mask (if I enable the clipping mask I'll kill the shadow and also, more important, the bezierpath)
Do you have any tips for this?
Basically, the goal should be:
have an element that moves horizontally (animated) but cannot be drawn outside the parent (the tabbar)
Actually, the code to draw the custom tabBar is:
class CustomTabBar: UITabBar {
/// The layer that defines the custom shape
private var shapeLayer: CALayer?
/// The radius for the border of the bar
var borderRadius: CGFloat = 0
override func layoutSubviews() {
super.layoutSubviews()
// aspect and shadow
isTranslucent = false
backgroundColor = UIColor.white
tintColor = AZTheme.PaletteColor.primaryColor
shadowImage = nil
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.1
layer.shadowOffset = CGSize(width: 0, height: -1)
layer.shadowRadius = 10
}
override func draw(_ rect: CGRect) {
drawShape()
}
/// Draw and apply the custom shape to the bar
func drawShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = createPath()
shapeLayer.fillColor = AZTheme.tabBarControllerBackgroundColor.cgColor
if let oldShapeLayer = self.shapeLayer {
self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
} else {
self.layer.insertSublayer(shapeLayer, at: 0)
}
self.shapeLayer = shapeLayer
}
}
// MARK: - Private functions
extension CustomTabBar {
/// Return the custom shape for the bar
internal func createPath() -> CGPath {
let height: CGFloat = self.frame.height
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addArc(withCenter: CGPoint(x: borderRadius, y: 0), radius: borderRadius, startAngle: CGFloat.pi, endAngle: CGFloat.pi * (3/2), clockwise: true)
path.addLine(to: CGPoint(x: frame.width - borderRadius, y: -borderRadius))
path.addArc(withCenter: CGPoint(x: frame.width - borderRadius, y: 0), radius: borderRadius, startAngle: CGFloat.pi * (3/2), endAngle: 0, clockwise: true)
path.addLine(to: CGPoint(x: frame.width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.close()
return path.cgPath
}
}
I solved splitting the owner of custom shape from the owner of the shadow in 2 different views. So I'm using 3 views achieve the goal.
CustomTabBar: has default size and casts shadow with offset.
|
└ SelectorContainer: is a view with custom shape (BezierPath) that is
positioned on the top of the TabBar to graphically "extend" the view
and have the feeling of a bigger TabBar. It has rounded corners on
the top-right, top-left margin. MaskToBounds enabled.
|
└ Selector: simple view that change the its origin through animation.
See the result here
The code:
class CustomTabBar: UITabBar {
/// The corner radius value for the top-left, top-right corners of the TabBar
var borderRadius: CGFloat = 0
/// Who is containing the selector. Is a subview of the TabBar.
private var selectorParent: UIView?
/// Who moves itself following the current section. Is a subview of ```selectorParent```.
private var selector: UIView?
/// The height of the ```selector```
private var selectorHeight: CGFloat = 5
/// The number of sections handled by the TabBarController.
private var numberOfSections: Int = 0
override func layoutSubviews() {
super.layoutSubviews()
isTranslucent = false
backgroundColor = UIColor.white
tintColor = AZTheme.PaletteColor.primaryColor
shadowImage = nil
layer.masksToBounds = false
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.1
layer.shadowOffset = CGSize(width: 0, height: -1)
layer.shadowRadius = 10
}
}
// MARK: - Private functions
extension CustomTabBar {
/// Create the selector element on the top of the TabBar
func setupSelectorView(numberOfSections: Int) {
self.numberOfSections = numberOfSections
// delete previous subviews (if exist)
if let selectorContainer = self.selectorParent {
selectorContainer.removeFromSuperview()
self.selector?.removeFromSuperview()
self.selectorParent = nil
self.selector = nil
}
// SELECTOR CONTAINER
let selectorContainerRect: CGRect = CGRect(x: 0,
y: -borderRadius,
width: frame.width,
height: borderRadius)
let selectorContainer = UIView(frame: selectorContainerRect)
selectorContainer.backgroundColor = UIColor.white
selectorContainer.AZ_roundCorners([.topLeft, .topRight], radius: borderRadius)
selectorContainer.layer.masksToBounds = true
self.addSubview(selectorContainer)
// SELECTOR
let selectorRect: CGRect = CGRect(x: 0,
y: 0,
width: selectorContainer.frame.width / CGFloat(numberOfSections),
height: selectorHeight)
let selector = UIView(frame: selectorRect)
selector.backgroundColor = AZTheme.PaletteColor.primaryColor
selectorContainer.addSubview(selector)
// add views to hierarchy
self.selectorParent = selectorContainer
self.selector = selector
}
/// Animate the position of the selector passing the index of the new section
func animateSelectorTo(sectionIndex: Int) {
guard let selectorContainer = self.selectorParent, let selector = self.selector else { return }
selector.layer.removeAllAnimations()
let sectionWidth: CGFloat = selectorContainer.frame.width / CGFloat(numberOfSections)
let newCoord = CGPoint(x: sectionWidth * CGFloat(sectionIndex), y: selector.frame.origin.y)
UIView.animate(withDuration: 0.25, delay: 0, options: UIView.AnimationOptions.curveEaseOut, animations: { [weak self] in
self?.selector?.frame.origin = newCoord
}, completion: nil)
}
}

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

Draw half circuler image like meter using UIBezierPath...!

My View Controller code contains below code
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let width: CGFloat = 240
let height: CGFloat = 240
let demoView = DemoView(frame: CGRect(x: self.view.frame.size.width/2 - width/2,
y: self.view.frame.size.height/2 - height/2,
width: width,
height: height))
let subView = UIView.init(frame: (CGRect(x: demoView.frame.origin.x - width,
y: demoView.frame.origin.y,
width: width * 2,
height: height * 2)))
self.view.addSubview(demoView)
self.view.addSubview(subView)
subView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
subView.layer.cornerRadius = subView.frame.size.height / 2
}
}
import UIKit
class DemoView: UIView {
var path: UIBezierPath!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.darkGray
}
override func draw(_ rect: CGRect) {
self.createTriangle()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func createTriangle() {
let count : Int = 9
let gap : CGFloat = 3
let yValue : CGFloat = CGFloat(self.frame.size.width - ((CGFloat(count - 1)) * gap)) / CGFloat(count);
for a in 0 ..< count {
let i : CGFloat = CGFloat(a)
let path1: UIBezierPath! = UIBezierPath()
path1.move(to: CGPoint(x: 0.0, y: self.frame.size.height))
path1.addLine(to: CGPoint(x: (yValue * i) > 0 ? (yValue * i) + i*gap : 0, y: (yValue * i) > 0 ? (yValue * i) + i*gap : 0))
path1.addLine(to: CGPoint(x:yValue * (i+1) + i*gap, y: yValue * (i+1) + i*gap))
path1.close()
UIColor.orange.setFill()
path1.fill()
}
}
}
Can anyone help me to achieve this thing?
Edit image :
Is this the result your want ?
The way I did it isn't with triangles but Arcs.
Add createPie() in your DemoView class and call it in draw(:) instead of your createTriangle().
This is my code:
func createPie() {
// 2 vars to configure width of gap/banches
var branchAmount = 10
var gapAngle = CGFloat.pi / 100
let startAngle = 3 * CGFloat.pi / 2
let endAngle = 2 * CGFloat.pi
let branchAngle = (endAngle - startAngle - (CGFloat(branchAmount) - 1) * gapAngle) / CGFloat(branchAmount)
let paths = UIBezierPath()
for i in 0..<branchAmount {
paths.move(to: CGPoint(x: 0.0, y: self.frame.size.height))
paths.addArc(withCenter: CGPoint(x: 0, y: self.frame.size.height),
radius: self.frame.size.height,
startAngle: startAngle + CGFloat(i) * (branchAngle + gapAngle),
endAngle: startAngle + CGFloat(i) * (branchAngle + gapAngle) + branchAngle,
clockwise: true)
}
paths.close()
UIColor.orange.setFill()
paths.fill()
}
Cheers!
EDIT: If you want to add a circular mask you and add this in the end of createPie() (which is no longer really a pie now..):
// Circular mask
let maskLayer = CAShapeLayer()
let maskPath = UIBezierPath(rect: bounds)
maskLayer.fillRule = kCAFillRuleEvenOdd // Circle will be substracted to the mask thanks to this
maskPath.move(to: CGPoint(x: 0.0, y: frame.size.height))
maskPath.addArc(withCenter: CGPoint(x: 0, y: frame.size.height), radius: maskRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
maskLayer.path = maskPath.cgPath
layer.mask = maskLayer
It just adds mask composed of the subtraction of bounds and the circle of origin (0, height)

Resources