Draw radial background on a UIBezierPath - ios

Above picture shows how the following code runs:
extension Int {
var degreesToRadians: Double { return Double(self) * .pi / 180 }
var radiansToDegrees: Double { return Double(self) * 180 / .pi }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
class SunBurstView: UIView {
override func draw(_ rect: CGRect) {
let radius: CGFloat = rect.size.width / 2
UIColor.yellow.setFill()
let bezierPath = UIBezierPath()
let centerPoint = CGPoint(x: rect.origin.x + radius, y: rect.size.height / 2)
var thisPoint = CGPoint(x: centerPoint.x + radius, y: centerPoint.y + radius)
bezierPath.move(to: centerPoint)
var thisAngle: CGFloat = 0.0
let sliceDegrees: CGFloat = 360.0 / 20 / 2.0
for _ in 0..<20 {
let x = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
let y = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
thisPoint = CGPoint(x: x, y: y)
bezierPath.addLine(to: thisPoint)
thisAngle += sliceDegrees
let x2 = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
let y2 = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
thisPoint = CGPoint(x: x2, y: y2)
bezierPath.addLine(to: thisPoint)
bezierPath.addLine(to: centerPoint)
thisAngle += sliceDegrees
}
bezierPath.close()
bezierPath.lineWidth = 0
let colors = [UIColor.green.cgColor, UIColor.blue.cgColor] as CFArray
let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
let endPosition = min(frame.width, frame.height) / 2
let center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
UIGraphicsGetCurrentContext()?.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endPosition, options: .drawsAfterEndLocation)
bezierPath.fill()
bezierPath.stroke()
}
}
I want that the radial background only fills the UIBezierPath. Normally this could be accomplished with a mask layer, but the gradient does not have a property mask. Any help on how to draw a gradient radial background only on the UIBezierPath is appreciated! It should be transparent on the UIView where there is no UIBezierPath.
Full code with a linear gradient (copy-paste will work):
class SunBurstView: UIView {
override func draw(_ rect: CGRect) {
let radius: CGFloat = rect.size.width / 2
UIColor.yellow.setFill()
let bezierPath = UIBezierPath()
let centerPoint = CGPoint(x: rect.origin.x + radius, y: rect.size.height / 2)
var thisPoint = CGPoint(x: centerPoint.x + radius, y: centerPoint.y + radius)
bezierPath.move(to: centerPoint)
var thisAngle: CGFloat = 0.0
let sliceDegrees: CGFloat = 360.0 / 20 / 2.0
for _ in 0..<20 {
let x = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
let y = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
thisPoint = CGPoint(x: x, y: y)
bezierPath.addLine(to: thisPoint)
thisAngle += sliceDegrees
let x2 = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
let y2 = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
thisPoint = CGPoint(x: x2, y: y2)
bezierPath.addLine(to: thisPoint)
bezierPath.addLine(to: centerPoint)
thisAngle += sliceDegrees
}
bezierPath.close()
// let colors = [UIColor.green.cgColor, UIColor.blue.cgColor] as CFArray
// let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
// let endPosition = min(frame.width, frame.height) / 2
// let center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
// UIGraphicsGetCurrentContext()?.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endPosition, options: .drawsAfterEndLocation)
//linear
let shape = CAShapeLayer()
shape.path = bezierPath.cgPath
shape.lineWidth = 0.0
shape.strokeColor = UIColor.black.cgColor
self.layer.addSublayer(shape)
let gradient = CAGradientLayer()
gradient.frame = bezierPath.bounds
gradient.colors = [UIColor.blue.cgColor, UIColor.green.cgColor]
let shapeMask = CAShapeLayer()
shapeMask.path = bezierPath.cgPath
gradient.mask = shapeMask
self.layer.addSublayer(gradient)
bezierPath.lineWidth = 0
bezierPath.fill()
bezierPath.stroke()
}
}

Here is one implementation of a Radial Gradient Layer:
class RadialGradientLayer: CALayer {
var center: CGPoint {
return CGPoint(x: bounds.width/2, y: bounds.height/2)
}
var radius: CGFloat {
return min(bounds.width / 2.0, bounds.height / 2.0)
}
var colors: [UIColor] = [UIColor.black, UIColor.lightGray] {
didSet {
setNeedsDisplay()
}
}
var cgColors: [CGColor] {
return colors.map({ (color) -> CGColor in
return color.cgColor
})
}
override init() {
super.init()
needsDisplayOnBoundsChange = true
}
required init(coder aDecoder: NSCoder) {
super.init()
}
override func draw(in ctx: CGContext) {
ctx.saveGState()
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: cgColors as CFArray, locations: nil) else {
return
}
ctx.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: radius, options: CGGradientDrawingOptions(rawValue: 0))
}
}
So, in your latest code, replace:
let gradient = CAGradientLayer()
gradient.frame = bezierPath.bounds
gradient.colors = [UIColor.blue.cgColor, UIColor.green.cgColor]
with:
let gradient = RadialGradientLayer()
gradient.frame = bezierPath.bounds
gradient.colors = [UIColor.blue, UIColor.green]
Resulting in:

