Swift ios rotate custom control from touch point

1.3k views Asked by At

I have written an custom control in Swift for IOS. The custom control is a circle with 4 segments in it, all with different colors. The goal of the control is to serve as some sort of "select tool". You rotate the circle and whatever color is on top is the selected color. The structure is as follows: There is one CircleLayer and in that CircleLayer there are 4 SegmentLayers all with different colors (See screenshot for example).

Rotating the circle works but the thing is that it's always rotating from the same point. For example when I click the circle on the red segment the circle rotates so that the green segment is where I just clicked.

Any suggestions to how I could make the control start rotating from where I clicked.

Wherever I click on the circle it's always jumps to the section in between green and blue

Current code for wheel control:

import UIKit

func degreesToRadians (value:Double) -> Double {
    return value * M_PI / 180.0
}

func radiansToDegrees (value:Double) -> Double {
    return value * 180.0 / M_PI
}

func square (value:CGFloat) -> CGFloat {
    return value * value
}

@IBDesignable public class CircleControl: UIControl, RotateStartDelegate {
    private var backingValue: CGFloat = 0.0

    /** Layer renderer **/
    private let circleRenderer = CircleRenderer()

    /** Contains the receiver’s current value. */
    public var value: CGFloat {
        get { return backingValue }
        set { setValue(newValue, animated: false) }
    }

    public var angle: Double = 0.0

    /** Set the angle of the circle */
    public func setValue(value: CGFloat, animated: Bool) {
        if(value != self.value) {
            self.backingValue = value

            // Update human-readable angle
            var a = radiansToDegrees(Double(value))
            self.angle = (a >= 0) ? a : a + 360.0

            // Set angle
            circleRenderer.setCircleAngle(value, animated: animated)
            sendActionsForControlEvents(.ValueChanged)
        }
    }

    /** Contains a Boolean value indicating whether changes in the sliders value generate continuous update events. */
    public var continuous = true

    public override init(frame: CGRect) {
        super.init(frame: frame)

        createSubLayers()

        let gr = RotationGestureRecognizer(delegate: self, target: self, action: "handleRotation:")
        self.addGestureRecognizer(gr)

        let tr = TapGestureRecognizer(target: self, action: "handleTap:")
        self.addGestureRecognizer(tr)

        self.setValue(circleRenderer.segmentMiddle, animated: false)
    }

    public required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    /** Set bounds of all the sub layers **/
    func createSubLayers() {
        circleRenderer.update(bounds)

        layer.addSublayer(circleRenderer.circleLayer)
    }

    func rotationStart(startingPoint: CGPoint, angle: CGFloat) {
        println("Rotation started")
        println(startingPoint)

//        circleRenderer.setDragStartingAngle(angle)
//        
////        circleRenderer.circleLayer.anchorPoint = startingPoint
////
////        circleRenderer.backingPointerAngle = angle
////        circleRenderer.setCircleAngle(angle, animated: true)
    }

    /** Animate to the color usert tapped on **/
    func handleTap(sender: AnyObject) {
        let tr = sender as! TapGestureRecognizer

    }

    /** Update the angle **/
    func handleRotation(sender: AnyObject) {
        let gr = sender as! RotationGestureRecognizer

        self.value = gr.rotation

        // When the gesture has stopped make a small correction to the middle of the segment
        if (gr.state == UIGestureRecognizerState.Ended) || (gr.state == UIGestureRecognizerState.Cancelled) {
            var segmentSize:Double = 360 / 4
            if self.angle >= 0.0 && self.angle < segmentSize {
                self.setValue(circleRenderer.segmentMiddle, animated: true)
            } else if self.angle >= segmentSize && self.angle < segmentSize * 2 {
                self.setValue(circleRenderer.segmentMiddle * 3, animated: true)
            } else if self.angle >= segmentSize * 2 && self.angle < segmentSize * 3 {
                self.setValue(circleRenderer.segmentMiddle * -3, animated: true)
            } else if self.angle >= segmentSize * 3 && self.angle < segmentSize * 4 {
                self.setValue(circleRenderer.segmentMiddle * -1, animated: true)
            }
        }
    }
}

private class CircleRenderer {
    /** Angle properties **/
    var backingPointerAngle: CGFloat = 0.0
    var circleAngle: CGFloat {
        get { return backingPointerAngle }
        set { setCircleAngle(newValue, animated: false) }
    }

    /** Layers **/
    let circleLayer = CALayer()
    let seg1Layer = CAShapeLayer()
    let seg2Layer = CAShapeLayer()
    let seg3Layer = CAShapeLayer()
    let seg4Layer = CAShapeLayer()

    /** Segment size **/
    let segmentSize:CGFloat = CGFloat( ( M_PI*2 ) / 4)

    /** Middle of a segment. The angle to rotate to **/
    let segmentMiddle:CGFloat = CGFloat(( M_PI*2 ) / 8)

