Text pixelating after scaling animation on view layer - ios

I am using following class to create curved text to be added to a CALayer
class CurvedTextLayer: CALayer {
override init() {
super.init()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override init(layer: Any) {
super.init(layer: layer)
}
init(with bounds: CGRect) {
super.init()
self.frame = bounds
self.setNeedsDisplay()
}
func update(bounds: CGRect) {
self.frame = bounds
self.setNeedsDisplay()
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
UIGraphicsPushContext(ctx)
ctx.translateBy(x: bounds.size.width / 2, y: bounds.size.height / 2)
ctx.scaleBy(x: 1, y: -1)
centreArcPerpendicular(text: "Hello round world", context: ctx, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: ctx, radius: 100, angle: CGFloat(-Double.pi/2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: ctx, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: CGFloat(Double.pi/4))
UIGraphicsPopContext()
}
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) }
let l = characters.count
let attributes = [NSAttributedString.Key.font: font]
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 chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
return 2 * asin(chord / (2 * radius))
}
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)
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
context.restoreGState()
}
}
In ViewController I add the layer as follows
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let sublayer = CurvedTextLayer(with: myView.bounds)
myView.layer.insertSublayer(sublayer, at: 0)
animate()
}
After this I add this animation
func animate() {
let rotationAnimation = CABasicAnimation(keyPath: "transform.scale")
rotationAnimation.fromValue = 1
rotationAnimation.toValue = 2
rotationAnimation.duration = 3
rotationAnimation.fillMode = .forwards
rotationAnimation.isRemovedOnCompletion = false
myView.layer.add(rotationAnimation, forKey: "wateva")
}
After scaling I get this pixelated text.
How can I make it so that it doesn't look pixelated?

Related

TextAttribute not getting applied

I have 3 function where Im drawing text on Image. The text includes attribute paragraphStyle = .right, but the text always getting aligned to left.
These are my function :
Function Name drawText() :
private func drawText(file: String = #file , line: Int = #line, function: String = #function) {
var myInflectionRate = inflectionRate
let EPS = 0.00000001
if myInflectionRate >= 0 {
myInflectionRate = max(EPS,myInflectionRate)
}else if myInflectionRate < 0{
myInflectionRate = min(-EPS,myInflectionRate)
}
let attributedText = NSAttributedString(string: text, attributes: textAttributes)
var rect = attributedText.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil)
let nsText = text as NSString
let textSize = nsText.size(withAttributes: textAttributes)
let angle = myInflectionRate*2*Double.pi
let arcLength = textSize.width
let r = arcLength / angle
let min_r = arcLength/(2*Double.pi)
let angleB = (Double.pi - angle) / 2.0
let widthOfRect = max(abs(angle) < Double.pi ? r * (sin(angle)/sin(angleB)) : abs(2*r), 2.0*min_r)
let positiveR = abs(r)
let positiveAngle = abs(angle)
let heightOfRect = max( positiveAngle < Double.pi ? positiveR - sqrt(positiveR*positiveR - (widthOfRect/2)*(widthOfRect/2)) : positiveR + positiveR*sin((positiveAngle-Double.pi)/2.0), 2.0*0)
rect.size = CGSize(width: widthOfRect + textSize.height, height: heightOfRect + 2.0*textSize.height)
if text.isEmpty {
rect.size = CGSize(width: 80, height: 80)
}
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
if myInflectionRate >= EPS {
context.translateBy (x: rect.size.width / 2, y: textSize.height + r )
}else if myInflectionRate <= -EPS{
context.translateBy (x: rect.size.width / 2, y: rect.size.height - textSize.height + r )
}else{
context.translateBy (x: rect.size.width / 2, y: rect.size.height/2)
}
context.scaleBy(x: 1, y: -1)
context.setFillColor(UIColor.clear.cgColor)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
imageViewForText?.image = image
imageViewForText?.contentMode = .scaleAspectFit
imageViewForText?.frame = self.bounds.insetBy(dx: globalInset!, dy: globalInset!)
imageViewForText?.backgroundColor = textBackgroundColor?.withAlphaComponent(textBackgroundAlpha ?? 1)
refresh()
}
Function Name centreArcPerpendicular() :
func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, attributes: [NSAttributedString.Key: AnyObject], clockwise: Bool, size: CGSize){
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
let l = characters.count
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
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
var modifiedAttributes = attributes
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
let ColorArraySize = CurrentTextConfiguration.shared.textConfiguration.textureTextColor.count
let offset = characters[i].size(withAttributes: attributes)
modifiedAttributes[NSAttributedString.Key.foregroundColor] = CurrentTextConfiguration.shared.textConfiguration.textureTextColor[i%ColorArraySize].getColor(bounds: CGRect(origin: .zero, size: offset))
thetaI += direction * arcs[i] / 2
}
centre(text: str, context: context, radius: r, angle: theta, attributes: modifiedAttributes, slantAngle: thetaI + slantCorrection,size : size)
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
if (chord/(2*radius)) > 1 {
return 2 * asin(1)
}else if (chord/(2*radius)) < -1 {
return 2 * asin(-1)
}
return 2 * asin(chord / (2 * radius))
}
Function name center() :
func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, attributes: [NSAttributedString.Key: AnyObject], slantAngle: CGFloat, size : CGSize) {
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()
}
I want to align my text from right , not left

