UIBezierPath is a UIKit class that encapsulates a vector path for 2D graphics in iOS. It combines geometric path description with drawing capabilities, allowing you to create lines, Bezier curves, arcs, and rectangles. Unlike CGPath, UIBezierPath contains its own drawing methods that work directly in the current graphics context. According to Apple Developer Documentation, 2026, UIBezierPath supports all standard Quartz 2D operations in an object-oriented wrapper.
Key Takeaways
UIBezierPath is a UIKit framework class that inherits from NSObject and implements NSCopying, NSSecureCoding. It provides a high-level Objective-C/Swift API for creating and drawing vector paths. Internally, UIBezierPath stores a reference to CGPathRef — a Core Graphics object — but adds convenient methods for drawing, geometry modification, and style management.
The main advantage of UIBezierPath over using CGPath directly is the built-in drawing methods. Calling fill() or stroke() on a UIBezierPath instance automatically configures the context and draws the path. The class also supports lineWidth, lineCapStyle, lineJoinStyle, miterLimit, and flatness properties, allowing you to control appearance without a separate style storage object.
UIBezierPath is used in most iOS applications for custom drawing inside UIView: from simple icons to complex animated charts. The draw(_:) method of the UIView class is the primary place where developers create and draw UIBezierPath. The class is fully compatible with Core Animation and can be used for path animation via CAShapeLayer and CABasicAnimation.
The move(to:) method moves the path starting point to the specified coordinates without drawing. This is a required call before building any segment. addLine(to:) adds a straight segment from the current position to the specified point. The close() method closes the contour by drawing a line from the last point back to the first point of the last move(to:).
UIBezierPath provides several convenient initializers for ready-made shapes. init(rect:) creates a rectangle, init(roundedRect:cornerRadius:) creates a rectangle with rounded corners. The UIRectCorner parameter allows rounding only selected corners, such as topLeft and bottomRight, leaving the others straight. This is widely used for creating custom buttons and cards.
init(ovalIn:) creates an ellipse inscribed in the specified rectangle. If the rectangle is a square, the ellipse becomes a circle. init(roundedRect:byRoundingCorners:cornerRadii:) is the most flexible option, allowing different radii for each corner. This initializer was introduced in iOS 6 and remains the standard for creating rounded containers.
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 50))
path.addLine(to: CGPoint(x: 200, y: 50))
path.addLine(to: CGPoint(x: 200, y: 150))
path.addLine(to: CGPoint(x: 125, y: 200))
path.addLine(to: CGPoint(x: 50, y: 150))
path.close()
addQuadCurve(to:controlPoint:) adds a quadratic Bezier curve with one control point. This type of curve is used for simple smooth bends when minimizing computation. A quadratic curve is always a parabola and is guaranteed to lie within the triangle formed by the start, control, and end points.
addCurve(to:controlPoint1:controlPoint2:) adds a cubic Bezier curve with two control points. Cubic curves are used in all modern vector editors and TrueType fonts. Two control points provide more degrees of freedom for precise shape control. This method is recommended for building organic and complex contours.
When constructing curves, it is important to maintain the continuity principle — smoothness at the connection point of segments. For C1 continuity (tangent), the control point of the next curve must lie on the same line as the control point of the previous one. For C2 continuity (curvature), more complex coordination is required, which is automatically provided in CAShapeLayer when animating between two UIBezierPath objects with the same number of control points.
| Method | Type | Control Points | Degree |
|---|---|---|---|
| addQuadCurve | Quadratic Bezier | 1 | 2 |
| addCurve | Cubic Bezier | 2 | 3 |
| addArc | Arc | — | — |
addArc(withCenter:radius:startAngle:endAngle:clockwise:) adds a circular arc with the specified parameters. Angles are specified in radians relative to the circle center. Zero angle corresponds to the rightward direction (3 o'clock on a clock face). The clockwise parameter determines the direction: true for clockwise, false for counterclockwise. Arcs are essential for pie charts, speedometers, and interactive progress rings.
For a full circle, use startAngle = 0 and endAngle = .pi * 2. To create a sector (as part of a pie chart), use addArc followed by addLine(to:) to the center and close(). UIBezierPath does not have a built-in addCircle method — a circle is created via init(ovalIn:) with a square rectangle or via addArc with a full angle.
The addArc(withCenter:radius:startAngle:endAngle:clockwise:) method automatically performs move(to:) to the arc starting point if the current position does not match it. If the current position differs, a straight line is drawn between it and the start of the arc. This behavior differs from Core Graphics, where an explicit move(to:) is required before each new contour.
let center = CGPoint(x: 150, y: 150)
let slice = UIBezierPath()
slice.move(to: center)
slice.addArc(
withCenter: center,
radius: 100,
startAngle: 0,
endAngle: .pi * 0.75,
clockwise: true
)
slice.close()
fill() fills the interior of the path with the current fillColor or the color set in the graphics context via setFill(). For self-intersecting paths, the fill rule is determined by the usesEvenOddFillRule property. If set to true, the even-odd rule is used: a point is considered inside the shape if a ray from it crosses the contour an odd number of times.
stroke() draws the path contour with the current lineWidth, lineCapStyle, and lineJoinStyle settings. lineCapStyle defines the shape of open segment ends: .butt (straight cut), .round (semicircular), .square (square with projection). lineJoinStyle defines the shape of joint corners: .miter (sharp), .round (rounded), .bevel (cut off).
For dashed lines, the setLineDash(_:count:phase:) property is used. It accepts an array of alternating dash and gap lengths, the count of elements, and the initial phase. Dash rendering is performed during path rasterization — complex patterns do not increase the geometric complexity of the contour. CAShapeLayer supports animation of the strokeEnd property for a “drawing” effect from start to finish.
override func draw(_ rect: CGRect) {
let path = UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 200, height: 200))
UIColor.blue.setFill()
UIColor.black.setStroke()
path.lineWidth = 4
path.fill()
path.stroke()
}
Let's create a custom progress indicator in the form of a ring using UIBezierPath. The ring is built from two arcs of the same radius with different stroke colors. The first arc is a full circle in gray (background), the second is a segment showing progress. For animation, CAShapeLayer and CABasicAnimation are used.
class RingView: UIView {
private let progressLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = bounds.width * 0.4
let lineWidth: CGFloat = 12
let backgroundRing = UIBezierPath()
backgroundRing.addArc(withCenter: center, radius: radius,
startAngle: -.pi / 2, endAngle: .pi * 1.5, clockwise: true)
progressLayer.path = backgroundRing.cgPath
progressLayer.strokeColor = UIColor.systemBlue.cgColor
progressLayer.lineWidth = lineWidth
progressLayer.lineCap = .round
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeEnd = 0
layer.addSublayer(progressLayer)
}
func animateProgress(to value: CGFloat) {
CABasicAnimation(keyPath: "strokeEnd").apply {
$0.toValue = value
$0.duration = 1.5
$0.timingFunction = CAMediaTimingFunction(name: .easeOut)
progressLayer.add($0, forKey: nil)
}
progressLayer.strokeEnd = value
}
}
In this example, UIBezierPath is used to create a circular contour, which is then passed to CAShapeLayer via the cgPath property. Animating strokeEnd from 0 to 1 visualizes the ring filling. This approach is used in loading indicators, fitness trackers, and progress widgets. The layoutSubviews method ensures path reconstruction when the View size changes.
lineCap = .round adds rounded ends to the arc, giving the indicator a neater look. The strokeEnd property is an animatable property of CAShapeLayer that does not require redrawing the entire contour each frame. Animation runs entirely on the GPU through Core Animation, ensuring 60 FPS with no CPU load.
Frequently Asked Questions
UIBezierPath is an object-oriented wrapper around CGPath that includes built-in fill() and stroke() methods. CGPath is an immutable C object from Core Graphics that requires explicit drawing via CGContext. UIBezierPath is more convenient for UIKit, while CGPath is more performant for reuse.
Use init(roundedRect:byRoundingCorners:cornerRadii:) with the .topLeft and .topRight parameters for roundedRect. This is the only UIBezierPath method that allows selectively rounding corners without creating a complex composite path.
The cgPath property returns an immutable CGPathRef that can be passed to CAShapeLayer or used with different colors and thicknesses. Modifications to UIBezierPath (point movement) do not affect the previously obtained cgPath until explicitly updated.
Check usesEvenOddFillRule — if the contour has self-intersections, the even-odd rule may leave areas unfilled. Set usesEvenOddFillRule = false for the standard non-zero winding fill rule.
Use CABasicAnimation with the key path "path" on CAShapeLayer. Both path states must contain the same number of segments and control points — otherwise the animation will be jerky or fail.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also