Detecting Touch on Bezier Path is Incorrect - swift

I draw a segmented circle using UIBezier Path with this code:
func createCircleWithStartAndgle(startAngle: CGFloat, endAngle: CGFloat, chartSection: Int) {
var subView = circleView
var radius: CGFloat = circleView.bounds.size.width / 2 - 20
var circleShapeLayer = CAShapeLayer()
var arcCenter = CGPoint(x: radius, y: radius)
var bezierPath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
circleShapeLayer.path = bezierPath.cgPath
circleShapeLayer.position = CGPoint(x:(subView?.bounds.midX)! - radius , y: (subView?.bounds.midY)! - radius)
circleShapeLayer.fillColor = UIColor.clear.cgColor
var strokeColor = UIColor()
switch chartSection {
case 0:
strokeColor = .red
firstLayer = circleShapeLayer
fPath = bezierPath
case 1:
strokeColor = .yellow
secondLayer = circleShapeLayer
sPath = bezierPath
case 2:
strokeColor = .blue
thirdLayer = circleShapeLayer
thPath = bezierPath
default:
strokeColor = .gray
}
circleShapeLayer.strokeColor = strokeColor.cgColor
circleShapeLayer.lineWidth = 10
subView?.layer.insertSublayer(circleShapeLayer, at: UInt32(chartSection))
}
Which gave me a satisfied result:
Now I want to be able to touch on each segment and identify the segment, and make it bigger.
When I use this code to detect touches, it works fine for red and yellow segments (first and second path), but in order to identify blue segment, I need to tap on left upper corner or a little bit up of the blue segment. If I tap on the segment itself, it detects it as it is a first segment, not third.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
guard let point = touch?.location(in: circleView) else { return }
guard let sublayers = circleView.layer.sublayers as? [CAShapeLayer] else { return }
for layer in sublayers {
if let path = layer.path {
if path == fPath.cgPath && path.contains(point) {
print("first segment")
} else if path == sPath.cgPath && path.contains(point) {
print("second segment")
} else if path == thPath.cgPath && path.contains(point) {
print("third segment")
}
}
}
}
What is wrong? How can I correctly detect taps and modify the strokWidth of tapped bezierPath?

Related

How to use the Zposition of UIBezierPath() and SKNode?

I have a UIView where i draw lines with UIBezierPath() and where i have SKSHapeNode child. Sometimes, the drawing is superimposed with the nodes. When it is the case, i want the node to be upside of the drawing. But even wehn the nodes have a bigger Zpos, the drawing is upside nodes.
I use this script when i want to draw a line :
private func addLine(fromPoint start: CGPoint, toPoint end:CGPoint) {
let line = CAShapeLayer()
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
line.zPosition = 80
line.path = linePath.cgPath
line.strokeColor = UIColor.black.cgColor
line.lineWidth = 5.0
line.lineJoin = CAShapeLayerLineJoin.round
self.view!.layer.addSublayer(line)
}
And i use this script for the SKSHapeNodes :
private func drawBase(map : Int) {
for i in 0...maps.xMaps[map].count - 1 {
let team = maps.mapTeam[map][i]
let weight = CGFloat(game!.baseWeight[i])
let base = SKShapeNode(rectOf: CGSize(width: weight, height: weight))
base.zPosition = 90
base.position = CGPoint(x: maps.xMaps[map][i], y: maps.yMaps[map][i])
if team == 0 {
base.fillColor = .blue
} else if team == 1 {
base.fillColor = .red
} else if team == 2 {
base.fillColor = .green
} else if team == 3 {
base.fillColor = .purple
}
mapEntity.append(base)
self.addChild(base)
}
}
I call the function here :
func drawMap(map : Int){
drawRoad(map: map)
drawBase(map: map)
}
DrawRoad is the function that draw lines :
private func drawRoad(map : Int) {
for i in 0...maps.xRoad[map].count - 1 {
let start = CGPoint(x: maps.xRoad[map][i][0], y: self.frame.height - maps.yRoad[map][i][0])
let end = CGPoint(x: maps.xRoad[map][i][1], y: self.frame.height - maps.yRoad[map][i][1])
addLine(fromPoint: start, toPoint: end)
}
}
As you can see i draw the line before adding the SKShapeNode but it do not work...