Chart with UIBezierPath at one below one

I am trying to make pie chart as below which is working fine but I have issue with end path (which is orange in below image).
What I want to do is make end of orange shape to below the green one so that I can achieve as below.
Any suggestion how this can be done?
Code can be found at below link.
https://drive.google.com/file/d/1ST0zNooLgRaI8s2pDK3NMjBQYjBSRoXB/view?usp=sharing
Below is what I have.
func drawBeizer(start_angle : CGFloat, end_angle : CGFloat, final_color : UIColor) {
let path1 : UIBezierPath = UIBezierPath()
path1.addArc(withCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: ((self.frame.size.width-main_view_width)/2), startAngle: start_angle, endAngle: end_angle, clockwise: true)
path1.lineWidth = main_view_width
path1.lineCapStyle = .round
final_color.setStroke()
path1.stroke()
}
This function I am passing start angle and end angle & color for the path.
This solution is for SwiftUI using Path.
struct DonutElement: Shape{
var width: CGFloat = 50
var startAngle: Angle
var endAngle: Angle
func path(in rect: CGRect) -> Path {
// define commonly used vars
// the extend of the shape
let outerRadius = min(rect.width, rect.height) / 2
// the middle of the shape
let midRadius = outerRadius - width / 2
// the inner radius
let innerRadius = outerRadius - width
// centerpoint used to move coordinate system in to center
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
return Path{ path in
// calculate the startpoint from the startAngle. startAngle is allready normalized
let startPoint = startAngle.polarCoordinates(center: center, radius: outerRadius)
// move the path without drawing to the starting position
path.move(to: startPoint)
// add the starting concav arc
// the center point for this arc are the polar coordinates relative to midradius of the donut
path.addArc(center: startAngle.polarCoordinates(center: center, radius: midRadius), radius: width / 2, startAngle: startAngle.normalizing(), endAngle: (.init(degrees: 180) - startAngle), clockwise: true)
// add the arc that presents the inner donut line
// center point is the center of the drawing rect with normalized angles
path.addArc(center: center, radius: innerRadius, startAngle: startAngle.normalizing(), endAngle: endAngle.normalizing(), clockwise: true)
// add the convex arc at the end of the donut element
// switch clockwise to false and swap end and start angle
// replace startAngle with endAngle
path.addArc(center: endAngle.polarCoordinates(center: center, radius: midRadius), radius: width / 2, startAngle: (.init(degrees: 180) - endAngle), endAngle: endAngle.normalizing(), clockwise: false)
// add the outer stroke to close the shape
path.addArc(center: center, radius: outerRadius, startAngle: endAngle.normalizing(), endAngle: startAngle.normalizing(), clockwise: false)
// just in case
path.closeSubpath()
}
}
}
extension Shape {
func fillWithStroke<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.stroke(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
struct AngleContainer: Identifiable, Hashable{
let id = UUID()
var startAngle: Angle
var endAngle: Angle
var color: Color
}
struct Donut: View{
// each element ends here and starts at the previous or 0. Values in percent
var elementStops: [(CGFloat , Color)] = [(0.4 , .blue), (45 , .red), (55 , .green), (78 , .gray), (100 , .white)]
var width: CGFloat = 50.0
private var angles: [AngleContainer] {
var angles = [AngleContainer]()
for (index, stop) in elementStops.enumerated(){
if index == 0{
let startAngle = Angle(degrees: 0)
let endAngle = Angle(degrees: stop.0/100 * 360)
angles.append(AngleContainer(startAngle: startAngle,endAngle: endAngle,color: stop.1))
} else{
let startAngle = Angle(degrees: elementStops[index - 1].0 / 100 * 360)
let endAngle = Angle(degrees: stop.0/100 * 360)
angles.append(AngleContainer(startAngle: startAngle,endAngle: endAngle,color: stop.1))
}
}
return angles
}
var body: some View{
ZStack{
ForEach(angles){ angleContainer in
DonutElement(width: width, startAngle: angleContainer.startAngle, endAngle: angleContainer.endAngle)
.fillWithStroke(angleContainer.color, strokeBorder: .black.opacity(0.2))
}
}
.background(.white)
}
}
// used for transfering coordinate system
extension CGPoint{
mutating func transfered(to center: CGPoint){
x = center.x + x
y = center.y - y
}
func transfering(to center: CGPoint) -> CGPoint{
.init(x: center.x + x, y: center.y - y)
}
}
I am normalizing angles to behave more like in math with 90 degrees at the top and not at the bottom.
// convenience so angle feels more natural
extension Angle{
func normalizing() -> Angle {
Angle(degrees: 0) - self
}
func polarCoordinates(center: CGPoint, radius: CGFloat) -> CGPoint{
CGPoint(x: cos(self.radians) * radius, y: sin(self.radians) * radius )
.transfering(to: center)
}
}
Result:
I found a way to do this. Below is the full code.
I am adding other addArc using end angles.
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myView: MyView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
myView.setupData(inner_color: [.green, .purple, .blue, .orange], inner_view_width: self.view.frame.width/5.0, inner_angle: [0.0, 90.0, 180.0, 270.0])
myView.backgroundColor = UIColor.white
}
}
MyView.swift
import UIKit
class MyView: UIView {
var main_radius : CGFloat = 0.0
var main_color : [UIColor] = [UIColor.black, UIColor.green]
var main_view_width : CGFloat = 1.0
var main_angle : [CGFloat] = [-CGFloat.pi, 0.0]
var actual_angle : [CGFloat] = []
func setupData(inner_color : [UIColor], inner_view_width : CGFloat, inner_angle : [CGFloat]) {
main_color = inner_color
main_view_width = inner_view_width
main_angle = inner_angle
actual_angle = inner_angle
var temp_main_angle : [CGFloat] = []
for i in 0..<main_angle.count {
temp_main_angle.append(main_angle[i]*Double.pi/180)
}
main_angle = temp_main_angle
draw(CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.width))
}
override func draw(_ rect: CGRect) {
if (main_color.count >= 2) {
var inner_position : Int = 0
// first draw with sharp corner
for i in 0..<main_color.count {
if (i == (main_color.count-1)) {
inner_position = 0
} else {
inner_position = i+1
}
drawBeizer(start_angle: main_angle[i], end_angle: main_angle[inner_position], final_color: main_color[i])
}
// append with ending icons for circle
for i in 0..<main_color.count {
if (i == (main_color.count-1)) {
inner_position = 0
} else {
inner_position = i+1
}
drawBeizer(start_angle: main_angle[inner_position], end_angle: main_angle[inner_position]+(1*Double.pi/180), final_color: main_color[i], withCircle: true) // make here final_color as .black to see what I am doing with this loop
}
}
}
func drawBeizer(start_angle : CGFloat, end_angle : CGFloat, final_color : UIColor, withCircle: Bool = false) {
let path1 : UIBezierPath = UIBezierPath()
path1.addArc(withCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: ((self.frame.size.width-main_view_width)/2), startAngle: start_angle, endAngle: end_angle, clockwise: true)
path1.lineWidth = main_view_width
if (withCircle) {
path1.lineCapStyle = .round
}
final_color.setStroke()
path1.stroke()
}
}
Full code here