Here's how you can make your original code work with minimal changes.
override func draw(_ rect: CGRect) {
// [...]
// your code untouched until this line:
bezierPath.close()
// change the rest of the function to:
bezierPath.fill()
UIGraphicsGetCurrentContext()!.setBlendMode(.sourceIn)
let colors = [UIColor.green.cgColor, UIColor.blue.cgColor] as CFArray
let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
let endPosition = min(frame.width, frame.height) / 2
let center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
UIGraphicsGetCurrentContext()?.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endPosition, options: .drawsAfterEndLocation)
}
Result:
The idea is to first draw the bezier with the rays. The color does not matter here, this is only to draw alpha.
Then we draw the gradient on top of the alpha using one of Quartz' peculiar blend modes: kCGBlendModeSourceIn (see Porter-Duff alpha compositing).
This mode draws anything on top of existing alpha, replacing only the pixel's color, leaving alpha as it was. It's basically like using the current drawing's alpha as a mask.

Related

Border only in corners with UIBezierPath() wrong space in lineDashPattern

Im trying to create a view with border only in corners (qrCode style) using lineDashPattern, but the space between the lines doesn't seem to match with the Pattern defined by me. This is my playground:
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let myView = UIView(frame: CGRect(x: 50, y: 50, width: 200, height: 200))
myView.backgroundColor = .black
let borderLayer = CAShapeLayer()
let d = (myView.bounds.width / 2) as NSNumber
borderLayer.strokeColor = UIColor.blue.cgColor
borderLayer.lineDashPattern = [d]
borderLayer.frame = myView.bounds
borderLayer.fillColor = nil
borderLayer.lineWidth = 10
borderLayer.strokeStart = 0.1
//borderLayer.lineDashPhase = -80
borderLayer.path = UIBezierPath(roundedRect: myView.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 30, height: 30)).cgPath
borderLayer.lineCap = CAShapeLayerLineCap.round
myView.layer.addSublayer(borderLayer)
view.addSubview(myView)
self.view = view
}
I'm trying to adjust the start of the lines with the lineDashPhase and strokeStart attributes, but doesn't seems to work. Can someone help me?
Interesting idea - using long dashes on a rounded rect... But I don't believe you'll get your desired results.
You probably want to draw the 4 corners like this:
Your path would start with:
move to: A
line to: B
arc with center: C.x B.y and clockwise from 9 o'clock to 12 o'clock
line to: D
then update your points, and draw the top-right / bottom-right / bottom-left corners.
Here's an example class:
class BracketView: UIView {
var radius: CGFloat = 30
var lineLength: CGFloat = 30
var lineWidth: CGFloat = 10
var lineColor: UIColor = .blue
var shapeLayer: CAShapeLayer!
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
shapeLayer = self.layer as? CAShapeLayer
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = .round
}
override func layoutSubviews() {
super.layoutSubviews()
// set these properties here, so we can change them if desired
shapeLayer.strokeColor = lineColor.cgColor
shapeLayer.lineWidth = lineWidth
let pth = UIBezierPath()
var ptA: CGPoint = .zero
var ptB: CGPoint = .zero
var ptC: CGPoint = .zero
var ptD: CGPoint = .zero
var arcCenter: CGPoint = .zero
var startAngle: CGFloat = 0
// top-left corner
startAngle = .pi // 9 o'clock
ptA.x = bounds.minX
ptA.y = bounds.minY + radius + lineLength
ptB.x = bounds.minX
ptB.y = bounds.minY + radius
ptC.x = bounds.minX + radius
ptC.y = bounds.minY
ptD.x = bounds.minX + radius + lineLength
ptD.y = bounds.minY
arcCenter.x = ptC.x
arcCenter.y = ptB.y
pth.move(to: ptA)
pth.addLine(to: ptB)
pth.addArc(withCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: startAngle + .pi * 0.5, clockwise: true)
pth.addLine(to: ptD)
// top-right corner
startAngle += (.pi * 0.5) // 12 o'clock
ptA.x = bounds.maxX - (radius + lineLength)
ptA.y = bounds.minY
ptB.x = bounds.maxX - radius
ptB.y = bounds.minY
ptC.x = bounds.maxX
ptC.y = bounds.minY + radius
ptD.x = bounds.maxX
ptD.y = bounds.minY + radius + lineLength
arcCenter.x = ptB.x
arcCenter.y = ptC.y
pth.move(to: ptA)
pth.addLine(to: ptB)
pth.addArc(withCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: startAngle + .pi * 0.5, clockwise: true)
pth.addLine(to: ptD)
// bottom-right corner
startAngle += (.pi * 0.5) // 3 o'clock
ptA.x = bounds.maxX
ptA.y = bounds.maxY - (radius + lineLength)
ptB.x = bounds.maxX
ptB.y = bounds.maxY - radius
ptC.x = bounds.maxX - radius
ptC.y = bounds.maxY
ptD.x = bounds.maxX - (radius + lineLength)
ptD.y = bounds.maxY
arcCenter.x = ptC.x
arcCenter.y = ptB.y
pth.move(to: ptA)
pth.addLine(to: ptB)
pth.addArc(withCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: startAngle + .pi * 0.5, clockwise: true)
pth.addLine(to: ptD)
// bottom-left corner
startAngle += (.pi * 0.5) // 6 o'clock
ptA.x = bounds.minX + radius + lineLength
ptA.y = bounds.maxY
ptB.x = bounds.minX + radius
ptB.y = bounds.maxY
ptC.x = bounds.minX
ptC.y = bounds.maxY - radius
ptD.x = bounds.minX
ptD.y = bounds.maxY - (radius + lineLength)
arcCenter.x = ptB.x
arcCenter.y = ptC.y
pth.move(to: ptA)
pth.addLine(to: ptB)
pth.addArc(withCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: startAngle + .pi * 0.5, clockwise: true)
pth.addLine(to: ptD)
shapeLayer.path = pth.cgPath
}
}
Result with a 200 x 200 frame, green background:

