Trim UIView with 2 arcs - ios

I have a UIView and I want to trim it with two circles, like I've drawn(sorry for the quality).
My code:
final class TrimmedView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
let size = CGSize(width: 70, height: 70)
let innerRadius: CGFloat = 366.53658283002471
let innerBottomRadius: CGFloat = 297.88543112651564
let path = UIBezierPath()
path.move(to: CGPoint(x: -innerRadius + (size.width / 2), y: innerRadius))
path.addArc(withCenter: CGPoint(x: size.width / 2, y: innerRadius), radius: innerRadius, startAngle: CGFloat.pi, endAngle: 0, clockwise: true)
path.move(to: CGPoint(x: -innerBottomRadius + (size.width / 2), y: innerBottomRadius))
path.addArc(withCenter: CGPoint(x: size.width / 2, y: innerBottomRadius), radius: innerBottomRadius, startAngle: 0, endAngle: CGFloat.pi, clockwise: true)
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.shadowPath = path.cgPath
layer.mask = shapeLayer
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
ViewController:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let view = UIView(frame: CGRect(origin: CGPoint(x: (self.view.bounds.width - 70) / 2, y: (self.view.bounds.height - 70) / 2), size: CGSize(width: 70, height: 70)))
view.backgroundColor = .red
self.view.addSubview(view)
let view1 = TrimmedView(frame: view.frame)
view1.backgroundColor = .yellow
self.view.addSubview(view1)
}
I got this result. It seems for me that top trimming works but the bottom doesn't and I don't know why. Any help would be appreciated. Thanks.

Here is a custom view that should give you what you want.
The UIBezierPath uses QuadCurves for the top "convex" arc and the bottom "concave" arc.
It is marked #IBDesignable so you can see it at design-time in IB / Storyboard. The "height" of the arc and the fill color are each set as #IBInspectable so you can adjust those values at design-time as well.
To use it in Storyboard:
Add a normal UIView
change the Class to BohdanShapeView
in the Attributes Inspector pane, set the Arc Offset and the Fill Color
set the background color as with a normal view (you'll probably use clear)
Result:
To use it via code:
let view1 = BohdanShapeView(frame: view.frame)
view1.fillColor = .systemTeal
view1.arcOffset = 10
self.view.addSubview(view1)
Here is the class:
#IBDesignable
class BohdanShapeView: UIView {
#IBInspectable var arcOffset: CGFloat = 0.0
#IBInspectable var fillColor: UIColor = UIColor.white
let shapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add the shape layer
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
// fill color for the shape
shapeLayer.fillColor = self.fillColor.cgColor
let width = bounds.size.width
let height = bounds.size.height
let bezierPath = UIBezierPath()
// start at arcOffset below top-left
bezierPath.move(to: CGPoint(x: 0.0, y: 0.0 + arcOffset))
// add curve to arcOffset below top-right
bezierPath.addQuadCurve(to: CGPoint(x: width, y: 0.0 + arcOffset), controlPoint: CGPoint(x: width * 0.5, y: 0.0 - arcOffset))
// add line to bottom-right
bezierPath.addLine(to: CGPoint(x: width, y: height))
// add curve to bottom-left
bezierPath.addQuadCurve(to: CGPoint(x: 0.0, y: height), controlPoint: CGPoint(x: width * 0.5, y: height - arcOffset * 2.0))
// close the path
bezierPath.close()
shapeLayer.path = bezierPath.cgPath
}
}

Related

How to draw a curve like this in UIBezierPath Swift?