    /** Initialize the colors **/
    init() {
        seg1Layer.fillColor = UIColor.greenColor().CGColor!
        seg2Layer.fillColor = UIColor(red: 204, green: 0, blue: 0, alpha: 100).CGColor!
        seg3Layer.fillColor = UIColor(red: 255, green: 203, blue: 0, alpha: 100).CGColor!
        seg4Layer.fillColor = UIColor.blueColor().CGColor!

        circleLayer.opaque = true
        circleLayer.backgroundColor = UIColor.clearColor().CGColor!
    }

    func setDragStartingAngle(angle: CGFloat) {
        // Set orientation?
    }

    /** Change the angle of the circle, animated or not animated **/
    func setCircleAngle(circleAngle: CGFloat, animated: Bool) {
        CATransaction.begin()
        CATransaction.setDisableActions(true)

        circleLayer.transform = CATransform3DMakeRotation(circleAngle, 0.0, 0.0, 0.1)

        if animated {
            let midAngle = (max(circleAngle, self.circleAngle) - min(circleAngle, self.circleAngle) ) / 2.0 + min(circleAngle, self.circleAngle)

            let animation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
            animation.duration = 0.25
            animation.values = [self.circleAngle, midAngle, circleAngle]
            animation.keyTimes = [0.0, 0.5, 1.0]

            animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
            circleLayer.addAnimation(animation, forKey: "shake")
        }

        CATransaction.commit()

        self.backingPointerAngle = circleAngle
    }

    /** Draw the segment layers paths **/
    func update() {
        let center = CGPoint(x: circleLayer.bounds.size.width / 2.0, y: circleLayer.bounds.size.height / 2.0)
        let radius:CGFloat = min(circleLayer.bounds.size.width, circleLayer.bounds.size.height) / 2 * 1

        let seg1Path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0.0, endAngle: segmentSize, clockwise: true)
        seg1Path.addLineToPoint(center)
        seg1Layer.path = seg1Path.CGPath

        let seg2Path = UIBezierPath(arcCenter: center, radius: radius, startAngle: segmentSize, endAngle: segmentSize*2, clockwise: true)
        seg2Path.addLineToPoint(center)
        seg2Layer.path = seg2Path.CGPath

        let seg3Path = UIBezierPath(arcCenter: center, radius: radius, startAngle: segmentSize*2, endAngle: segmentSize*3, clockwise: true)
        seg3Path.addLineToPoint(center)
        seg3Layer.path = seg3Path.CGPath

        let seg4Path = UIBezierPath(arcCenter: center, radius: radius, startAngle: segmentSize*3, endAngle: segmentSize*4, clockwise: true)
        seg4Path.addLineToPoint(center)
        seg4Layer.path = seg4Path.CGPath
    }

    /** Update the frame and position of the layers **/
    func update(bounds: CGRect) {
        let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)

        circleLayer.position = position
        circleLayer.bounds = bounds

        circleLayer.addSublayer(seg1Layer)
        circleLayer.addSublayer(seg2Layer)
        circleLayer.addSublayer(seg3Layer)
        circleLayer.addSublayer(seg4Layer)

        update()
    }
}

import UIKit.UIGestureRecognizerSubclass

private class TapGestureRecognizer : UITapGestureRecognizer {
    var rotation: CGFloat = 0.0
    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        super.touchesBegan(touches, withEvent: event)

        if let touch = touches[touches.startIndex] as? UITouch {
            self.rotation = rotationForLocation(touch.locationInView(self.view))
        }
    }

    func rotationForLocation(location: CGPoint) -> CGFloat {
        let offset = CGPoint(x: location.x - view!.bounds.midX, y: location.y - view!.bounds.midY)
        return atan2(offset.y, offset.x)
    }
}

protocol RotateStartDelegate {
    func rotationStart(startingPoint: CGPoint, angle: CGFloat)
}

private class RotationGestureRecognizer: UIPanGestureRecognizer {
    /** Angle of rotation **/
    var rotation: CGFloat = 0.0
    var rotationStart: CGFloat = 0.0
    var currentState: String = "idle"

    var rotationDelegate: RotateStartDelegate

    init(delegate: RotateStartDelegate, target: AnyObject, action: Selector) {
        self.rotationDelegate = delegate

        super.init(target: target, action: action)
    }

    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        super.touchesBegan(touches, withEvent: event)

        self.currentState = "begin"

        if let touch = touches[touches.startIndex] as? UITouch {
            self.rotationDelegate.rotationStart(touch.locationInView(self.view), angle: rotationForLocation(touch.locationInView(self.view)))
        }
    }

    override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
        super.touchesMoved(touches, withEvent: event)

        self.currentState = "dragging"
        updateRotationWithTouches(touches)
    }

    override func touchesEnded(touches: Set<NSObject>!, withEvent event: UIEvent!) {
        super.touchesEnded(touches, withEvent: event)

        self.currentState = "idle"
    }

    func updateRotationWithTouches(touches: Set<NSObject>) {
        if let touch = touches[touches.startIndex] as? UITouch {
            self.rotation = rotationForLocation(touch.locationInView(self.view))
        }
    }

    func rotationForLocation(location: CGPoint) -> CGFloat {
        let offset = CGPoint(x: location.x - view!.bounds.midX, y: location.y - view!.bounds.midY)
        return atan2(offset.y, offset.x)
    }
}
0

There are 0 answers