Code not executing after end of UIView.animate?

I am in the draw function of an UIView.
I want to draw a triangle, then after an animation, it's a mirror copy.
I can draw the triangle and the animation, or the triangle and it's copy, but when I add the code for drawing the copy in the "finished in" portion of the animation, it isn't drawn.
I don't know how to solve this problem...
Here is my code (with drawing of the mirror copy not showing anything).
public override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let viewWidth = self.bounds.width
let viewHeight = self.bounds.height
context.setLineWidth(1.0)
context.setStrokeColor(UIColor.darkGray.cgColor)
context.beginPath()
let path = CGMutablePath()
let r = sqrt(viewHeight * viewHeight + viewWidth * viewWidth) * 0.50
let p1 = CGPoint(x: viewWidth/2, y: 20 + r)
var angle = CGFloat.pi * (90.0 + 30.0) / 180.0
let p2 = CGPoint(x: p1.x + cos(angle) * r, y: p1.y - sin(angle) * r)
angle = CGFloat.pi * 90.0 / 180.0
let p3 = CGPoint(x: p1.x + cos(angle) * r, y: p1.y - sin(angle) * r)
path.move(to: p1)
path.addLine(to: p2)
path.addLine(to: p3)
path.closeSubpath()
context.addPath(path)
context.strokePath()
let trf = CGAffineTransform(scaleX: -1, y: 1)
let sv = UIView(frame:self.bounds)
self.addSubview(sv)
let shapeLayer = CAShapeLayer.init()
shapeLayer.path = path
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
sv.layer.addSublayer(shapeLayer)
shapeLayer.display()
UIView.animate(withDuration: 0.5, delay: 0, options: [],
animations: {
sv.transform = trf
}) { finished in
sv.removeFromSuperview()
context.setStrokeColor(UIColor.red.cgColor)
context.scaleBy(x: -1, y: 1)
context.translateBy(x:-viewWidth, y:0)
context.addPath(path)
context.strokePath()
context.scaleBy(x: -1, y: 1)
context.translateBy(x:viewWidth, y:0)
}
}

iOS Swift - Animate drawing of arc filled, not the border stroke

