How to Build a Gradient Border Voting Button with Custom Animations on iOS
This article walks through the design and implementation of a voting component featuring a transparent gradient border and gradient‑filled irregular button, covering three solution options, detailed path calculations, CoreGraphics and CoreAnimation techniques, and complete Swift code for drawing and animating the UI.
Requirement
The component must display a voting option as an irregular button whose border is a transparent gradient and whose background is a gradient fill, with a smooth animation when the user taps it.
Solution Comparison
GIF – Easy to use but consumes large file size and offers little customization.
Lottie – More flexible; requires a designer to provide an animation JSON file.
Code drawing – Most flexible and lightweight, but the implementation is more complex.
Considering package size, collaboration cost, and animation complexity, the code‑drawing approach was chosen.
Path Analysis
The outer black line represents the visible area of the GradientLayer mask (the ShapeLayer path for the fill).
The inner yellow line is the visible area of the border GradientLayer mask (the ShapeLayer path for the stroke).
The blue line indicates the radius; the outer radius is larger than the inner radius by borderWidth/2.
Because a stroke is centered on its path, the border must be offset by borderWidth/2 to keep it fully inside the desired area.
Both black and yellow lines share the same corner center point.
Border width is 12 pt, left circle radius is half the height (36 pt), corner radii on the right are 8 pt, resulting in left radius 30 pt and right radius 2 pt.
From the diagram we need to compute three key points for the path:
left = (height/2, height/2)
rightTop = (width - radius / tan(angle/2), radius)
rightBottom = (width - height / tan(angle) - radius * tan(angle/2), height - radius)Key Knowledge Points
CALayer mask – A mask layer defines the visible region of its parent layer; the mask’s color is irrelevant, only its shape matters.
anchorPoint – Determines the reference point for all layer transformations. Default is (0.5, 0.5). Setting it to (0, 0) or (1, 0) makes the left or right option shrink toward its respective edge.
stroke vs. fill – stroke draws a line centered on the path (requires offset), while fill fills the interior bounded by the path.
CoreGraphics – The low‑level graphics library providing CGPoint, CGRect, CGSize, etc.
QuartzCore / CoreAnimation – Provides layer‑based animation APIs such as CAKeyframeAnimation and CAMediaTimingFunction. Animations are applied to CALayer, not UIView.
UIBezierPath – UIKit wrapper around CGPath for creating circles, arcs, rectangles, and custom shapes.
Drawing the Path
private func drawBezierPath(rect: CGRect, smallAngle: CGFloat, shape: Bool) -> UIBezierPath {
let bezierPath = UIBezierPath()
let circleRadius = rect.height / 2
let leftTop = state == .left ? circleRadius : bigRadius
let rightTop = state == .left ? smallRadius : circleRadius
let rightBottom = state == .left ? bigRadius : circleRadius
let leftBottom = state == .left ? circleRadius : smallRadius
let radiusOffset = shape ? 0 : borderWidth / 2
// calculate points based on state …
// add arcs and lines
return bezierPath
}Animating the Component
The animation consists of three stages: initial shape, narrowed intermediate shape, and final shape. Each stage updates the path and bounds of the shape and border layers.
public func startAnimate() {
let timingFunctions = [CAMediaTimingFunction(name: .linear), CAMediaTimingFunction(name: .linear)]
let shapeKeyFrameAnimation = CAKeyframeAnimation(keyPath: "path")
shapeKeyFrameAnimation.duration = 2
shapeKeyFrameAnimation.fillMode = .forwards
shapeKeyFrameAnimation.isRemovedOnCompletion = false
shapeKeyFrameAnimation.values = [shapePath, middleShapePath, endShapePath]
shapeKeyFrameAnimation.timingFunctions = timingFunctions
shaperLayer.removeAllAnimations()
shaperLayer.add(shapeKeyFrameAnimation, forKey: "shapeAnimation")
let borderKeyFrameAnimation = CAKeyframeAnimation(keyPath: "path")
borderKeyFrameAnimation.duration = 2
borderKeyFrameAnimation.fillMode = .forwards
borderKeyFrameAnimation.isRemovedOnCompletion = false
borderKeyFrameAnimation.values = [borderPath, middleBorderPath, endBorderPath]
borderKeyFrameAnimation.timingFunctions = timingFunctions
borderLayer.removeAllAnimations()
borderLayer.add(borderKeyFrameAnimation, forKey: "borderAnimation")
let shapeGradientKeyFrameAnimation = CAKeyframeAnimation(keyPath: "bounds")
shapeGradientKeyFrameAnimation.duration = 2
shapeGradientKeyFrameAnimation.fillMode = .forwards
shapeGradientKeyFrameAnimation.isRemovedOnCompletion = false
shapeGradientKeyFrameAnimation.values = [startBounds, middleBounds, endBounds]
shapeGradientKeyFrameAnimation.timingFunctions = timingFunctions
shapeGradientLayer.removeAllAnimations()
shapeGradientLayer.add(shapeGradientKeyFrameAnimation, forKey: "shapeGradientAnimation")
borderGradientLayer.removeAllAnimations()
borderGradientLayer.add(shapeGradientKeyFrameAnimation, forKey: "shapeGradientAnimation")
}Conclusion
The problem reduces to precise geometric calculations; drawing helper lines greatly improves development efficiency. The demo code shows a functional prototype, while a production version would need to handle edge cases such as 100 %/0 % voting ratios, extreme width ratios, text layout, and tap handling.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
