Created
April 27, 2021 08:19
-
-
Save kvdesa/5e6a68cc7c7e907bdcfb9dc077c92747 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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