I am trying to Implement drawing of Arc from certain start angle to certain end angle. I am trying to animate drawing of arc but have no luck.
I have looked for couple of implementations but all of them refers to be drawing of animating boder of circle.
Like drawing a pie but with one color. With certain start and end angle. Animated drawing of pie.
What I like is to have have animated filling while arc is being animated from start angle to end angle.
Like arc is being drawn clockwise and while drawing it is filling its inside color.
Any idea How can I acheive this in Swift?
Thanks in advance.
Edit
This is what I have drawn:
(gray color shows area covered by drawing pie)
Code for drawing Pie
import Foundation
import UIKit
#IBDesignable class PieChart : UIView {
#IBInspectable var percentage: CGFloat = 50 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderWidth: CGFloat = 2 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderColor: UIColor = UIColor.white {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var percentageFilledColor: UIColor = UIColor.primary {
didSet {
self.setNeedsDisplay()
}
}
let circleLayer = CAShapeLayer()
override func draw(_ rect: CGRect) {
drawPie(rect: rect, endPercent: percentage, color: UIColor.primary)
}
func drawPie(rect: CGRect, endPercent: CGFloat = 70, color: UIColor) {
let center = CGPoint(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2)
let radius = min(rect.width, rect.height) / 2
let gpath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(360.degreesToRadians), clockwise: true)
UIColor.white.setFill()
gpath.fill()
let circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0), endAngle:CGFloat(360.degreesToRadians), clockwise: true)
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = outerBorderColor.cgColor
circleLayer.lineWidth = outerBorderWidth
circleLayer.borderColor = outerBorderColor.cgColor
self.layer.addSublayer(circleLayer)
let π: CGFloat = 3.14
let halfPi: CGFloat = π / 2
let startPercent: CGFloat = 0.0
let startAngle = (startPercent / 100 * π * 2 - π ) + halfPi
let endAngle = (endPercent / 100 * π * 2 - π ) + halfPi
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true)
path.close()
percentageFilledColor.setFill()
path.fill()
}
}
I've created this code using playground and swift. This code will draw a pie chart animating the filling color. Make sure you have the Timeline pane open on Xcode to see the animation working. I couldn't find a way to pass the path function directly to CAAnimation so my workaround was to create a list of steps, each one containing the path for a percentage stage. You can change the steps for a smother animation.
//: Playground - noun: a place where people can play
import UIKit
import XCPlayground
let STEPS_ANIMATION = 50
let initialPercentage : CGFloat = 0.10
let finalPercentage : CGFloat = 0.75
func buildPiePath(frame : CGRect, percentage : CGFloat) -> UIBezierPath {
let newPath = UIBezierPath()
let startPoint = CGPoint(x: frame.size.width, y: frame.height / 2.0)
let centerPoint = CGPoint(x: frame.size.width / 2.0, y: frame.height / 2.0)
let startAngle : CGFloat = 0
let endAngle : CGFloat = percentage * 2 * CGFloat(M_PI)
newPath.move(to: centerPoint)
newPath.addLine(to: startPoint)
newPath.addArc(withCenter: centerPoint,
radius: frame.size.width / 2.0,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
newPath.addLine(to: centerPoint)
newPath.close()
return newPath
}
func buildPiePathList(frame: CGRect,
startPercentage: CGFloat,
finalPercentage: CGFloat) -> [AnyObject] {
var listValues = [AnyObject]()
for index in 1...STEPS_ANIMATION {
let delta = finalPercentage - startPercentage
let currentPercentage = CGFloat(index) / CGFloat(STEPS_ANIMATION)
let percentage = CGFloat(startPercentage + (delta * currentPercentage))
listValues.append(buildPiePath(frame: frame,
percentage: percentage)
.cgPath)
}
return listValues
}
// Container for pie chart
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
container.backgroundColor = UIColor.gray
XCPShowView(identifier: "Container View", view: container)
let circleFrame = CGRect(x: 20, y: 20, width: 360, height: 360)
// Red background
let background = CAShapeLayer()
background.frame = circleFrame
background.fillColor = UIColor.red.cgColor
background.path = buildPiePath(frame: circleFrame, percentage: 1.0).cgPath
container.layer.addSublayer(background)
// Green foreground that animates
let foreground = CAShapeLayer()
foreground.frame = circleFrame
foreground.fillColor = UIColor.green.cgColor
foreground.path = buildPiePath(frame: circleFrame, percentage: initialPercentage).cgPath
container.layer.addSublayer(foreground)
// Filling animation
let fillAnimation = CAKeyframeAnimation(keyPath: "path")
fillAnimation.values = buildPiePathList(frame: circleFrame,
startPercentage: initialPercentage,
finalPercentage: finalPercentage)
fillAnimation.duration = 3
fillAnimation.calculationMode = kCAAnimationDiscrete
foreground.add(fillAnimation, forKey:nil)

Draw half circuler image like meter using UIBezierPath...!

My View Controller code contains below code
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let width: CGFloat = 240
let height: CGFloat = 240
let demoView = DemoView(frame: CGRect(x: self.view.frame.size.width/2 - width/2,
y: self.view.frame.size.height/2 - height/2,
width: width,
height: height))
let subView = UIView.init(frame: (CGRect(x: demoView.frame.origin.x - width,
y: demoView.frame.origin.y,
width: width * 2,
height: height * 2)))
self.view.addSubview(demoView)
self.view.addSubview(subView)
subView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5)
subView.layer.cornerRadius = subView.frame.size.height / 2
}
}
import UIKit
class DemoView: UIView {
var path: UIBezierPath!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.darkGray
}
override func draw(_ rect: CGRect) {
self.createTriangle()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func createTriangle() {
let count : Int = 9
let gap : CGFloat = 3
let yValue : CGFloat = CGFloat(self.frame.size.width - ((CGFloat(count - 1)) * gap)) / CGFloat(count);
for a in 0 ..< count {
let i : CGFloat = CGFloat(a)
let path1: UIBezierPath! = UIBezierPath()
path1.move(to: CGPoint(x: 0.0, y: self.frame.size.height))
path1.addLine(to: CGPoint(x: (yValue * i) > 0 ? (yValue * i) + i*gap : 0, y: (yValue * i) > 0 ? (yValue * i) + i*gap : 0))
path1.addLine(to: CGPoint(x:yValue * (i+1) + i*gap, y: yValue * (i+1) + i*gap))
path1.close()
UIColor.orange.setFill()
path1.fill()
}
}
}
Can anyone help me to achieve this thing?
Edit image :
Is this the result your want ?
The way I did it isn't with triangles but Arcs.
Add createPie() in your DemoView class and call it in draw(:) instead of your createTriangle().
This is my code:
func createPie() {
// 2 vars to configure width of gap/banches
var branchAmount = 10
var gapAngle = CGFloat.pi / 100
let startAngle = 3 * CGFloat.pi / 2
let endAngle = 2 * CGFloat.pi
let branchAngle = (endAngle - startAngle - (CGFloat(branchAmount) - 1) * gapAngle) / CGFloat(branchAmount)
let paths = UIBezierPath()
for i in 0..<branchAmount {
paths.move(to: CGPoint(x: 0.0, y: self.frame.size.height))
paths.addArc(withCenter: CGPoint(x: 0, y: self.frame.size.height),
radius: self.frame.size.height,
startAngle: startAngle + CGFloat(i) * (branchAngle + gapAngle),
endAngle: startAngle + CGFloat(i) * (branchAngle + gapAngle) + branchAngle,
clockwise: true)
}
paths.close()
UIColor.orange.setFill()
paths.fill()
}
Cheers!
EDIT: If you want to add a circular mask you and add this in the end of createPie() (which is no longer really a pie now..):
// Circular mask
let maskLayer = CAShapeLayer()
let maskPath = UIBezierPath(rect: bounds)
maskLayer.fillRule = kCAFillRuleEvenOdd // Circle will be substracted to the mask thanks to this
maskPath.move(to: CGPoint(x: 0.0, y: frame.size.height))
maskPath.addArc(withCenter: CGPoint(x: 0, y: frame.size.height), radius: maskRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
maskLayer.path = maskPath.cgPath
layer.mask = maskLayer
It just adds mask composed of the subtraction of bounds and the circle of origin (0, height)

iOS Animation with Core Graphics

I am currently creating a Hangman game application in iOS using Swift.
I have all of the game mechanics completed, and wanted to use Core Graphics to draw the hangman and gallows. I can draw the gallows and hangman using UIBezierPath, and have broken the drawing of each part of the view (gallows, head, body, left arm,...) into separate functions that are called to create the entire image. All of this is inside a custom UIView class.
What I can't seem to figure out is how to add successive parts of the Hangman image as the user makes incorrect guesses. I am trying to avoid having a series of images that I cycle between. Instead, I want to use the same custom view to draw the stick figure piece by piece using Core Graphics.
How can I implement a function that will draw each successive part of the figure?
Something like "self.customView.updateDrawing()" where updateDrawing method in the customView would take info from the model about the number of incorrect guesses and call the appropriate methods to draw the needed parts of the stick figure.
UPDATE
Here is my attempt at implementing the view from this post https://codereview.stackexchange.com/questions/97424/hangman-in-swift into my own hangman game. They work great for drawing the gallows, but I can't actually get the image to update itself and draw successive body parts to add the hangman. Here is my code so far:
GameViewTwo : this is the GameView from above post, and I've tried to split it up and add the different parts as layers to the view.
import UIKit
enum BodyPart: Int {
case Head = 1
case Body = 2
case LeftArm = 5
case RightArm = 6
case LeftLeg = 3
case RightLeg = 4
case LeftEye = 7
case RightEye = 8
case Mouth = 9
case Unknown = 10
}
class GameViewTwo: UIView {
var path = UIBezierPath()
var newPartLayer = CAShapeLayer()
private var bodyStart: CGPoint = CGPoint.zero
private var bodyEnd: CGPoint = CGPoint.zero
private var headMiddle: CGPoint = CGPoint.zero
struct DrawingConstants {
static let gallowBaseStartScale: CGFloat = 0.15
static let gallowBaseEndScale: CGFloat = 0.85
static let gallowBaseHeight: CGFloat = 10
static let gallowHeight: CGFloat = 0.05 //static let gallowHeight: CGFloat = 0.15
static let gallowHeightStart: CGFloat = 0.175
static let gallowHeightWidth: CGFloat = 10
static let gallowAcrossScale: CGFloat = 0.5
static let gallowTipHeight: CGFloat = 17.5
static let headRadius: CGFloat = 16
static let bodyLength: CGFloat = 25
static let bodyHeight: CGFloat = 25
static let legLength: CGFloat = 50
static let grassHeightScale: CGFloat = 0.68
static let armBack: CGFloat = 5
}
struct ScaleConstants {
static let bodyLength: CGFloat = 50
static let limbLength: CGFloat = 25
static let handHeightScale: CGFloat = 0.4
static let headRadius: CGFloat = 20
static let eyeRadius = CGFloat(0.15 * ScaleConstants.headRadius)
static let eyeOffset = CGFloat(0.3 * ScaleConstants.headRadius)
static let mouthOffSet = CGFloat(0.3 * ScaleConstants.headRadius)
static let mouthRadius = CGFloat(0.25 * ScaleConstants.headRadius)
}
var part : BodyPart?
func setPart(part: BodyPart){
self.part = part
}
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
drawGallow()
}
func add(part: BodyPart){
let partPath = path(forPart: part)
newPartLayer.frame = bounds
newPartLayer.path = partPath.cgPath
newPartLayer.strokeColor = UIColor.black.cgColor
let strokeAnim = CABasicAnimation(keyPath: "strokeEnd")
strokeAnim.fromValue = 0
strokeAnim.toValue = 1
strokeAnim.duration = 1
layer.addSublayer(newPartLayer)
newPartLayer.add(strokeAnim, forKey: "path")
}
func path(forPart: BodyPart)-> UIBezierPath {
switch forPart {
case BodyPart.Head :
let centerX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let centerY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + ScaleConstants.headRadius)
let center = CGPoint(x: centerX, y: centerY)
headMiddle = center
path = UIBezierPath(arcCenter: center, radius: ScaleConstants.headRadius, startAngle: CGFloat(0), endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(2)
return path
case BodyPart.Body :
let add = CGFloat(DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + 2 * ScaleConstants.headRadius)
let startPointY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + add)
let startPointX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let startPoint = CGPoint(x: startPointX, y: startPointY)
let endPoint = CGPoint(x: startPoint.x, y: startPoint.y + ScaleConstants.bodyLength)
bodyStart = startPoint
bodyEnd = endPoint
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.LeftLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.RightLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.LeftArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.RightArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
path.lineWidth = CGFloat(2)
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case BodyPart.LeftEye :
UIColor.black.set()
let eyeMiddle = CGPoint(x: headMiddle.x - ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
case BodyPart.RightEye :
UIColor.black.set()
let eyeMiddle = CGPoint(x: headMiddle.x + ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
default:
return path
}
}
/******************************************************************************************/
func connectPoints(bottomLeftPoint: CGPoint, bottomRightPoint: CGPoint, topLeftPoint: CGPoint, topRightPoint: CGPoint, color: UIColor) {
color.set()
let path = UIBezierPath()
path.move(to: bottomLeftPoint)
path.addLine(to: topLeftPoint)
path.addLine(to: topRightPoint)
path.addLine(to: bottomRightPoint)
path.close()
path.fill()
path.stroke()
}
func calculateMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
return CGPoint(x: (point1.x + point2.x) / 2, y: (point1.y + point2.y) / 2)
}
/*****************************************************************************************/
func drawGrass() {
let topStartPoint = CGPoint(x: CGFloat(0), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: topStartPoint.y)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: CGFloat(bounds.size.height))
let bottomLeftPoint = CGPoint(x: CGFloat(0), y: bottomRightPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topStartPoint, topRightPoint: topRightPoint, color: UIColor.green)
}
func drawSky() {
let bottomLeftPoint = CGPoint(x: CGFloat(0), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
let topLeftPoint = CGPoint(x: CGFloat(0), y: CGFloat(0))
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: CGFloat(0))
let bottomRightPoint = CGPoint(x: CGFloat(bounds.size.width), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.cyan)
}
func drawGallow() {
drawGallowBase()
drawGallowHeight()
drawGallowAcross()
drawGallowTip()
}
func drawGallowBase() {
let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowBaseStartScale), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale))
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: bottomLeftPoint.y - DrawingConstants.gallowBaseHeight)
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowBaseEndScale), y: topLeftPoint.y)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
func drawGallowHeight() {
let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowHeightStart), y: CGFloat(bounds.size.height * DrawingConstants.grassHeightScale - DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: bottomLeftPoint.x + DrawingConstants.gallowHeightWidth, y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: bounds.size.height * DrawingConstants.gallowHeight)
let topRightPoint = CGPoint(x: bottomRightPoint.x, y: topLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
func drawGallowAcross() {
let bottomLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowHeightStart) + DrawingConstants.gallowHeightWidth, y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale), y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight))
let topRightPoint = CGPoint(x: CGFloat(bottomRightPoint.x), y: topLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
func drawGallowTip() {
let topLeftPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - DrawingConstants.gallowHeightWidth), y: CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let topRightPoint = CGPoint(x: CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale), y: topLeftPoint.y)
let bottomLeftPoint = CGPoint(x: topLeftPoint.x, y: topLeftPoint.y + DrawingConstants.gallowTipHeight)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
connectPoints(bottomLeftPoint: bottomLeftPoint, bottomRightPoint: bottomRightPoint, topLeftPoint: topLeftPoint, topRightPoint: topRightPoint, color: UIColor.brown)
}
/*******************************************************************************************/
}
and then it is called from my GameViewController like this
let newPart = BodyPart(rawValue: (model?.attempts)!)
self.hangmanView.add(part: newPart!)
where hangmanView is a GameViewTwo
Create an enumeration for each body part. Make it an integer so that you can instantiate it with the number of wrong answers.
enum BodyPart: Int {
case head = 0
case leftArm
case rightArm
...
}
to make a new body part
let newPart = BodyPart(rawValue: incorrectGuesses - 1)
Give your view a few functions...
func add(part: BodyPart){
// first get a drawing path according to that part
let partPath = path(for: part)
// lets add a new layer so we can animate it seperately
let newPartLayer = CAShapeLayer()
newPartLayer.frame = bounds
newPartLayer.path = partPath
// linewidth, linecap you might want to play with
let strokeAnim = CABasicAnimation(keyPath: "strokeEnd")
strokeAnim.fromValue = 0
strokeAnim.toValue = 1
strokeAnim.duration = 0.5 // play with it, see what you like
layer.addSublayer(layer: newPartLayer)
newPartLayer.add(anim: strokeAnim)
}
func path(for: BodyPart) -> CGPath {
// draw your part path, return it as a CGPath
}
Then from your controller, instantiate a new part and add it to your hangmanview, it will draw the layer as you drew it in the drawing code. UIBezierPath has a .cgPath variable to cast as a core graphics path.
let newPart = BodyPart(rawValue: incorrectGuesses - 1)
hangmanView.add(part: newPart)
When you want a newgame, just remove all the sublayers from the hangmanView. CAShapeLayers are really useful for simply little shapes, and you can animate changing the path (keyPath: "path") or stroking the path in either direction. It'll look good. You could even move parts around independently or do other fun things with them if you kept a reference to all the part layers. This way, you kind of abide by the model-view-controller paradigm. You keep the state of the game away from your view, all your view does it add on body parts, and your controller provides the parts to do so. It's not a huge deal with simple things, but it's good to keep in mind as you get better and things get more complicated.
Quick edit: you might want to try keyPath: "path", give it a start path(maybe the body path) and an end path, it will look like the part grows out of the body. Good luck!
*** Edited answer.
I cleaned up your code here. I'm not totally sure why you have certain variables in there or the little class private struct, but thats ok, I left em in. I moved some stuff around but mostly kept it the same except where it was clear you were drawing a rectangle instead of some other shape. I didn't try running it. Lemme know how it goes.
enum BodyPart: Int {
case noose = 0
case head
case body
case leftLeg
case rightLeg
case leftArm
case rightArm
case leftEye
case rightEye
case mouth
case unknown
}
class HangmanView: UIView {
var partLayers = [CAShapeLayer]()
private var bodyStart: CGPoint = CGPoint.zero
private var bodyEnd: CGPoint = CGPoint.zero
private var headMiddle: CGPoint = CGPoint.zero
var currentPart: BodyPart?
func clear() {
for i in partLayers {
i.removeFromSuperlayer()
}
partLayers.removeAll()
}
func add(part: BodyPart){
currentPart = part
let newPartLayer = CAShapeLayer()
let partPath = path(forPart: part)
newPartLayer.frame = bounds
newPartLayer.path = partPath.cgPath
newPartLayer.strokeColor = UIColor.black.cgColor
newPartLayer.fillColor = UIColor.clear.cgColor
newPartLayer.lineCap = kCALineCapRound
newPartLayer.lineWidth = part == .rightEye || part == .leftEye ? 1 : 2
let strokeAnim = CABasicAnimation(keyPath: "strokeEnd")
strokeAnim.fromValue = 0
strokeAnim.toValue = 1
strokeAnim.duration = 1
layer.addSublayer(newPartLayer)
newPartLayer.add(strokeAnim, forKey: "path")
partLayers.append(newPartLayer)
}
func path(forPart: BodyPart) -> UIBezierPath {
switch forPart {
case .noose:
return UIBezierPath()
case .head :
let centerX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let centerY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + ScaleConstants.headRadius)
let center = CGPoint(x: centerX, y: centerY)
headMiddle = center
let path = UIBezierPath(arcCenter: center, radius: ScaleConstants.headRadius, startAngle: CGFloat(0), endAngle: CGFloat(2 * M_PI), clockwise: true)
return path
case .body :
let add = CGFloat(DrawingConstants.gallowBaseHeight + DrawingConstants.gallowTipHeight + 2 * ScaleConstants.headRadius)
let startPointY = CGFloat(bounds.size.height * DrawingConstants.gallowHeight + add)
let startPointX = CGFloat(bounds.size.width * DrawingConstants.gallowAcrossScale - (DrawingConstants.gallowHeightWidth / 2))
let startPoint = CGPoint(x: startPointX, y: startPointY)
let endPoint = CGPoint(x: startPoint.x, y: startPoint.y + ScaleConstants.bodyLength)
bodyStart = startPoint
bodyEnd = endPoint
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .leftLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .rightLeg :
let startPoint = CGPoint(x: bodyEnd.x, y: bodyEnd.y)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y + ScaleConstants.limbLength)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .leftArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x - ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .rightArm :
let startPoint = CGPoint(x: bodyStart.x, y: bodyStart.y + ScaleConstants.handHeightScale * ScaleConstants.bodyLength)
let endPoint = CGPoint(x: startPoint.x + ScaleConstants.limbLength, y: startPoint.y - ScaleConstants.limbLength * ScaleConstants.handHeightScale)
let path = UIBezierPath()
path.move(to: startPoint)
path.addLine(to: endPoint)
return path
case .leftEye :
let eyeMiddle = CGPoint(x: headMiddle.x - ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
case .rightEye :
let eyeMiddle = CGPoint(x: headMiddle.x + ScaleConstants.eyeOffset, y: headMiddle.y - ScaleConstants.eyeOffset)
let path = UIBezierPath(arcCenter: eyeMiddle, radius: ScaleConstants.eyeRadius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true)
path.lineWidth = CGFloat(1)
return path
default:
return UIBezierPath()
}
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// get the drawing context and save the ground state
context.saveGState()
// add sky and grass, now they are just rectangles
context.setFillColor(UIColor.cyan.cgColor)
context.fill(sky(rect))
context.setFillColor(UIColor.green.cgColor)
context.fill(grass(rect))
context.setFillColor(UIColor.brown.cgColor)
context.addPath(gallowBase(rect))
context.addPath(gallowHeight(rect))
context.addPath(gallowAcross(rect))
context.addPath(gallowTip(rect))
context.fillPath()
context.restoreGState()
}
func gallowBase(_ rect: CGRect) -> CGPath {
let bottomLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowBaseStartScale), y: CGFloat(rect.height * DrawingConstants.grassHeightScale))
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: bottomLeftPoint.y - DrawingConstants.gallowBaseHeight)
let topRightPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowBaseEndScale), y: topLeftPoint.y)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func gallowHeight(_ rect: CGRect) -> CGPath {
let bottomLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowHeightStart), y: CGFloat(rect.height * DrawingConstants.grassHeightScale - DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: bottomLeftPoint.x + DrawingConstants.gallowHeightWidth, y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: rect.height * DrawingConstants.gallowHeight)
let topRightPoint = CGPoint(x: bottomRightPoint.x, y: topLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func gallowAcross(_ rect: CGRect) -> CGPath {
let bottomLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowHeightStart) + DrawingConstants.gallowHeightWidth, y: CGFloat(rect.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let bottomRightPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowAcrossScale), y: bottomLeftPoint.y)
let topLeftPoint = CGPoint(x: bottomLeftPoint.x, y: CGFloat(rect.height * DrawingConstants.gallowHeight))
let topRightPoint = CGPoint(x: CGFloat(bottomRightPoint.x), y: topLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func gallowTip(_ rect: CGRect) -> CGPath {
let topLeftPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowAcrossScale - DrawingConstants.gallowHeightWidth), y: CGFloat(rect.height * DrawingConstants.gallowHeight + DrawingConstants.gallowBaseHeight))
let topRightPoint = CGPoint(x: CGFloat(rect.width * DrawingConstants.gallowAcrossScale), y: topLeftPoint.y)
let bottomLeftPoint = CGPoint(x: topLeftPoint.x, y: topLeftPoint.y + DrawingConstants.gallowTipHeight)
let bottomRightPoint = CGPoint(x: topRightPoint.x, y: bottomLeftPoint.y)
let path = CGMutablePath()
path.addLines(between: [bottomLeftPoint,topLeftPoint, topRightPoint,bottomRightPoint])
path.closeSubpath()
return path
}
func grass(_ rect: CGRect) -> CGRect {
let grassRect = CGRect(x: 0, y: rect.height * DrawingConstants.grassHeightScale, width: rect.width, height: (1 - DrawingConstants.grassHeightScale) * rect.height)
return grassRect
}
func sky(_ rect: CGRect) -> CGRect {
let skyRect = CGRect(x: 0, y: 0, width: rect.width, height: DrawingConstants.grassHeightScale * rect.height)
return skyRect
}
func calculateMidPoint(point1: CGPoint, point2: CGPoint) -> CGPoint {
return CGPoint(x: (point1.x + point2.x) / 2, y: (point1.y + point2.y) / 2)
}
struct DrawingConstants {
static let gallowBaseStartScale: CGFloat = 0.15
static let gallowBaseEndScale: CGFloat = 0.85
static let gallowBaseHeight: CGFloat = 10
static let gallowHeight: CGFloat = 0.05 //static let gallowHeight: CGFloat = 0.15
static let gallowHeightStart: CGFloat = 0.175
static let gallowHeightWidth: CGFloat = 10
static let gallowAcrossScale: CGFloat = 0.5
static let gallowTipHeight: CGFloat = 17.5
static let headRadius: CGFloat = 16
static let bodyLength: CGFloat = 25
static let bodyHeight: CGFloat = 25
static let legLength: CGFloat = 50
static let grassHeightScale: CGFloat = 0.68
static let armBack: CGFloat = 5
}
struct ScaleConstants {
static let bodyLength: CGFloat = 50
static let limbLength: CGFloat = 25
static let handHeightScale: CGFloat = 0.4
static let headRadius: CGFloat = 20
static let eyeRadius = 0.15 * headRadius
static let eyeOffset = 0.3 * headRadius
static let mouthOffSet = 0.3 * headRadius
static let mouthRadius = 0.25 * headRadius
}
}
An approach like this would work. Have a property of your custom HangmanView class called incorrectGuesses. Changing that property will trigger a redraw of the view by calling setNeedsDisplay on itself.
The use of the switch statement and fallthough will allow more of the drawing to appear as the incorrectGuesses increases.
class HangmanView: UIView {
var incorrectGuesses = 0 {
didSet {
self.setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
switch incorrectGuesses {
case let x where x > 5:
drawRightLeg()
fallthrough
case 5:
drawLeftLeg()
fallthrough
case 4:
drawRightArm()
fallthrough
case 3:
drawLeftArm()
fallthrough
case 2:
drawBody()
fallthrough
case 1:
drawHead()
fallthrough
case 0:
drawGallows()
default:
break
}
}
func drawGallows() {
// code to draw gallows
}
func drawHead() {
// code to draw head
}
func drawBody() {
// code to draw body
}
func drawLeftArm() {
// code to draw left arm
}
func drawRightArm() {
// code to draw right arm
}
func drawLeftLeg() {
// code to draw left leg
}
func drawRightLeg() {
// code to draw right leg
}
}
To use it, you could have an outlet for the HangmanView in your ViewController, and you'd just set the incorrectGuesses to update the view:
class ViewController: UIViewController {
#IBOutlet weak var hangmanView: HangManView!
var incorrectGuesses = 0
func gameplay() {
...
if letter not in word {
incorrectGuesses += 1
// Update the HangmanView
hangmanView.incorrectGuesses = incorrectGuesses
}
}
}
In your Storyboard, add a UIView and set the Class to HangmanView in the Identity Inspector and then connect it to the outlet.
If you want to animate the drawing of the HangmanView, you could add an animationCount property to the HangmanView. When that is set to a value, such as 3, it will animate the drawing bit by bit every 0.5 seconds. To use it, you'd set hangmanView.animationCount = 3.
var animationCount = 0 {
didSet {
incorrectGuesses = 0
if animationCount > incorrectGuesses {
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
self.incorrectGuesses += 1
if self.incorrectGuesses >= self.animationCount {
timer.invalidate()
}
}
}
}
}

Resources