I am trying to develop a screen whose background looks like this:
Here I am trying to develop the gray curved background and it fills the lower part of the screen as well. I'm very new to UIBezierPath and I've tried this:
class CurvedView: UIView {
//MARK:- Data Types
//MARK:- View Setup
override func draw(_ rect: CGRect) {
let fillColor: UIColor = .blue
let path = UIBezierPath()
let y:CGFloat = 0
print(rect.height)
print(rect.width)
path.move(to: CGPoint(x: .zero, y: 100))
path.addLine(to: CGPoint(x: 60, y: 100))
path.addCurve(to: .init(x: 100, y: 0), controlPoint1: .init(x: 125, y: 80), controlPoint2: .init(x: 50, y: 80))
path.close()
fillColor.setFill()
path.fill()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .init(hex: "#dfe1e3")
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = .init(hex: "#dfe1e3")
}
}
This code gave me this:
I followed a lot of tutorials but I didn't get the exact understanding. I understood that for this curve I have to move to (0,100) and then add a line and then add a curve and ten extend the line add a curve then straight line lower curve and then straight line and close. But, when I started as you can see the blue line didn't cover the upper part. Can any one please help me?
Here some example that I create, you can change the value to make it more similar to what you want
Here a guide how control point in a curve work
Note: I called this code in viewDidload
let path = UIBezierPath()
let fillColor = UIColor.blue
let y: CGFloat = UIScreen.main.bounds.size.height
let x: CGFloat = UIScreen.main.bounds.size.width
let height: CGFloat = 200
path.move(to: CGPoint(x: 0, y: y)) // bottom left
path.addLine(to: CGPoint(x: 0, y: y - 20)) // top left
path.addCurve(to: CGPoint(x: x, y: y - height), controlPoint1: CGPoint(x: x * 2 / 3, y: y), controlPoint2: CGPoint(x: x * 5 / 6, y: y - height * 6 / 5)) // curve to top right
path.addLine(to: CGPoint(x: x, y: y)) // bottom right
path.close() // close the path from bottom right to bottom left
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = fillColor.cgColor
view.layer.addSublayer(shapeLayer)
in reference to #aiwiguna
class CurvedView: UIView {
//MARK:- Data Types
//MARK:- View Setup
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
let fillColor = UIColor.blue
let y: CGFloat = UIScreen.main.bounds.size.height
let x: CGFloat = UIScreen.main.bounds.size.width
let height: CGFloat = 200
path.move(to: CGPoint(x: 0, y: y)) // bottom left
path.addLine(to: CGPoint(x: 0, y: y - 20)) // top left
path.addCurve(to: CGPoint(x: x, y: y - height), controlPoint1: CGPoint(x: x * 2 / 3, y: y), controlPoint2: CGPoint(x: x * 5 / 6, y: y - height * 6 / 5)) // curve to top right
path.addLine(to: CGPoint(x: x, y: y)) // bottom right
path.close() // close the path from bottom right to bottom left
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = fillColor.cgColor
path.close()
fillColor.setFill()
path.fill()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .red
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = .yellow
}
}

Suggestions regarding UIImageView customization

