Skip to content

Instantly share code, notes, and snippets.

@kvdesa
Created April 27, 2021 08:19
Show Gist options
  • Save kvdesa/5e6a68cc7c7e907bdcfb9dc077c92747 to your computer and use it in GitHub Desktop.
Save kvdesa/5e6a68cc7c7e907bdcfb9dc077c92747 to your computer and use it in GitHub Desktop.
class CurvedView: UIView {
// MARK: - Constants
private enum Constants {
static let defaultCurvePercent: CGFloat = 0.1
static let defaultCurveSide = CurveSide.top
static let defaultCurveDirection = CurveDirection.inwards
}
// MARK: - Enums
enum CurveSide {
case top, left, bottom, right
}
enum CurveDirection {
case inwards, outwards
}
// MARK: - Getters & Setters
var curvePercent: CGFloat {
get {
_curvePercent
}
set {
_curvePercent = min(1, max(0, newValue))
}
}
// MARK: - Properties
var curveSide = Constants.defaultCurveSide
var curveDirection = Constants.defaultCurveDirection
private var _curvePercent = Constants.defaultCurvePercent
// MARK: - Overridden
override func layoutSubviews() {
super.layoutSubviews()
let shapeLayer = CAShapeLayer(layer: layer)
shapeLayer.path = curvedPath().cgPath
shapeLayer.frame = bounds
shapeLayer.masksToBounds = true
layer.mask = shapeLayer
}
}
// MARK: - Private
private extension CurvedView {
func curvedPath() -> UIBezierPath {
let path = UIBezierPath()
switch (curveSide, curveDirection) {
case (.top, .inwards):
let curveStartPoint = CGPoint(x: 0, y: 0)
let curveMiddlePoint = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * curvePercent)
let curveEndPoint = CGPoint(x: bounds.size.width, y: 0)
let curveControlPoint = calculateQuadCurveControlPoint(startPoint: curveStartPoint,
endPoint: curveEndPoint,
middlePoint: curveMiddlePoint)
path.move(to: curveStartPoint)
path.addQuadCurve(to: curveEndPoint,
controlPoint: curveControlPoint)
path.addLine(to: CGPoint(x: bounds.size.width, y: bounds.size.height))
path.addLine(to: CGPoint(x: 0, y: bounds.size.height))
path.addLine(to: CGPoint(x: 0, y: 0))
case (.top, .outwards):
let curveStartPoint = CGPoint(x: 0, y: bounds.size.height * curvePercent)
let curveMiddlePoint = CGPoint(x: bounds.size.width * 0.5, y: 0)
let curveEndPoint = CGPoint(x: bounds.size.width, y: bounds.size.height * curvePercent)
let curveControlPoint = calculateQuadCurveControlPoint(startPoint: curveStartPoint,
endPoint: curveEndPoint,
middlePoint: curveMiddlePoint)
path.move(to: curveStartPoint)
path.addQuadCurve(to: curveEndPoint,
controlPoint: curveControlPoint)
path.addLine(to: CGPoint(x: bounds.size.width, y: bounds.size.height))
path.addLine(to: CGPoint(x: 0, y: bounds.size.height))
path.addLine(to: CGPoint(x: 0, y: 0))
case (.left, .inwards):
let curveStartPoint = CGPoint(x: 0, y: bounds.size.height)
let curveMiddlePoint = CGPoint(x: bounds.size.width * curvePercent, y: bounds.size.height * 0.5)
let curveEndPoint = CGPoint(x: 0, y: 0)
let curveControlPoint = calculateQuadCurveControlPoint(startPoint: curveStartPoint,
endPoint: curveEndPoint,
middlePoint: curveMiddlePoint)
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: bounds.size.width, y: 0))
path.addLine(to: CGPoint(x: bounds.size.width, y: bounds.size.height))
path.addLine(to: curveStartPoint)
path.addQuadCurve(to: curveEndPoint,
controlPoint: curveControlPoint)
case (.left, .outwards):
let curveStartPoint = CGPoint(x: bounds.size.width * curvePercent, y: bounds.size.height)
let curveMiddlePoint = CGPoint(x: 0, y: bounds.size.height * 0.5)
let curveEndPoint = CGPoint(x: bounds.size.width * curvePercent, y: 0)
let curveControlPoint = calculateQuadCurveControlPoint(startPoint: curveStartPoint,
endPoint: curveEndPoint,
middlePoint: curveMiddlePoint)
path.move(to: CGPoint(x: bounds.size.width * curvePercent, y: 0))
path.addLine(to: CGPoint(x: bounds.size.width, y: 0))
path.addLine(to: CGPoint(x: bounds.size.width, y: bounds.size.height))
path.addLine(to: curveStartPoint)
path.addQuadCurve(to: curveEndPoint,
controlPoint: curveControlPoint)
case (.bottom, .inwards):
let curveStartPoint = CGPoint(x: bounds.size.width, y: bounds.size.height)
let curveMiddlePoint = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height - bounds.size.height * curvePercent)
let curveEndPoint = CGPoint(x: 0, y: bounds.size.height)
let curveControlPoint = calculateQuadCurveControlPoint(startPoint: curveStartPoint,
endPoint: curveEndPoint,
middlePoint: curveMiddlePoint)
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: bounds.size.width, y: 0))
path.addLine(to: curveStartPoint)
path.addQuadCurve(to: curveEndPoint,
controlPoint: curveControlPoint)
path.addLine(to: CGPoint(x: 0, y: 0))
case (.bottom, .outwards):
let curveStartPoint = CGPoint(x: bounds.size.width, y: bounds.size.height - (bounds.size.height * curvePercent))
let curveMiddlePoint = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height)
let curveEndPoint = CGPoint(x: 0, y: bounds.size.height - (bounds.size.height * curvePercent))
let curveControlPoint = calculateQuadCurveControlPoint(startPoint: curveStartPoint,
endPoint: curveEndPoint,
middlePoint: curveMiddlePoint)
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: bounds.size.width, y: 0))
path.addLine(to: curveStartPoint)
path.addQuadCurve(to: curveEndPoint,
controlPoint: curveControlPoint)
path.addLine(to: CGPoint(x: 0, y: 0))
case (.right, .inwards):
let curveStartPoint = CGPoint(x: bounds.size.width, y: 0)
let curveMiddlePoint = CGPoint(x: bounds.size.width - (bounds.size.width * curvePercent), y: bounds.size.height * 0.5)
let curveEndPoint = CGPoint(x: bounds.size.width, y: bounds.size.height)
let curveControlPoint = calculateQuadCurveControlPoint(startPoint: curveStartPoint,
endPoint: curveEndPoint,
middlePoint: curveMiddlePoint)
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: curveStartPoint)
path.addQuadCurve(to: curveEndPoint,
controlPoint: curveControlPoint)
path.addLine(to: CGPoint(x: 0, y: bounds.size.height))
path.addLine(to: CGPoint(x: 0, y: 0))
case (.right, .outwards):
let curveStartPoint = CGPoint(x: bounds.size.width - (bounds.size.width * curvePercent), y: 0)
let curveMiddlePoint = CGPoint(x: bounds.size.width, y: bounds.size.height * 0.5)
let curveEndPoint = CGPoint(x: bounds.size.width - (bounds.size.width * curvePercent), y: bounds.size.height)
let curveControlPoint = calculateQuadCurveControlPoint(startPoint: curveStartPoint,
endPoint: curveEndPoint,
middlePoint: curveMiddlePoint)
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: curveStartPoint)
path.addQuadCurve(to: curveEndPoint,
controlPoint: curveControlPoint)
path.addLine(to: CGPoint(x: 0, y: bounds.size.height))
path.addLine(to: CGPoint(x: 0, y: 0))
}
path.close()
return path
}
/// Calculates the correct `controlPoint` for a QuadCurve based on start, end and middle points.
/// - Parameters:
/// - startPoint: Start point.
/// - endPoint: End point.
/// - middlePoint: Middle point that the curve must go through.
/// - Returns: Control point to be used in a QuadCurve.
func calculateQuadCurveControlPoint(startPoint: CGPoint, endPoint: CGPoint, middlePoint: CGPoint) -> CGPoint {
CGPoint(x: calculateQuadCurveControlPoint1Dimension(p0: startPoint.x, p1: endPoint.x, control: middlePoint.x),
y: calculateQuadCurveControlPoint1Dimension(p0: startPoint.y, p1: endPoint.y, control: middlePoint.y))
}
/// Calculates the correct 1 dimensional `controlPoint` for a QuadCurve based on start, end and middle points.
/// https://stackoverflow.com/a/38753266/9323816
/// - Parameters:
/// - p0: Start point.
/// - p1: End point.
/// - control: Middle point that the curve must go through.
/// - Returns: 1 dimensional control point to be used in a QuadCurve.
func calculateQuadCurveControlPoint1Dimension(p0: CGFloat, p1: CGFloat, control: CGFloat) -> CGFloat {
2 * control - (p0 * 0.5) - (p1 * 0.5)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment