Instantly share code, notes, and snippets.
Created
January 20, 2019 00:34
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save mickspecial/284374a3f0c5b8752cac1651f5209e00 to your computer and use it in GitHub Desktop.
Custom Circle Button
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 CircleButton: UIControl { | |
// Mods | |
private var fontSize: CGFloat! | |
private var circleDiameter: CGFloat! | |
enum LabelPostition { | |
case topLeft, bottomLeft, topRight, bottomRight, left, right, top, bottom | |
} | |
enum IconSize { | |
case small, large, xlarge | |
} | |
private var iconSize = IconSize.small | |
private var labelPost = LabelPostition.top | |
private var frameSize: CGFloat { | |
return circleDiameter + (padding * 2) + fontSize | |
} | |
private var padding: CGFloat { | |
return fontSize * 0.55 | |
} | |
private var radiusCalc: CGFloat { | |
return circleDiameter * 0.5 + padding | |
} | |
private var iconsize: CGFloat { | |
return circleDiameter * 0.6 | |
} | |
var buttonImageView: UIImageView = { | |
let logoView = UIImageView() | |
logoView.contentMode = .scaleAspectFill | |
logoView.tintColor = UIColor.orange | |
logoView.translatesAutoresizingMaskIntoConstraints = false | |
logoView.isUserInteractionEnabled = false | |
return logoView | |
}() | |
var circleView: UIView = { | |
let circle = UIView() | |
circle.contentMode = .scaleAspectFill | |
circle.isUserInteractionEnabled = false | |
circle.translatesAutoresizingMaskIntoConstraints = false | |
return circle | |
}() | |
var mainTitle: String = "" | |
init(named name: String, image: UIImage, labelPostition: LabelPostition = .left, size: IconSize) { | |
super.init(frame:CGRect(x: 0, y: 0, width: 100, height: 100)) | |
switch size { | |
case .small: | |
self.fontSize = 18 | |
self.circleDiameter = 80 | |
case .large: | |
self.fontSize = 24 | |
self.circleDiameter = 120 | |
case .xlarge: | |
self.fontSize = 32 | |
self.circleDiameter = 200 | |
} | |
mainTitle = name | |
labelPost = labelPostition | |
let tintedImage = image.withRenderingMode(.alwaysTemplate) | |
buttonImageView.image = tintedImage | |
isOpaque = false | |
setupView() | |
} | |
override func draw(_ rect: CGRect) { | |
guard let context = UIGraphicsGetCurrentContext() else { return } | |
let size = self.bounds.size | |
context.translateBy (x: size.width / 2, y: size.height / 2) | |
context.scaleBy (x: 1, y: -1) | |
let f = UIFont.systemFont(ofSize: fontSize, weight: UIFont.Weight.semibold) | |
var startAngle: CGFloat = .pi | |
var clock = true | |
switch labelPost { | |
case .topLeft: | |
startAngle = 3 / 4 * .pi | |
clock = true | |
case .bottomLeft: | |
startAngle = 5 / 4 * .pi | |
clock = false | |
case .topRight: | |
startAngle = (.pi / 4) | |
clock = true | |
case .bottomRight: | |
startAngle = -(.pi / 4) | |
clock = false | |
case .left: | |
startAngle = .pi | |
clock = true | |
case .right: | |
startAngle = 0 | |
clock = true | |
case .top: | |
startAngle = .pi / 2 | |
clock = true | |
case .bottom: | |
startAngle = -(.pi / 2) | |
clock = false | |
} | |
centreArcPerpendicular(text: mainTitle, context: context, radius: radiusCalc, angle: startAngle, colour: UIColor.orange, font: f, clockwise: clock) | |
} | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
} | |
private func setupView() { | |
addSubview(circleView) | |
addSubview(buttonImageView) | |
setupLayout() | |
} | |
override func layoutSubviews() { | |
super.layoutSubviews() | |
let h = circleView.frame.size.height | |
circleView.layer.cornerRadius = h * 0.5 | |
circleView.layer.borderColor = UIColor.darkGray.cgColor | |
circleView.layer.borderWidth = 5 | |
} | |
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool) { | |
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str | |
let l = characters.count | |
let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font, NSAttributedString.Key.kern: 0.8] | |
var arcs: [CGFloat] = [] | |
var totalArc: CGFloat = 0 | |
for i in 0 ..< l { | |
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)] | |
totalArc += arcs[i] | |
} | |
let direction: CGFloat = clockwise ? -1 : 1 | |
let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2 | |
var thetaI = theta - direction * totalArc / 2 | |
for i in 0 ..< l { | |
thetaI += direction * arcs[i] / 2 | |
centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection) | |
thetaI += direction * arcs[i] / 2 | |
} | |
} | |
func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) { | |
let attributes = [NSAttributedString.Key.foregroundColor: c, NSAttributedString.Key.font: font] | |
context.saveGState() | |
context.scaleBy(x: 1, y: -1) | |
context.translateBy(x: r * cos(theta), y: -(r * sin(theta))) | |
context.rotate(by: -slantAngle) | |
let offset = str.size(withAttributes: attributes) | |
context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually) | |
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes) | |
context.restoreGState() | |
} | |
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat { | |
return 2 * asin(chord / (2 * radius)) | |
} | |
private func setupLayout() { | |
translatesAutoresizingMaskIntoConstraints = false | |
NSLayoutConstraint.activate([ | |
heightAnchor.constraint(equalToConstant: frameSize), | |
widthAnchor.constraint(equalToConstant: frameSize), | |
circleView.heightAnchor.constraint(equalToConstant: circleDiameter), | |
circleView.widthAnchor.constraint(equalToConstant: circleDiameter), | |
circleView.centerXAnchor.constraint(equalTo: centerXAnchor), | |
circleView.centerYAnchor.constraint(equalTo: centerYAnchor), | |
buttonImageView.centerXAnchor.constraint(equalTo: centerXAnchor), | |
buttonImageView.centerYAnchor.constraint(equalTo: centerYAnchor), | |
buttonImageView.heightAnchor.constraint(equalToConstant: iconsize), | |
buttonImageView.widthAnchor.constraint(equalToConstant: iconsize) | |
]) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The reason I set the image size within the class was because I needed to draw the circle / text. Couldn't get it to work otherwise. Ideally I wouldn't but not sure how to achieve that. So I needed to hack it together.