I'm trying to figure out the best way to recreate this image in code. I've thought about taking two UIImageViews and connecting them via constraints but that would only get me 50% of the way there because there wouldn't be a diagonal white line splitting the two unique colors. I also want to be able to programmatically change the color of each half of the UIImageView.
My class was very similar to what "May Rest in Peace" posted, but since I had put it together already, I'll go ahead and post it.
The main difference is that I implemented #IBDesignable and #IBInspectable so you can see it and make adjustments in Storyboard / IB
#IBDesignable
class AaronView: UIView {
let leftLayer: CAShapeLayer = CAShapeLayer()
let rightLayer: CAShapeLayer = CAShapeLayer()
let maskLayer: CAShapeLayer = CAShapeLayer()
#IBInspectable
var leftColor: UIColor = UIColor(red: 0.5, green: 0.6, blue: 0.8, alpha: 1.0) {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var rightColor: UIColor = UIColor(red: 0.0, green: 0.5, blue: 0.4, alpha: 1.0) {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var divColor: UIColor = UIColor.white {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var divAngle: CGFloat = 5.0 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var divWidth: CGFloat = 8.0 {
didSet {
setNeedsLayout()
}
}
#IBInspectable
var radius: CGFloat = 32.0 {
didSet {
setNeedsLayout()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(leftLayer)
layer.addSublayer(rightLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
let x1 = bounds.minX
let y1 = bounds.minY
let x2 = bounds.maxX
let y2 = bounds.maxY
var path = UIBezierPath()
let offset = (bounds.width / 2) * tan(divAngle * CGFloat.pi / 180)
path.move(to: CGPoint(x: x1, y: y1))
path.addLine(to: CGPoint(x: x2 / 2.0 - divWidth / 2.0 + offset, y: y1))
path.addLine(to: CGPoint(x: x2 / 2.0 - divWidth / 2.0 - offset, y: y2))
path.addLine(to: CGPoint(x: x1, y: y2))
path.close()
leftLayer.path = path.cgPath
path = UIBezierPath()
path.move(to: CGPoint(x: x2 / 2.0 + divWidth / 2.0 + offset, y: y1))
path.addLine(to: CGPoint(x: x2, y: y1))
path.addLine(to: CGPoint(x: x2, y: y2))
path.addLine(to: CGPoint(x: x2 / 2.0 + divWidth / 2.0 - offset, y: y2))
path.close()
rightLayer.path = path.cgPath
leftLayer.fillColor = leftColor.cgColor
rightLayer.fillColor = rightColor.cgColor
maskLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: radius).cgPath
layer.mask = maskLayer
backgroundColor = divColor
}
}
Using Defaults:
Result:
and some changes:
Result:
A UIImageView holds a static bitmap image. You could just generate an image like that, save it as a JPEG/PNG/TIF, and load the image into a UIImageView as a bitmap. That doesn't sound like what you want however.
I'd suggest creating a custom subclass of UIView. From there you could go a couple of different ways.
You could have your view override the draw() method for UIView and use Core Graphics calls to draw into the graphics context. Core Graphics is pretty specialized and will require some research to get the hang of.
You could have your custom view add Core Animation (CA) layers that draw your shapes for you. The class CAShapeLayer would be a good choice for this. You'll need to read up on CALayers and how to use them (which is also fairly arcane bit of learning.)
In general Apple steers you towards using layers and letting the system do the rendering for you. That's probably how I would do this. (Using CAShapeLayers, which in turn use CGPath objects.)
I created a custom view based on what you need based on #DuncanC's suggestions
class AngledSplitView: UIView {
var leftLayer: CAShapeLayer!
var rightLayer: CAShapeLayer!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
init (frame: CGRect,
leftColor: UIColor,
rightColor: UIColor,
separatorWidth: CGFloat,
separatorAngleInDegrees: CGFloat) {
super.init(frame: frame)
setupViews(leftColor: leftColor,
rightColor: rightColor,
separatorWidth: separatorWidth,
separatorAngleInDegrees: separatorAngleInDegrees)
}
func setupViews(leftColor: UIColor,
rightColor: UIColor,
separatorWidth: CGFloat,
separatorAngleInDegrees: CGFloat) {
// sets the image's frame to fill our view
createLeftView(leftColor: leftColor, separatorWidth: separatorWidth, separatorAngleInDegrees: separatorAngleInDegrees)
createRightView(rightColor: rightColor, separatorWidth: separatorWidth, separatorAngleInDegrees: separatorAngleInDegrees)
}
func setLeftColor(leftColor: UIColor) {
leftLayer.fillColor = leftColor.cgColor
}
func setRightColor(rightColor: UIColor) {
rightLayer.fillColor = rightColor.cgColor
}
func createLeftView(leftColor: UIColor,
separatorWidth: CGFloat,
separatorAngleInDegrees: CGFloat) {
let path = UIBezierPath()
let leftLayer = CAShapeLayer()
let offset = (bounds.height / 2) * tan(separatorAngleInDegrees * CGFloat.pi / 180)
path.move(to: bounds.origin)
path.addLine(to: CGPoint(x: bounds.width / 2 - separatorWidth / 2 + offset,
y: bounds.origin.y))
path.addLine(to:CGPoint(x: bounds.width / 2 - separatorWidth / 2 - offset,
y: bounds.height))
path.addLine(to:CGPoint(x: bounds.origin.x, y: bounds.height))
path.addLine(to:bounds.origin)
path.close()
leftLayer.path = path.cgPath
leftLayer.fillColor = leftColor.cgColor
self.layer.addSublayer(leftLayer)
}
func createRightView(rightColor: UIColor,
separatorWidth: CGFloat,
separatorAngleInDegrees: CGFloat) {
let path = UIBezierPath()
let rightLayer = CAShapeLayer()
let offset = (bounds.height / 2) * tan(separatorAngleInDegrees * CGFloat.pi / 180)
path.move(to: CGPoint(x: bounds.width / 2 + separatorWidth / 2 + offset,
y: bounds.origin.y))
path.addLine(to: CGPoint(x: bounds.width / 2 + separatorWidth / 2 + offset,
y: bounds.origin.y))
path.addLine(to:CGPoint(x: bounds.width / 2 + separatorWidth / 2 - offset,
y: bounds.height))
path.addLine(to:CGPoint(x: bounds.width, y: bounds.height))
path.addLine(to:CGPoint(x: bounds.width, y: bounds.origin.y))
path.close()
rightLayer.path = path.cgPath
rightLayer.fillColor = rightColor.cgColor
self.layer.addSublayer(rightLayer)
}
}
You can use it like this:
let customView = AngledSplitView(
frame: CGRect(x: 20, y: 30, width: view.frame.width - 40, height:
view.frame.height / 4),
leftColor: .red,
rightColor: .blue,
separatorWidth: 20,
separatorAngleInDegrees: 45)
view.addSubview(customView)

How to draw a custom rounded rectangle in SWIFT?

I'm trying to draw a shape shown on the upper image programmatically.
This shape has custom rounded corners.
view.layer.cornerRadius = some value less than half diameter
This didn't work. Setting cornerRadius draws straight lines on every side(as seen on the bottom image) but the shape I'm trying to draw has no straight lines at all and it's not an oval.
I also tried below without luck. This code just draws an oval.
var path = UIBezierPath(ovalIn: CGRect(x: 000, y: 000, width: 000, height: 000))
I believe this can not be done by setting cornerRadius.
There should be something more.
I have no idea what class should I use and how.
Please anybody give me some direction.
Thanks!
I accomplished this by drawing a quadratic.
let dimention: CGFloat = some value
let path = UIBezierPath()
path.move(to: CGPoint(x: dimension/2, y: 0))
path.addQuadCurve(to: CGPoint(x: dimension, y: dimension/2),
controlPoint: CGPoint(x: dimension, y: 0))
path.addQuadCurve(to: CGPoint(x: dimension/2, y: dimension),
controlPoint: CGPoint(x: dimension, y: dimension))
path.addQuadCurve(to: CGPoint(x: 0, y: dimension/2),
controlPoint: CGPoint(x: 0, y: dimension))
path.addQuadCurve(to: CGPoint(x: dimension/2, y: 0),
controlPoint: CGPoint(x: 0, y: 0))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.position = CGPoint(x: 0, y: 0)
self.someView.layer.addSublayer(shapeLayer)
You have to add cipsToBounds to the code for getting the image with given cornerRadius.
Try the blow codes ,
view.layer.cornerRadius = some value
view.clipsToBounds = true
You can use following path for your custom shape
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 150, height: 150), byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 8, height: 8))
You can change width and height according to your requirements and UI
class RoundView: UIView {
var roundCorner: UIRectCorner? = nil
var roundRadius:CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed(“nib name”, owner: self, options: nil))
addSubview(contentView)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
isUserInteractionEnabled = true
}
override func layoutSubviews() {
super.layoutSubviews()
if roundCorner != nil {
// self.roundCorners([.topRight, .bottomLeft, .bottomRight], radius: 15)
self.roundCorners(roundCorner!, radius: roundRadius)
}
}
}

