Skip to content

Instantly share code, notes, and snippets.

@aibobrov
Created September 17, 2018 14:09
Show Gist options
  • Save aibobrov/054c68162c7b0b844858a2b63baf1d0e to your computer and use it in GitHub Desktop.
Save aibobrov/054c68162c7b0b844858a2b63baf1d0e to your computer and use it in GitHub Desktop.
View masked by a rounded polygon
import UIKit
extension UIBezierPath {
/// Makes a bezier path which can be used for a rounded polygon
/// layer
///
/// - Parameters:
/// - rect: uiview rect bounds
/// - lineWidth: width border line
/// - sides: number of polygon's sides
/// - cornerRadius: radius for corners
/// - rotationOffset: offset of rotation of the view
/// - Returns: the newly created bezier path for layer mask
static func roundedPolygonPath(rect: CGRect, lineWidth: CGFloat, sides: Int, cornerRadius: CGFloat, rotationOffset: CGFloat = 0) -> UIBezierPath {
let path = UIBezierPath()
let theta: CGFloat = CGFloat(2.0 * .pi) / CGFloat(sides) // How much to turn at every corner
let width = min(rect.size.width, rect.size.height) // Width of the square
let center = CGPoint(x: rect.origin.x + width / 2.0, y: rect.origin.y + width / 2.0)
// Radius of the circle that encircles the polygon
// Notice that the radius is adjusted for the corners, that way the largest outer
// dimension of the resulting shape is always exactly the width - linewidth
let radius = (width - lineWidth + cornerRadius - (cos(theta) * cornerRadius)) / 2.0
// Start drawing at a point, which by default is at the right hand edge
// but can be offset
var angle = CGFloat(rotationOffset)
let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
path.move(to: CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta)))
for _ in 0 ..< sides {
angle += theta
let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
let tip = CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
let start = CGPoint(x: corner.x + cornerRadius * cos(angle - theta), y: corner.y + cornerRadius * sin(angle - theta))
let end = CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta))
path.addLine(to: start)
path.addQuadCurve(to: end, controlPoint: tip)
}
path.close()
// Move the path to the correct origins
let bounds = path.bounds
let transform = CGAffineTransform(translationX: -bounds.origin.x + rect.origin.x + lineWidth / 2.0, y: -bounds.origin.y + rect.origin.y + lineWidth / 2.0)
path.apply(transform)
return path
}
}
@IBDesignable
class PolygonMaskedView: UIView {
@IBInspectable var cornerRadius: CGFloat = 0 {
didSet {
updateMask()
}
}
@IBInspectable var lineWidth: CGFloat = 0 {
didSet {
updateMask()
}
}
@IBInspectable var sides: Int = 0 {
didSet {
updateMask()
}
}
@IBInspectable var rotationOffset: CGFloat = 0 {
didSet {
updateMask()
}
}
@IBInspectable var isChecked: Bool = false {
didSet {
updateMask()
}
}
@IBInspectable var strokeColor: UIColor = .clear {
didSet {
updateMask()
}
}
@IBInspectable var fillColor: UIColor = .clear {
didSet {
updateMask()
}
}
var borderLayer: CAShapeLayer = CAShapeLayer()
private func updateMask() {
layer.mask = nil
let mask = CAShapeLayer()
guard sides > 2 else { return }
let path: UIBezierPath = .roundedPolygonPath(
rect: bounds,
lineWidth: lineWidth,
sides: sides,
cornerRadius: cornerRadius,
rotationOffset: rotationOffset
)
let transform = CGAffineTransform(scaleX: bounds.width / path.bounds.width, y: bounds.height / path.bounds.height)
path.apply(transform)
mask.path = path.cgPath
layer.mask = mask
borderLayer.removeFromSuperlayer()
if !isChecked {
borderLayer.path = path.cgPath
borderLayer.lineWidth = lineWidth
borderLayer.strokeColor = strokeColor.cgColor
borderLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(borderLayer)
}
backgroundColor = isChecked ? fillColor : .clear
}
override func layoutSubviews() {
super.layoutSubviews()
updateMask()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
updateMask()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment