Related
I am implementing line graph as shown in the attached video and image with dynamic values added in the array at every 1 second, mainly I want the graph to be drawn with the animation as shown in the video attached.
Till date i have tried using the https://github.com/Boris-Em/BEMSimpleLineGraph but i am not getting required result.
OUTPUT REQUIRED VIDEO LINK this reference video is from my android app.
CURRENT OUTPUT VIDEO LINK this is the output i am getting using BEMSimpleLineGraph
NOTE:- As we can see in the current output video there is no smoothness and animation as reference video, so need to achieve same
Need to get smoothness and curves exactly as shown HERE.!!
Also i have tried building the custom file for graph taking references from links during my research time, below is the code that i have tried
class GraphCustom: UIView {
// var data: [CGFloat] = [2, 6, 12, 4, 5, 7, 5, 6, 6, 3] {
// didSet {
// setNeedsDisplay()
// }
// }
#objc var dynamicData : [CGFloat] = [0]{
didSet {
setNeedsDisplay()
}
}
let shapeLayer = CAShapeLayer()
var fromValue = CGPoint()
var toValue = CGPoint()
func coordYFor(index: Int) -> CGFloat {
return bounds.height - bounds.height * dynamicData[index] / (dynamicData.max() ?? 0)
}
#objc override func draw(_ rect: CGRect) {
let path = quadCurvedPath()
// UIColor.black.setStroke()
// path.lineWidth = 1
// path.stroke()
// let animation = CABasicAnimation(keyPath: "strokeEnd")
// let timing = CAMediaTimingFunction()
// animation.duration = 10.0
// animation.fromValue = 0
// animation.toValue = 1
// self.layer.add(animation, forKey: "strokeAnim")
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 2.0
shapeLayer.lineCap = kCALineCapRound
self.layer.addSublayer(shapeLayer)
}
#objc func drawLine(value:CGFloat){
dynamicData.append(value)
if (dynamicData.count > 3){
dynamicData.remove(at: 0)
}
print("dynamicData >>>>>>>",dynamicData)
// let path = quadCurvedPath()
//
// let animation = CABasicAnimation(keyPath: "strokeEnd")
//
//// animation.fromValue = 0.0
//// animation.byValue = 1.0
// animation.duration = 2.0
//
// animation.fillMode = kCAFillModeForwards
// animation.isRemovedOnCompletion = false
//
// shapeLayer.add(animation, forKey: "drawLineAnimation")
// UIColor.black.setStroke()
// path.lineWidth = 1
// path.stroke()
}
func quadCurvedPath() -> UIBezierPath {
let path = UIBezierPath()
let step = bounds.width / CGFloat(dynamicData.count - 1)
print("Indccccc>>>>",dynamicData.count)
var p1 = CGPoint(x: 0, y: coordYFor(index:dynamicData.count - 1 ))
path.move(to: p1)
// drawPoint(point: p1, color: UIColor.red, radius: 3)
if (dynamicData.count == 2) {
path.addLine(to: CGPoint(x: step, y: coordYFor(index: 1)))
return path
}
var oldControlP: CGPoint?
for i in 0..<dynamicData.count {
let p2 = CGPoint(x: step * CGFloat(i), y: coordYFor(index: i))
let mid = midPoint(p1: p1, p2: p2)
path.addQuadCurve(to: mid, controlPoint: controlPoint(p1: mid, p2: p1))
path.addQuadCurve(to: p2, controlPoint: controlPoint(p1: mid, p2: p2))
p1 = p2
}
/* for i in 1..<dynamicData.count {
print(">>>>>>>>>>",i)
let p2 = CGPoint(x: step * CGFloat(i), y: coordYFor(index: i))
// drawPoint(point: p2, color: UIColor.red, radius: 3)
var p3: CGPoint?
if i < dynamicData.count - 1 {
p3 = CGPoint(x: step * CGFloat(i + 1), y: coordYFor(index: i + 1))
}
let newControlP = controlPointForPoints(p1: p1, p2: p2, next: p3)
// print(" >>>>>>>>>>>>>>>>",p2,oldControlP ?? p1,newControlP ?? p2)
path.addCurve(to: p2, controlPoint1: oldControlP ?? p1, controlPoint2: newControlP ?? p2)
fromValue = oldControlP ?? p1
toValue = newControlP ?? p2
p1 = p2
oldControlP = antipodalFor(point: newControlP, center: p2)
}*/
return path;
}
func controlPoint(p1: CGPoint, p2: CGPoint) -> CGPoint {
var controlPoint = midPoint(p1: p1, p2: p2)
let diffY = abs(p2.y - controlPoint.y)
if p1.y < p2.y {
controlPoint.y += diffY
} else if p1.y > p2.y {
controlPoint.y -= diffY
}
return controlPoint
}
func midPoint(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2)
}
/// located on the opposite side from the center point
func antipodalFor(point: CGPoint?, center: CGPoint?) -> CGPoint? {
guard let p1 = point, let center = center else {
return nil
}
let newX = 2 * center.x - p1.x
let diffY = abs(p1.y - center.y)
let newY = center.y + diffY * (p1.y < center.y ? 1 : -1)
return CGPoint(x: newX, y: newY)
}
/// halfway of two points
func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
}
/// Find controlPoint2 for addCurve
/// - Parameters:
/// - p1: first point of curve
/// - p2: second point of curve whose control point we are looking for
/// - next: predicted next point which will use antipodal control point for finded
func controlPointForPoints(p1: CGPoint, p2: CGPoint, next p3: CGPoint?) -> CGPoint? {
guard let p3 = p3 else {
return nil
}
let leftMidPoint = midPointForPoints(p1: p1, p2: p2)
let rightMidPoint = midPointForPoints(p1: p2, p2: p3)
var controlPoint = midPointForPoints(p1: leftMidPoint, p2: antipodalFor(point: rightMidPoint, center: p2)!)
if p1.y.between(a: p2.y, b: controlPoint.y) {
controlPoint.y = p1.y
} else if p2.y.between(a: p1.y, b: controlPoint.y) {
controlPoint.y = p2.y
}
let imaginContol = antipodalFor(point: controlPoint, center: p2)!
if p2.y.between(a: p3.y, b: imaginContol.y) {
controlPoint.y = p2.y
}
if p3.y.between(a: p2.y, b: imaginContol.y) {
let diffY = abs(p2.y - p3.y)
controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1)
}
// make lines easier
controlPoint.x += (p2.x - p1.x) * 0.1
return controlPoint
}
func drawPoint(point: CGPoint, color: UIColor, radius: CGFloat) {
let ovalPath = UIBezierPath(ovalIn: CGRect(x: point.x - radius, y: point.y - radius, width: radius * 2, height: radius * 2))
color.setFill()
ovalPath.fill()
}
}
extension CGFloat {
func between(a: CGFloat, b: CGFloat) -> Bool {
return self >= Swift.min(a, b) && self <= Swift.max(a, b)
}
}
I have been trying to get required output since long time and have tried almost every link available on stackoverflow.
Any help would be great..!!
Thank You.
Imagine I am having a full semi-circle from 0 to Pi from the unit circle. There is a small number on the left side named min and a big number on the right side called max. There are both interchangeable inside the app depending on some factors.
Does anybody of you have a nice idea on how to draw a scale like I did in the drawing below? I would like to have longer lines for every x mod 10 = 0 and three larger ones in between. The grey circle is just for orientation.
So I started with the following piece of code:
let radius = CGFloat(40)
let dashLong = CGFloat(10)
let dashShort = CGFloat(5)
let middle = CGPoint(x: 50, y: 50)
let leftAngle = CGFloat(Double.pi)
let rightAngle = CGFloat(0)
let min = 45 //random num
let max = 117 //random num
let innerPath = UIBezierPath(arcCenter: middle, radius: radius, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
let middlePath = UIBezierPath(arcCenter: middle, radius: radius+dashShort, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
let outerPath = UIBezierPath(arcCenter: middle, radius: radius+dashLong, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
So there is a radius and also the length of the two types of dashes in the scale. I chose 45 and 117 as random integers for the extrem values of the scale. My three paths which do not need to be drawn are just an orientation on where the dashes need to be started and ended on. So for 50,60,...110 there start at the innerPath and go to the outer one, I am pretty sure that must be in the same angle for a dash on all circles.
Does anyone has a very smart idea how to continue this to calc the dashes and draw them without getting messed up code?
Here's the math for drawing a tick mark.
Let's do everything as CGFloat to keep the conversions to a minimum:
let radius: CGFloat = 40
let dashLong: CGFloat = 10
let dashShort: CGFloat 5
let middle = CGPoint(x: 50, y: 50)
let leftAngle: CGFloat = .pi
let rightAngle: CGFloat = 0
let min: CGFloat = 45 //random num
let max: CGFloat = 117 //random num
First, compute your angle.
let value: CGFloat = 50
let angle = (max - value)/(max - min) * .pi
Now compute your two points:
let p1 = CGPoint(x: middle.x + cos(angle) * radius,
y: middle.y - sin(angle) * radius)
// use dashLong for a long tick, and dashShort for a short tick
let radius2 = radius + dashLong
let p2 = CGPoint(x: middle.x + cos(angle) * radius2,
y: middle.y - sin(angle) * radius2)
Then draw a line between p1 and p2.
Note: In iOS, the coordinate system is upside down with +Y being down, which is why the sin calculation is subtracted from middle.y.
Complete Example
enum TickStyle {
case short
case long
}
class ScaleView: UIView {
// ScaleView properties. If any are changed, redraw the view
var radius: CGFloat = 40 { didSet { self.setNeedsDisplay() } }
var dashLong: CGFloat = 10 { didSet { self.setNeedsDisplay() } }
var dashShort: CGFloat = 5 { didSet { self.setNeedsDisplay() } }
var middle = CGPoint(x: 50, y: 50) { didSet { self.setNeedsDisplay() } }
var leftAngle: CGFloat = .pi { didSet { self.setNeedsDisplay() } }
var rightAngle: CGFloat = 0 { didSet { self.setNeedsDisplay() } }
var min: CGFloat = 45 { didSet { self.setNeedsDisplay() } }
var max: CGFloat = 117 { didSet { self.setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
// draw the arc
path.move(to: CGPoint(x: middle.x - radius, y: middle.y))
path.addArc(withCenter: middle, radius: radius, startAngle: leftAngle, endAngle: rightAngle, clockwise: true)
let startTick = ceil(min / 2.5) * 2.5
let endTick = floor(max / 2.5) * 2.5
// add tick marks every 2.5 units
for value in stride(from: startTick, through: endTick, by: 2.5) {
let style: TickStyle = value.truncatingRemainder(dividingBy: 10) == 0 ? .long : .short
addTick(at: value, style: style, to: path)
}
// stroke the path
UIColor.black.setStroke()
path.stroke()
}
// add a tick mark at value with style to path
func addTick(at value: CGFloat, style: TickStyle, to path: UIBezierPath) {
let angle = (max - value)/(max - min) * .pi
let p1 = CGPoint(x: middle.x + cos(angle) * radius,
y: middle.y - sin(angle) * radius)
var radius2 = radius
if style == .short {
radius2 += dashShort
} else if style == .long {
radius2 += dashLong
}
let p2 = CGPoint(x: middle.x + cos(angle) * radius2,
y: middle.y - sin(angle) * radius2)
path.move(to: p1)
path.addLine(to: p2)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view = ScaleView(frame: CGRect(x: 50, y: 50, width: 100, height: 60))
view.backgroundColor = .yellow
self.view.addSubview(view)
}
}
Picture of scale running in app:
My suggestion is Draw this semi circle in CALayer and Draw lines from centre of the semi circle in different CALayer and Mask both of them so that It appears Like this
I got a center CGPoint and radius Float, I need to get N number of points surrounding the circle, for example in below image how to get the 12 points corresponding red dots.
This is my incomplete function:
func getCirclePoints(centerPoint point: CGPoint, and radius: CGFloat, n: Int) [CGPoint] {
let result: [CGPoint] = stride(from: 0.0, to: 360.0, by: CGFloat(360 / n)).map {
let bearing = $0 * .pi / 180
// NO IDEA WHERE TO MOVE FURTHER
}
return result
}
getCirclePoints(centerPoint: CGPoint(x: 160, y: 240), radius: 120.0, n: 12)
You were almost there!
func getCirclePoints(centerPoint point: CGPoint, radius: CGFloat, n: Int)->[CGPoint] {
let result: [CGPoint] = stride(from: 0.0, to: 360.0, by: Double(360 / n)).map {
let bearing = CGFloat($0) * .pi / 180
let x = point.x + radius * cos(bearing)
let y = point.y + radius * sin(bearing)
return CGPoint(x: x, y: y)
}
return result
}
let points = getCirclePoints(centerPoint: CGPoint(x: 160, y: 240), radius: 120.0, n: 12)
I didn't think and was very clear as an argument name so I've removed this.
Use radians instead of degrees. They are needed inside trigonometric functions
func getCirclePoints(centerPoint point: CGPoint, and radius: CGFloat, n: Int) -> [CGPoint] {
return Array(repeating: 0, count: n).enumerated().map { offset, element in
let cgFloatIndex = CGFloat(offset)
let radiansStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
let radians = radiansStep * cgFloatIndex
let x = cos(radians) * radius + point.x
let y = sin(radians) * radius + point.y
return CGPoint(x: x, y: y)
}
}
func getCirclePoints1(centerPoint point: CGPoint, and radius: CGFloat, n: Int) -> [CGPoint] {
var resultPoints: [CGPoint] = []
let radianStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
for radians in stride(from: CGFloat(0.0), to: CGFloat.pi * CGFloat(2.0), by: radianStep) {
let x = cos(radians) * radius + point.x
let y = sin(radians) * radius + point.y
resultPoints.append(CGPoint(x: x, y: y))
}
return resultPoints
}
func getCirclePoints2(centerPoint point: CGPoint, and radius: CGFloat, n: Int) -> [CGPoint] {
let radianStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
return stride(from: CGFloat(0.0), to: CGFloat.pi * CGFloat(2.0), by: radianStep).map { element in
let cgFloatIndex = CGFloat(element)
let radiansStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
let radians = radiansStep * cgFloatIndex
let x = cos(radians) * radius + point.x
let y = sin(radians) * radius + point.y
return CGPoint(x: x, y: y)
}
}
getCirclePoints(centerPoint: CGPoint(x: 160, y: 240), and: 120.0, n: 12)
For having a draw reference
import UIKit
let numOfItems = 10
class customView : UIView {
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
for i in 0...numOfItems
{
let angle = 360/CGFloat(numOfItems) * CGFloat(i) * .pi / 180
let rad = self.bounds.size.width/2 - 10
let x = bounds.midX + cos(angle) * rad
let y = bounds.midY + sin(angle) * rad
let circlePath = UIBezierPath(
arcCenter: CGPoint(x:x,y:y),
radius:10,
startAngle:0,
endAngle:360,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 3
shapeLayer.path = circlePath.cgPath
layer.addSublayer(shapeLayer)
}
}
}
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()
}
}
}
}
}
I'm new to swift and I currently follow screencasts from CS193P. In one episode, It show me to create smiley face using drawRect. I tried to replicate it but it's turn out It's doesn't work very well. In Original Screencast, it turn out to as well blown smiley face but In my exercise, mine is turn out to as follows;
It seem work on landscape mode though.
My Code is as follows and could you please guide me which parts is wrong ???
import UIKit
class FaceView: UIView {
var faceCenter: CGPoint {
return convertPoint(center, fromView:superview)
}
var scale:CGFloat {
return 0.9
}
private struct Scaling {
static let FaceRadiusToEyeRadiusRatio: CGFloat = 10
static let FaceRadiusToEyeOffsetRatio: CGFloat = 3
static let FaceRadiusToEyeSeperationRatio: CGFloat = 1.5
static let FaceRadiusToMouthWidthRatio: CGFloat = 1
static let FaceRadiusToMouthHeightRatio: CGFloat = 3
static let FaceRadiusToMouthOffsetRatio: CGFloat = 3
}
private enum eye {case Left, Right}
private func bezierPathForEye(whichEye: eye) -> UIBezierPath {
let eyeRadius = faceRadius / Scaling.FaceRadiusToEyeRadiusRatio
let eyeVerticalOffset = faceRadius / Scaling.FaceRadiusToEyeOffsetRatio
let eyeHorizontalSeparation = faceRadius / Scaling.FaceRadiusToEyeSeperationRatio
var eyeCenter = faceCenter
eyeCenter.y = eyeVerticalOffset
switch whichEye {
case .Left: eyeCenter.x -= eyeHorizontalSeparation / 2
case .Right: eyeCenter.x += eyeHorizontalSeparation / 2
}
let path = UIBezierPath(arcCenter: eyeCenter, radius: eyeRadius, startAngle: 0, endAngle: CGFloat(2*M_PI), clockwise: true)
path.lineWidth = lineWidth;
return path
}
private func bezierPathForSmile(fractionOfMaxSmile: Double) -> UIBezierPath {
let mouthWidth = faceRadius / Scaling.FaceRadiusToMouthWidthRatio
let mouthHeight = faceRadius / Scaling.FaceRadiusToMouthHeightRatio
let mouthVerticalOffset = faceRadius / Scaling.FaceRadiusToMouthOffsetRatio
let smileHeight = CGFloat(max(min(fractionOfMaxSmile,1),-1)) * mouthHeight
let start = CGPoint(x:faceCenter.x - mouthWidth / 2 , y:faceCenter.y + mouthVerticalOffset)
let end = CGPoint(x:start.x + mouthWidth, y: start.y)
let cp1 = CGPoint(x:start.x + mouthWidth / 3, y: start.y + smileHeight)
let cp2 = CGPoint(x:end.x - mouthWidth/3, y:cp1.y)
let path = UIBezierPath()
path.moveToPoint(start)
path.addCurveToPoint(end, controlPoint1: cp1, controlPoint2: cp2)
path.lineWidth = lineWidth
return path
}
var faceRadius: CGFloat {
return min(bounds.size.width,bounds.size.height) / 2 * scale
}
var lineWidth: CGFloat = 3 { didSet { setNeedsDisplay() } }
var color: UIColor = UIColor.blueColor() { didSet { setNeedsDisplay() } }
override func drawRect(rect: CGRect) {
let facePath = UIBezierPath(arcCenter: faceCenter, radius: faceRadius, startAngle: 0, endAngle: CGFloat(2*M_PI), clockwise: true)
facePath.lineWidth = 3
color.set()
facePath.stroke()
bezierPathForEye(.Left).stroke()
bezierPathForEye(.Right).stroke()
let smiliness = 0.75
let smilepath = bezierPathForSmile(smiliness)
smilepath.stroke()
}
}
FYI, That's not an assignment and I just trying to replicate things for exercising.
Change from
eyeCenter.y = eyeVerticalOffset
into this
eyeCenter.y -= eyeVerticalOffset