How Do i create UIView like in the image?

i want to add curve in my UIView as shown in image.
How can i create such uiview?
You can use UIBezierPath and use addCurve method to create your view.
//1. Create this new Class
class ComplexView: UIView {
var path: UIBezierPath!
override init(frame: CGRect) {
super.init(frame: frame)
self.alpha = 0.3
complexShape()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
// Specify the fill color and apply it to the path.
UIColor.blue.setFill()
path.fill()
// Specify a border (stroke) color.
UIColor.magenta.setStroke()
path.stroke()
}
func complexShape() {
path = UIBezierPath()
path.move(to: CGPoint(x: 0.0, y: 0.0))
path.addCurve(to: CGPoint(x: 0, y: self.frame.size.height),
controlPoint1: CGPoint(x: 50.0, y: 25.0),
controlPoint2: CGPoint(x: 50.0, y: self.frame.size.height - 25.0))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
self.backgroundColor = UIColor.orange
self.layer.mask = shapeLayer
}
}
In your ViewController call this View and add this view to your main view.
//2. In you Viewcontoller add ComplexView
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let width: CGFloat = 100.0
let height: CGFloat = 500.0
let complexView = ComplexView(frame: CGRect(x: 0,
y: self.view.frame.size.height/2 - height/2,
width: width,
height: height))
self.view.addSubview(complexView)
}
You need to play around addCurve method to get your desired shape.