how to Curve label text in iOS swift

I have a label which I want to make curve up and curve down using slider. I achieve this using following code:
func drawCurvedString(on layer: CALayer, text: NSAttributedString, angle: CGFloat, radius: CGFloat) {
var radAngle = angle.radians
let textSize = text.boundingRect(
with: CGSize(width: .max, height: .max),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
.integral
.size
let perimeter: CGFloat = 2 * .pi * radius
let textAngle: CGFloat = textSize.width / perimeter * 2 * .pi
var textRotation: CGFloat = 0
var textDirection: CGFloat = 0
if angle > CGFloat(10).radians, angle < CGFloat(170).radians {
// bottom string
textRotation = 0.5 * .pi
textDirection = -2 * .pi
radAngle += textAngle / 2
} else {
// top string
textRotation = 1.5 * .pi
textDirection = 2 * .pi
radAngle -= textAngle / 2
}
for c in 0..<text.length {
let letter = text.attributedSubstring(from: NSRange(c..<c+1))
let charSize = letter.boundingRect(
with: CGSize(width: .max, height: .max),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
.integral
.size
let letterAngle = (charSize.width / perimeter) * textDirection
let x = radius * cos(radAngle + (letterAngle / 2))
let y = radius * sin(radAngle + (letterAngle / 2))
let singleChar = drawText(
on: layer,
text: letter,
frame: CGRect(
x: (layer.frame.size.width / 2) - (charSize.width / 2) + x,
y: (layer.frame.size.height / 2) - (charSize.height / 2) + y,
width: charSize.width,
height: charSize.height))
layer.addSublayer(singleChar)
singleChar.transform = CATransform3DMakeAffineTransform(CGAffineTransform(rotationAngle: radAngle - textRotation))
radAngle += letterAngle
}
}
func drawText(on layer: CALayer, text: NSAttributedString, frame: CGRect) -> CATextLayer {
let textLayer = CATextLayer()
textLayer.frame = frame
textLayer.string = text
textLayer.alignmentMode = kCAAlignmentCenter
textLayer.contentsScale = UIScreen.main.scale
return textLayer
}
But the problem is its add a sublayer of text. All I want to make existing text curve using slider value. There is some code available but that is in objective-c so can anyone help how to achieve this without adding sublayer in swift.
Thanks in advance.
Here is a complete example...
View Controller
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let testLabel = UILabelX()
testLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain original image view Top / Leading / Trailing
testLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
testLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// let's use the image's aspect ratio
testLabel.heightAnchor.constraint(equalToConstant: 300.0),
])
testLabel.backgroundColor = .yellow
testLabel.text = "This is a test of the UILabelX subclass."
}
}
UILabel subclass - from: https://stackoverflow.com/a/69056167/6257435
#IBDesignable
class UILabelX: UILabel {
#IBInspectable var angle: CGFloat = 1.6
#IBInspectable var clockwise: Bool = true
override func draw(_ rect: CGRect) {
centreArcPerpendicular()
}
/**
This draws the self.text around an arc of radius r,
with the text centred at polar angle theta
*/
func centreArcPerpendicular() {
guard let context = UIGraphicsGetCurrentContext() else { return }
let str = self.text ?? ""
let size = self.bounds.size
context.translateBy(x: size.width / 2, y: size.height / 2)
let radius = getRadiusForLabel()
let l = str.count
// let attributes: [String : Any] = [NSAttributedString.Key: self.font]
let attributes : [NSAttributedString.Key : Any] = [.font : self.font]
let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
var totalArc: CGFloat = 0 // ... and the total arc subtended by the string
// Calculate the arc subtended by each letter and their total
for i in 0 ..< l {
// arcs = [chordToArc(characters[i].widthOfString(usingFont: self.font), radius: radius)]
arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: radius)]
totalArc += arcs[i]
}
// Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
// or anti-clockwise (right way up at 6 o'clock)?
let direction: CGFloat = clockwise ? -1 : 1
let slantCorrection = clockwise ? -CGFloat(Double.pi/2) : CGFloat(Double.pi/2)
// The centre of the first character will then be at
// thetaI = theta - totalArc / 2 + arcs[0] / 2
// But we add the last term inside the loop
var thetaI = angle - direction * totalArc / 2
for i in 0 ..< l {
thetaI += direction * arcs[i] / 2
// Call centre with each character in turn.
// Remember to add +/-90º to the slantAngle otherwise
// the characters will "stack" round the arc rather than "text flow"
centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
// The centre of the next character will then be at
// thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
// but again we leave the last term to the start of the next loop...
thetaI += direction * arcs[i] / 2
}
}
func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
// *******************************************************
// Simple geometry
// *******************************************************
return 2 * asin(chord / (2 * radius))
}
/**
This draws the String str centred at the position
specified by the polar coordinates (r, theta)
i.e. the x= r * cos(theta) y= r * sin(theta)
and rotated by the angle slantAngle
*/
func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
// Set the text attributes
let attributes = [NSAttributedString.Key.font: self.font!] as [NSAttributedString.Key : Any]
// Save the context
context.saveGState()
// Move the origin to the centre of the text (negating the y-axis manually)
context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
// Rotate the coordinate system
context.rotate(by: -slantAngle)
// Calculate the width of the text
let offset = str.size(withAttributes: attributes)
// Move the origin by half the size of the text
context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
// Draw the text
str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
// Restore the context
context.restoreGState()
}
func getRadiusForLabel() -> CGFloat {
// Imagine the bounds of this label will have a circle inside it.
// The circle will be as big as the smallest width or height of this label.
// But we need to fit the size of the font on the circle so make the circle a little
// smaller so the text does not get drawn outside the bounds of the circle.
let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
let heightOfFont = self.text?.size(withAttributes: [NSAttributedString.Key.font: self.font]).height ?? 0
// Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}

How to do hit detection in core graphics

Core graphics is pretty new to me, and I'm facing some issues detecting clicks on my custom graphics.
I generated som code with the demo of paincode which i then heavily modified. It draws a "pie" like this:
The code I used for this looks like this:
import UIKit
public class DrawTest : NSObject {
static var hitAreas = [Int:UIBezierPath]()
static func didHit(_ point: CGPoint){
let res = hitAreas.first{ $0.value.contains(point) }?.key
print("HIT: ", res)
}
public class func drawDartboard(frame targetFrame: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let resizedFrame: CGRect = targetFrame
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100)
let sliceRect = CGRect(x: 0, y: 0, width: 100, height: 100)
context.saveGState()
context.clip(to: sliceRect)
context.translateBy(x: sliceRect.minX, y: sliceRect.minY)
context.translateBy(x: 0, y: sliceRect.height)
context.scaleBy(x: 1, y: -1)
let dark = UIColor(red: 0.235, green: 0.208, blue: 0.208, alpha: 1.000)
let light = UIColor(red: 0.435, green: 0.408, blue: 0.408, alpha: 1.000)
var slice = 0
while slice < 20 {
let sliceColor = slice%2 == 0 ? dark : light
DrawTest.drawSlice(frame: CGRect(origin: .zero, size: sliceRect.size), roration: CGFloat(slice*18), sliceColor: sliceColor, slice: slice )
slice += 1
}
context.restoreGState()
}
public class func drawSlice(frame targetFrame: CGRect, roration: CGFloat, sliceColor: UIColor, slice: Int) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let resizedFrame: CGRect = targetFrame
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100)
context.saveGState()
context.translateBy(x: 49.99, y: 50)
context.rotate(by: roration * CGFloat.pi/180)
let sliceFillPath = UIBezierPath()
sliceFillPath.move(to: CGPoint(x: -7.82, y: 49.38))
sliceFillPath.addCurve(to: CGPoint(x: 7.83, y: 49.38), controlPoint1: CGPoint(x: -2.63, y: 50.2), controlPoint2: CGPoint(x: 2.65, y: 50.2))
sliceFillPath.addLine(to: CGPoint(x: 0.01, y: -0))
sliceFillPath.addLine(to: CGPoint(x: -7.82, y: 49.38))
sliceFillPath.close()
sliceColor.setFill()
sliceFillPath.fill()
hitAreas[slice] = sliceFillPath
context.restoreGState()
}
}
I'm calling the draw code from a simple UIView subclass like below. This is also were I attach a TapGerstureRecognizer.
import UIKit
class DartBoardView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
DrawTest.didHit(loc)
}
}
override func draw(_ rect: CGRect) {
DrawTest.drawDartboard(frame: bounds)
}
}
The drawing looks like I want it to, but I want to be able to select each of the slices, this is the part that is not working. I am pretty sure that the issue has to do with the point I pass to didHit is local to my View but the UIBezierPath I store in hitAreas and call contains uses the local coordinates of the UIBezierPath, this is why I never get a hit.
I have no idea how to solve this and desperately need help. My guess is that this should be solved by 1) drawing my slices directy on the UIView´s coordinate system, but that would require a lot af math 2) somehow translate the local coordinates of each UIBezierPath to the scope of the view when hit testing
This is all very confusing at all constructive input is very appreciated.
There are various approaches, depending on exactly what your end-goal is.
One approach:
calculate the "degrees-per-slice" ... 360 / 20 = 18
get the angle from the center point to the touch point
"fix" the angle by 1/2 of the slice width (since the slices don't start at zero)
divide that angle by degrees-per-slice to get the slice number
Use these two extensions to make it easy to get the angle (in degrees):
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
extension CGPoint {
func angle(to otherPoint: CGPoint) -> CGFloat {
let pX = otherPoint.x - x
let pY = otherPoint.y - y
let radians = atan2f(Float(pY), Float(pX))
var degrees = CGFloat(radians).degrees
while degrees < 0 {
degrees += 360
}
return degrees
}
}
And, in the code you posted, in your DrawTest class, change didHit to:
static func didHit(_ point: CGPoint, in bounds: CGRect){
let c: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let angle = c.angle(to: point)
var fixedAngle = Int(angle) + 99 // 90 degrees + 1/2 of slice width
if fixedAngle >= 360 {
fixedAngle -= 360
}
print("HIT:", fixedAngle / 18)
}
and include the bounds when you call it from DartBoardView class as:
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
// include self's bounds
DrawTest.didHit(loc, in: bounds)
}
}
Drawbacks include:
you'd also need to check the "line length" to make sure it doesn't extend outside the circle
you don't have easy access to the slice bezier paths (if you want to do something else with them)
Another approach would be to use shape layers for each slice, making it easier to track the bezier paths.
Start with a Struct for the slices:
struct Slice {
var color: UIColor = .white
var path: UIBezierPath = UIBezierPath()
var shapeLayer: CAShapeLayer = CAShapeLayer()
var key: Int = 0
}
The DartBoardView class becomes (note: it uses the same CGFloat extension from above):
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
class DartBoardView: UIView {
// array of slices
var slices: [Slice] = []
// slice width in degrees
let sliceWidth: CGFloat = 360.0 / 20.0
// easy to understand 12 o'clock (3 o'clock is Zero)
let twelveOClock: CGFloat = 270
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let dark = UIColor(red: 0.235, green: 0.208, blue: 0.208, alpha: 1.000)
let light = UIColor(red: 0.435, green: 0.408, blue: 0.408, alpha: 1.000)
for slice in 0..<20 {
let sliceColor = slice % 2 == 1 ? dark : light
let s = Slice(color: sliceColor, key: slice)
s.shapeLayer.fillColor = s.color.cgColor
layer.addSublayer(s.shapeLayer)
slices.append(s)
}
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
if let s = slices.first(where: { $0.path.contains(loc) }) {
print("HIT:", s.key)
} else {
print("Tapped outside the circle!")
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
let c: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let radius: CGFloat = bounds.midX
// slice width in radians
let ww: CGFloat = sliceWidth.radians
// start 1/2 sliceWidth less than 12 o'clock
var startDegrees: CGFloat = twelveOClock.radians - (ww * 0.5)
for i in 0..<slices.count {
let endDegrees: CGFloat = startDegrees + ww
let pth: UIBezierPath = UIBezierPath()
pth.addArc(withCenter: c, radius: radius, startAngle: startDegrees, endAngle: endDegrees, clockwise: true)
pth.addLine(to: c)
pth.close()
slices[i].path = pth
slices[i].shapeLayer.path = pth.cgPath
startDegrees = endDegrees
}
}
}
And here's an example controller class to demonstrate:
class DartBoardViewController: UIViewController {
let dartBoard = DartBoardView()
override func viewDidLoad() {
super.viewDidLoad()
dartBoard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dartBoard)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
dartBoard.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dartBoard.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dartBoard.heightAnchor.constraint(equalTo: dartBoard.widthAnchor),
dartBoard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
dartBoard.backgroundColor = .black
}
}
Edit
Not as complex as it may seem.
Here's an implementation of a full Dart Board (without the numbers - I'll leave that as an exercise for you):
Segment Struct
struct Segment {
var value: Int = 0
var multiplier: Int = 1
var color: UIColor = .cyan
var path: UIBezierPath = UIBezierPath()
var layer: CAShapeLayer = CAShapeLayer()
}
DartBoardView class
class DartBoardView: UIView {
var doubleSegments: [Segment] = [Segment]()
var outerSingleSegments: [Segment] = [Segment]()
var tripleSegments: [Segment] = [Segment]()
var innerSingleSegments: [Segment] = [Segment]()
var singleBullSegment: Segment = Segment()
var doubleBullSegment: Segment = Segment()
var allSegments: [Segment] = [Segment]()
let boardLayer: CAShapeLayer = CAShapeLayer()
let darkColor: UIColor = UIColor(white: 0.1, alpha: 1.0)
let lightColor: UIColor = UIColor(red: 0.975, green: 0.9, blue: 0.8, alpha: 1.0)
let darkRedColor: UIColor = UIColor(red: 0.8, green: 0.1, blue: 0.1, alpha: 1.0)
let darkGreenColor: UIColor = UIColor(red: 0.0, green: 0.5, blue: 0.3, alpha: 1.0)
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(boardLayer)
boardLayer.fillColor = UIColor.black.cgColor
// points starting at 3 o'clock
let values: [Int] = [
6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5, 20, 1, 18, 4, 13,
]
// local vars for reuse
var seg: Segment = Segment()
var c: UIColor = .white
// doubles and triples
for i in 0..<values.count {
c = i % 2 == 1 ? darkRedColor : darkGreenColor
seg = Segment(value: values[i],
multiplier: 2,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
doubleSegments.append(seg)
seg = Segment(value: values[i],
multiplier: 3,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
tripleSegments.append(seg)
}
// singles
for i in 0..<values.count {
c = i % 2 == 1 ? darkColor : lightColor
seg = Segment(value: values[i],
multiplier: 1,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
outerSingleSegments.append(seg)
seg = Segment(value: values[i],
multiplier: 1,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
innerSingleSegments.append(seg)
}
// bull and double bull
seg = Segment(value: 25,
multiplier: 1,
color: darkGreenColor,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
singleBullSegment = seg
seg = Segment(value: 25,
multiplier: 2,
color: darkRedColor,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
doubleBullSegment = seg
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
if let s = allSegments.first(where: { $0.path.contains(loc) }) {
print("HIT:", s.multiplier == 3 ? "Triple" : s.multiplier == 2 ? "Double" : "Single", s.value)
} else {
print("Tapped outside!")
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
// initialize local variables for reuse / readability
var startAngle: CGFloat = 0
var outerDoubleRadius: CGFloat = 0.0
var innerDoubleRadius: CGFloat = 0.0
var outerTripleRadius: CGFloat = 0.0
var innerTripleRadius: CGFloat = 0.0
var outerBullRadius: CGFloat = 0.0
var innerBullRadius: CGFloat = 0.0
// initialize local constants
let viewCenter: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
// leave 20% for the numbers area
let diameter = bounds.width * 0.8
// dart board radii in mm
let specRadii: [CGFloat] = [
170, 162, 107, 99, 16, 6
]
// convert to view size
let factor: CGFloat = (diameter * 0.5) / specRadii[0]
outerDoubleRadius = specRadii[0] * factor
innerDoubleRadius = specRadii[1] * factor
outerTripleRadius = specRadii[2] * factor
innerTripleRadius = specRadii[3] * factor
outerBullRadius = specRadii[4] * factor
innerBullRadius = specRadii[5] * factor
let wireColor: UIColor = UIColor(white: 0.8, alpha: 1.0)
let wedgeWidth: CGFloat = 360.0 / 20.0
let incAngle: CGFloat = wedgeWidth.radians
startAngle = -(incAngle * 0.5)
var path: UIBezierPath = UIBezierPath()
// outer board layer
path = UIBezierPath(ovalIn: bounds)
boardLayer.path = path.cgPath
for i in 0..<20 {
let endAngle = startAngle + incAngle
var shape = doubleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerDoubleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: innerDoubleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
doubleSegments[i].path = path
shape.fillColor = doubleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = outerSingleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: innerDoubleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: outerTripleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
outerSingleSegments[i].path = path
shape.fillColor = outerSingleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = tripleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerTripleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: innerTripleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
tripleSegments[i].path = path
shape.fillColor = tripleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = innerSingleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: innerTripleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: outerBullRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
innerSingleSegments[i].path = path
shape.fillColor = innerSingleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
startAngle = endAngle
}
let singleBullPath = UIBezierPath(ovalIn: CGRect(x: viewCenter.x - outerBullRadius, y: viewCenter.y - outerBullRadius, width: outerBullRadius * 2, height: outerBullRadius * 2))
let doubleBullPath = UIBezierPath(ovalIn: CGRect(x: viewCenter.x - innerBullRadius, y: viewCenter.y - innerBullRadius, width: innerBullRadius * 2, height: innerBullRadius * 2))
var shape = singleBullSegment.layer
singleBullPath.append(doubleBullPath)
singleBullPath.usesEvenOddFillRule = true
shape.fillRule = .evenOdd
shape.path = singleBullPath.cgPath
singleBullSegment.path = singleBullPath
shape.fillColor = singleBullSegment.color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = doubleBullSegment.layer
shape.path = doubleBullPath.cgPath
doubleBullSegment.path = doubleBullPath
shape.fillColor = doubleBullSegment.color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
// append all segments for hit-testing
allSegments = []
allSegments.append(contentsOf: tripleSegments)
allSegments.append(contentsOf: outerSingleSegments)
allSegments.append(contentsOf: doubleSegments)
allSegments.append(contentsOf: innerSingleSegments)
allSegments.append(singleBullSegment)
allSegments.append(doubleBullSegment)
}
}
CGFloat extension
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
Example view controller
class DartBoardViewController: UIViewController {
let dartBoard = DartBoardView()
override func viewDidLoad() {
super.viewDidLoad()
dartBoard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dartBoard)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
dartBoard.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dartBoard.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dartBoard.heightAnchor.constraint(equalTo: dartBoard.widthAnchor),
dartBoard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
dartBoard.backgroundColor = .clear
}
}
Result:
and debug output from a few taps:
HIT: Double 20
HIT: Single 18
HIT: Triple 2
HIT: Single 25
HIT: Double 25

How to draw a circle diagram looking like a clock face using UIKit

I've been trying to figure this out for too long. With the help of this blog I managed to draw the diagram itself, but it can't show me any data, because it seems like my idea of creating a context array is not possible and I can have only one context per view, is that right? So how can I change the color of each marker individually? I've seen the solution using SpriteKit, but I don't know anything at all about SpriteKit.
func degree2Radian(a:CGFloat)->CGFloat {
let b = CGFloat(M_PI) * a/180
return b
}
override func draw(_ rect: CGRect) {
color.set()
pathForCircleCenteredAtPoint(midPoint: circleCenter, withRadius: circleRadius).stroke()
color = UIColor.white
color.set()
pathForCircleCenteredAtPoint(midPoint: CGPoint(x: bounds.midX, y: bounds.midY), withRadius: circleRadius).fill()
color = UIColor(red: 0.93, green: 0.93, blue: 0.94, alpha: 1)
color.set()
let ctx = UIGraphicsGetCurrentContext()
for i in 0...100 {
secondMarkers(ctx: ctx!, x: circleCenter.x, y: circleCenter.y, radius: circleRadius - 4, sides: 100, color: color)
}
diagramArray[0].strokePath()
}
func degree2radian(a:CGFloat)->CGFloat {
let b = CGFloat(M_PI) * a/180
return b
}
func circleCircumferencePoints(sides:Int,x:CGFloat,y:CGFloat,radius:CGFloat,adjustment:CGFloat=0)->[CGPoint] {
let angle = degree2radian(a: 360/CGFloat(sides))
let cx = x // x origin
let cy = y // y origin
let r = radius // radius of circle
var i = sides
var points = [CGPoint]()
while points.count <= sides {
let xpo = cx - r * cos(angle * CGFloat(i)+degree2radian(a: adjustment))
let ypo = cy - r * sin(angle * CGFloat(i)+degree2radian(a: adjustment))
points.append(CGPoint(x: xpo, y: ypo))
i -= 1;
}
return points
}
func secondMarkers(ctx:CGContext, x:CGFloat, y:CGFloat, radius:CGFloat, sides:Int, color:UIColor) {
// retrieve points
let points = circleCircumferencePoints(sides: sides,x: x,y: y,radius: radius)
// create path
// determine length of marker as a fraction of the total radius
var divider:CGFloat = 1/16
//for p in points {
let path = CGMutablePath()
divider = 1/10
let xn = points[counter].x + divider * (x-points[counter].x)
let yn = points[counter].y + divider * (y-points[counter].y)
// build path
path.move(to: CGPoint(x: points[counter].x, y: points[counter].y))
//path, nil, p.x, p.y)
path.addLine(to: CGPoint(x: xn, y: yn))
//CGPathAddLineToPoint(path, nil, xn, yn)
path.closeSubpath()
// add path to context
ctx.addPath(path)
ctx.setStrokeColor(color.cgColor)
ctx.setLineWidth(2.0)
//ctx.strokePath()
diagramArray.append(ctx)
counter += 1
//}
// set path color
}
So basically I'm trying to append context for each marker to an array, but when I draw one element of this array, it draws the whole diagram. This is what I need to achieve.
You shouldn't need to create more than one CGContext - you should just be reusing the same one to draw all graphics. Also, your method to calculate the secondMarkers seems unnecessarily complex. I believe this does what you want:
private func drawTicks(context: CGContext, tickCount: Int, center: CGPoint, startRadius: CGFloat, endRadius: CGFloat, ticksToColor: Int) {
for i in 0 ... tickCount {
let color: UIColor = i < ticksToColor ? .blue : .lightGray
context.setStrokeColor(color.cgColor)
let angle = .pi - degree2Radian(a: (CGFloat(360.0) / CGFloat(tickCount)) * CGFloat(i))
let path = CGMutablePath()
path.move(to: circleCircumferencePoint(center: center, angle: angle, radius: startRadius))
path.addLine(to: circleCircumferencePoint(center: center, angle: angle, radius: endRadius))
context.addPath(path)
context.strokePath()
}
}
private func circleCircumferencePoint(center: CGPoint, angle: CGFloat, radius: CGFloat) -> CGPoint {
return CGPoint(x: radius * sin(angle) + center.x, y: radius * cos(angle) + center.y)
}

Resources