Change color gradient around donut view

I am trying to make an animated donut view that when given a value between 0 and 100 it will animate round the view up to that number. I have this working fine but want to fade the color from one to another, then another on the way around. Currently, when I add my gradient it goes from left to right and not around the circumference of the donut view.
class CircleScoreView: UIView {
private let outerCircleLayer = CAShapeLayer()
private let outerCircleGradientLayer = CAGradientLayer()
private let outerCircleLineWidth: CGFloat = 5
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
buildLayers()
}
/// Value must be within 0...100 range
func setScore(_ value: Int, animated: Bool = false) {
if value != 0 {
let clampedValue: CGFloat = CGFloat(value.clamped(to: 0...100)) / 100
if !animated {
outerCircleLayer.strokeEnd = clampedValue
} else {
let outerCircleAnimation = CABasicAnimation(keyPath: "strokeEnd")
outerCircleAnimation.duration = 1.0
outerCircleAnimation.fromValue = 0
outerCircleAnimation.toValue = clampedValue
outerCircleAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
outerCircleLayer.strokeEnd = clampedValue
outerCircleLayer.add(outerCircleAnimation, forKey: "outerCircleAnimation")
}
outerCircleGradientLayer.colors = [Constant.Palette.CircleScoreView.startValue.cgColor,
Constant.Palette.CircleScoreView.middleValue.cgColor,
Constant.Palette.CircleScoreView.endValue.cgColor]
}
}
private func buildLayers() {
// Outer background circle
let arcCenter = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
let startAngle = CGFloat(-0.5 * Double.pi)
let endAngle = CGFloat(1.5 * Double.pi)
let circlePath = UIBezierPath(arcCenter: arcCenter,
radius: (frame.size.width - outerCircleLineWidth) / 2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
// Outer circle
setupOuterCircle(outerCirclePath: circlePath)
}
private func setupOuterCircle(outerCirclePath: UIBezierPath) {
outerCircleLayer.path = outerCirclePath.cgPath
outerCircleLayer.fillColor = UIColor.clear.cgColor
outerCircleLayer.strokeColor = UIColor.black.cgColor
outerCircleLayer.lineWidth = outerCircleLineWidth
outerCircleLayer.lineCap = CAShapeLayerLineCap.round
outerCircleGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
outerCircleGradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
outerCircleGradientLayer.frame = bounds
outerCircleGradientLayer.mask = outerCircleLayer
layer.addSublayer(outerCircleGradientLayer)
}
}
I am going for something like this but the color isn't one block but gradients around the donut view from one color to the next.
If you imported AngleGradientLayer into your project then all you should need to do is change:
private let outerCircleGradientLayer = CAGradientLayer() to
private let outerCircleGradientLayer = AngleGradientLayer()

Connect points in grid with lines