Drawing circles proportionally to containing view in swift

I am trying to draw 3 circles inside my 3 views but only the top view circle is drawn even doth the code is the same. I can't seem to understand where is the problem, that's why the two other circle are not there?
class ViewController: UIViewController {
///Views used to display the progress view
var topView: CircleView!
var middleView: CircleView!
var bottomView: CircleView!
override func viewDidLoad() {
super.viewDidLoad()
createThreeViews()
}
func createThreeViews(){
let viewHeight = self.view.bounds.height
let viewWidth = self.view.bounds.width
//Top View
let topTimerFrame = CGRect(x: 0, y: 0, width: viewWidth, height: 3/6 * viewHeight)
topView = CircleView(frame: topTimerFrame)
topView.backgroundColor = UIColor.redColor()
//Middle View
let middleTimerFrame = CGRect(x: 0, y: topTimerFrame.height, width: viewWidth, height: 2/6 * viewHeight)
middleView = CircleView(frame: middleTimerFrame)
middleView.backgroundColor = UIColor.blueColor()
//Bottom view
let bottomTimerFrame = CGRect(x: 0, y: topTimerFrame.height + middleTimerFrame.height, width: viewWidth, height: 1/6 * viewHeight)
bottomView = CircleView(frame: bottomTimerFrame)
bottomView.backgroundColor = UIColor.greenColor()
//add top circle and set constraint
self.view.addSubview(topView)
//add middle circle and set constraints
self.view.addSubview(middleView)
//add bottom circle and set constraints
self.view.addSubview(bottomView)
}
}
//class used to create the views and draw circles in them
class CircleView: UIView {
let π:CGFloat = CGFloat(M_PI)
let circle = CAShapeLayer()
var secondLayerColor: UIColor = UIColor.whiteColor()
//custom initializer
override init(frame: CGRect) {
super.init(frame: frame)
userInteractionEnabled = true
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
userInteractionEnabled = true
setup()
}
func setup() {
//draw the circle and add to layer
circle.frame = bounds
circle.lineWidth = CGFloat(4)
circle.fillColor = UIColor.whiteColor().CGColor
circle.strokeEnd = 1
layer.addSublayer(circle)
setupShapeLayer(circle)
}
override func layoutSubviews() {
super.layoutSubviews()
setupShapeLayer(circle)
}
func setupShapeLayer(shapeLayer: CAShapeLayer) {
shapeLayer.frame = bounds
let radius = frame.height/2 - circle.lineWidth/2
let startAngle = CGFloat(0)
let endAngle = 2*π
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
shapeLayer.path = path.CGPath
}
}
You are drawing your circle with an offset to your frame (the other two circles are drawn, but outside of the frames).
Change:
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
to:
let path = UIBezierPath(arcCenter: CGPoint(x: center.x , y: frame.height/2) , radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
Btw. you probably want to change setupShapeLayer(circle) in setup() to setNeedsLayout(); layoutIfNeeded(), otherwise it will be drawn twice the first time.

Resources