TheTotalJim
TheTotalJim

Reputation: 77

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

Upvotes: 1

Views: 1740

Answers (2)

twiz_
twiz_

Reputation: 1198

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
}
}

Upvotes: 2

vacawama
vacawama

Reputation: 154513

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()
                }
            }
        }
    }
}

Upvotes: 1

Related Questions