I need to put line that will connect every 2 points in the grid when someone tap between those points , so they can be connected . I manage to create the point grid :
func drawPointGrid() {
let points: CGFloat = 5
let cellWidth = bounds.width / points
let cellHeight = bounds.height / points
for i in 0..<Int(points) {
for j in 0..<Int(points) {
let circleX: CGFloat = ((CGFloat(i) + 0.5) * cellWidth)
let circleY: CGFloat = ((CGFloat(j) + 0.5) * cellHeight)
let centerCirclePath = UIBezierPath(ovalIn: CGRect(x: circleX, y: circleY, width: diameter, height: diameter))
let customlayer = CAShapeLayer()
customlayer.path = centerCirclePath.cgPath
customlayer.fillColor = UIColor.black.cgColor
layer.addSublayer(customlayer)
}
}
}
This is my visual point grid :
I manage to make line on the view , but only when I click for start point and click again for end point, so the line to be created , but I need this line to be created between every 2 points on horizontal and vertical when user tap between them :
override func draw(_ rect: CGRect) {
drawPointGrid()
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showMoreActions))
tapGestureRecognizer.numberOfTapsRequired = 1
addGestureRecognizer(tapGestureRecognizer)
}
// draw line from point to point that are clicked
var firstPoint: CGPoint?
var secondPoint: CGPoint?
func showMoreActions(touch: UITapGestureRecognizer) {
let touchPoint = touch.location(in: self)
guard let _ = firstPoint else {
firstPoint = touchPoint
return
}
guard let _ = secondPoint else {
secondPoint = touchPoint
addLine(start: firstPoint!,end: secondPoint!)
firstPoint = nil
secondPoint = nil
return
}
}
func addLine(start: CGPoint,end:CGPoint) {
let line = CAShapeLayer()
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
line.path = linePath.cgPath
line.strokeColor = UIColor.black.cgColor
line.lineWidth = 2
line.lineJoin = kCALineJoinRound
layer.addSublayer(line)
}
I've written a function called findEndPoints that finds the CGPoint coordinates of the proper line end points given a touchPoint in the view. I did most of the work as type Double to keep from having to worry about converting between that and CGFloat.
I also moved the setup of the touchGestureRecognizer to a setup routine that is called from the inits since you only want to do that once and draw could be called more than once.
class DotsView: UIView {
let diameter = CGFloat(5)
func setup() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showMoreActions))
tapGestureRecognizer.numberOfTapsRequired = 1
addGestureRecognizer(tapGestureRecognizer)
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func draw(_ rect: CGRect) {
drawPointGrid()
}
// draw line between points
func showMoreActions(touch: UITapGestureRecognizer) {
let touchPoint = touch.location(in: self)
let (start, end) = findEndPoints(touchPt: touchPoint)
addLine(start: start, end: end)
}
func addLine(start: CGPoint,end:CGPoint) {
let line = CAShapeLayer()
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
line.path = linePath.cgPath
line.strokeColor = UIColor.black.cgColor
line.lineWidth = 2
line.lineJoin = kCALineJoinRound
layer.addSublayer(line)
}
func drawPointGrid() {
let points: CGFloat = 5
let diameter: CGFloat = 5
let cellWidth = bounds.width / points
let cellHeight = bounds.height / points
for i in 0..<Int(points) {
for j in 0..<Int(points) {
let circleX: CGFloat = ((CGFloat(i) + 0.5) * cellWidth)
let circleY: CGFloat = ((CGFloat(j) + 0.5) * cellHeight)
let centerCirclePath = UIBezierPath(ovalIn: CGRect(x: circleX, y: circleY, width: diameter, height: diameter))
let customlayer = CAShapeLayer()
customlayer.path = centerCirclePath.cgPath
customlayer.fillColor = UIColor.black.cgColor
layer.addSublayer(customlayer)
}
}
}
func findEndPoints(touchPt: CGPoint) -> (pt1: CGPoint, pt2: CGPoint) {
let points = 5.0
let cellWidth = Double(bounds.width) / points
let cellHeight = Double(bounds.height) / points
// convert touch point to grid coordinates
let gridX = Double(touchPt.x) / cellWidth - 0.5
let gridY = Double(touchPt.y) / cellHeight - 0.5
// snap to nearest point in the grid
let snapX = round(gridX)
let snapY = round(gridY)
// find distance from touch to snap point
let distX = abs(gridX - snapX)
let distY = abs(gridY - snapY)
// start second point on top of first
var secondX = snapX
var secondY = snapY
if distX < distY {
// this is a vertical line
if secondY > gridY {
secondY -= 1
} else {
secondY += 1
}
} else {
// this is a horizontal line
if secondX > gridX {
secondX -= 1
} else {
secondX += 1
}
}
let halfdot = Double(diameter) / 2
// convert line points to view coordinates
let pt1 = CGPoint(x: (snapX + 0.5) * cellWidth + halfdot, y: (snapY + 0.5) * cellHeight + halfdot)
let pt2 = CGPoint(x: (secondX + 0.5) * cellWidth + halfdot, y: (secondY + 0.5) * cellHeight + halfdot)
return (pt1, pt2)
}
}

How to make a custom semi-circular progress indicator with terminator?

I'd like to make a custom semi-circular inverted progress indicator that looks like this:
I've tried with CAShapeLayer for background, progress line and progress line terminator (small white circle). I'm using ring.strokeEnd = progress to define where the progress line ends.
How to add the line terminator to the end of progress line?
Here's my code (note: I've hardcoded the position for the small white circle, as I don't know how to properly implement this):
class AKBezierson: UIView {
var ringBackground: CAShapeLayer!
var ring: CAShapeLayer!
var ringTerminator: CAShapeLayer!
var innerRect: CGRect!
var radius: CGFloat { return min(innerRect.width, innerRect.height) / 2 }
var progress: CGFloat = 0.8 { didSet { updateLayerProperties() } }
private struct Const {
static let gap: CGFloat = 10
static let width: CGFloat = 3.0
}
override func layoutSubviews() {
super.layoutSubviews()
innerRect = CGRectInset(bounds, Const.width / 2 + Const.gap, Const.width / 2 + Const.gap)
let path = UIBezierPath()
path.addArcWithCenter(
CGPointMake(bounds.width / 2, bounds.height / 2),
radius: radius,
startAngle: CGFloat(0),
endAngle: CGFloat(M_PI),
clockwise: true)
if (ringBackground == nil) {
ringBackground = CAShapeLayer()
layer.addSublayer(ringBackground)
ringBackground.path = path.CGPath
ringBackground.fillColor = nil
ringBackground.lineWidth = Const.width
ringBackground.strokeColor = UIColor(white: 1.0, alpha: 0.5).CGColor
}
ringBackground.frame = ringBackground.bounds
if (ring == nil) {
ring = CAShapeLayer()
layer.addSublayer(ring)
ring.path = path.CGPath
ring.fillColor = nil
ring.lineWidth = Const.width
ring.strokeColor = UIColor.whiteColor().CGColor
ring.strokeEnd = progress
}
ring.frame = ring.bounds
if (ringTerminator == nil) { // small circle
ringTerminator = CAShapeLayer()
layer.addSublayer(ringTerminator)
let terminatorCircle = UIBezierPath(ovalInRect: CGRectMake(15, 83, 12, 12))
ringTerminator.path = terminatorCircle.CGPath
ringTerminator.fillColor = UIColor.whiteColor().CGColor
ringTerminator.lineWidth = Const.width
ringTerminator.strokeColor = UIColor.whiteColor().CGColor
}
ringTerminator.frame = ringTerminator.bounds
updateLayerProperties()
}
func updateLayerProperties () {
if !(ring == nil) {
ring.strokeEnd = progress
}
}
}
I've also tried to calculate the point where the progress line ends and move the line terminator to a new position every time the progress changed, but I was not satisfied with the animation since the terminator was trying to reach the new position following the shortest (strait) path.

moving pacman in swift

i am brand new to swift and i am trying to program a pacman. i am trying to move the pacman to the direction of the swipe, so far i have managed to move it to the edges of the screen, the problem is that when i try to move it not from the edge of the screen but in the middle of the swipe action, it just goes to the edge of the screen and moves to the swipe direction, here is the code for one direction:
var x = view.center.x
for var i = x; i > 17; i--
{
var origin: CGPoint = self.view.center
var move = CABasicAnimation(keyPath:"position.x")
move.speed = 0.13
move.fromValue = NSValue(nonretainedObject: view.center.x)
move.toValue = NSValue(nonretainedObject: i)
view.layer.addAnimation(move, forKey: "position")
view.center.x = i
}
the thing is that i know the problem which is when i swipe to the direction that i want the for loop will not wait for the animation to stop but it will finish the loop in less than a second and i need sort of delay here or other code.
This was an interesting question, so I decided to make an example in SpriteKit. There isn't any collision detection, path finding or indeed even paths. It is merely an example of how to make 'Pac-Man' change direction when a swipe occurs.
I have included the GameScene below:
class GameScene: SKScene {
enum Direction {
case Left
case Right
case Up
case Down
}
lazy var openDirectionPaths = [Direction: UIBezierPath]()
lazy var closedDirectionPaths = [Direction: UIBezierPath]()
lazy var wasClosedPath = false
lazy var needsToUpdateDirection = false
lazy var direction = Direction.Right
lazy var lastChange: NSTimeInterval = NSDate().timeIntervalSince1970
var touchBeganPoint: CGPoint?
let pacmanSprite = SKShapeNode(circleOfRadius: 15)
override func didMoveToView(view: SKView) {
let radius: CGFloat = 15, diameter: CGFloat = 30, center = CGPoint(x:radius, y:radius)
func createPaths(startDegrees: CGFloat, endDegrees: CGFloat, inout dictionary dic: [Direction: UIBezierPath]) {
var path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startDegrees.toRadians(), endAngle: endDegrees.toRadians(), clockwise: true)
path.addLineToPoint(center)
path.closePath()
dic[.Right] = path
for d: Direction in [.Up, .Left, .Down] {
path = path.pathByRotating(90)
dic[d] = path
}
}
createPaths(35, 315, dictionary: &openDirectionPaths)
createPaths(1, 359, dictionary: &closedDirectionPaths)
pacmanSprite.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
pacmanSprite.fillColor = UIColor.yellowColor()
pacmanSprite.lineWidth = 2
if let path = openDirectionPaths[.Right] {
pacmanSprite.path = path.CGPath
}
pacmanSprite.strokeColor = UIColor.blackColor()
self.addChild(pacmanSprite)
updateDirection()
// Blocks to stop 'Pacman' changing direction outside of a defined path?
//375/25 = 15 width
//666/37 = 18 height
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
touchBeganPoint = positionOfTouch(inTouches: touches)
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
if let touchStartPoint = touchBeganPoint,
touchEndPoint = positionOfTouch(inTouches: touches) {
if touchStartPoint == touchEndPoint {
return
}
let degrees = atan2(touchStartPoint.x - touchEndPoint.x,
touchStartPoint.y - touchEndPoint.y).toDegrees()
var oldDirection = direction
switch Int(degrees) {
case -135...(-45): direction = .Right
case -45...45: direction = .Down
case 45...135: direction = .Left
default: direction = .Up
}
if (oldDirection != direction) {
needsToUpdateDirection = true
}
}
}
override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
touchBeganPoint = nil
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if let nodes = self.children as? [SKShapeNode] {
for node in nodes {
let p = node.position
let s = node.frame.size
//let s = node.size
if p.x - s.width > self.size.width {
node.position.x = -s.width
}
if p.y - s.height > self.size.height {
node.position.y = -s.height
}
if p.x < -s.width {
node.position.x = self.size.width + (s.width / 2)
}
if p.y < -s.height {
node.position.y = self.size.height + (s.height / 2)
}
if needsToUpdateDirection || NSDate().timeIntervalSince1970 - lastChange > 0.25 {
if let path = wasClosedPath ? openDirectionPaths[direction]?.CGPath : closedDirectionPaths[direction]?.CGPath {
node.path = path
}
wasClosedPath = !wasClosedPath
lastChange = NSDate().timeIntervalSince1970
}
updateDirection()
}
}
}
// MARK:- Helpers
func positionOfTouch(inTouches touches: Set<NSObject>) -> CGPoint? {
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
return location
}
return nil
}
func updateDirection() {
if !needsToUpdateDirection {
return
}
pacmanSprite.removeActionForKey("Move")
func actionForDirection() -> SKAction {
let Delta: CGFloat = 25
switch (direction) {
case .Up:
return SKAction.moveByX(0.0, y: Delta, duration: 0.1)
case .Down:
return SKAction.moveByX(0.0, y: -Delta, duration: 0.1)
case .Right:
return SKAction.moveByX(Delta, y: 0.0, duration: 0.1)
default:
return SKAction.moveByX(-Delta, y: 0.0, duration: 0.1)
}
}
let action = SKAction.repeatActionForever(actionForDirection())
pacmanSprite.runAction(action, withKey: "Move")
needsToUpdateDirection = false
}
}
The repository can be found here
I have added the MIT license, so you can fork this repository if you wish. I hope this helps.