Swift ios rotate custom control from touch point - ios

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

Related

Rotate collectionView in circle following user direction

I am trying create collectionView with circuler layout and I want the collectionView to rotate in circle as the user swipe his finger on screen round in whatever direction. I found the circle layout for collectionView here is what I have done so far
to rotate this collectionView I have wrote this code
add gesture to collectionView
panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.gestureReader(_:)))
panGesture.cancelsTouchesInView = false
self.collectionView.addGestureRecognizer(panGesture)
here is the gestureReader and animation methods
#objc private func gestureReader(_ gesture: UIPanGestureRecognizer) {
var startLocation = CGPoint.zero
var endLocation = CGPoint.zero
let currentLocation = gesture.location(in: self.collectionView)
if gesture.state == .began {
startLocation = currentLocation
}
if gesture.state == .ended {
endLocation = currentLocation
self.startRotatingView(start: startLocation, end: endLocation)
}
}
private func startRotatingView(start:CGPoint, end: CGPoint) {
let dx = end.x - start.x
let dy = end.y - start.y
let distance = abs(sqrt(dx*dx + dy*dy))
print(distance)
if start.x > end.x {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}else {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}
}
private func circleAnimation(_ angle:CGFloat) {
UIView.animate(withDuration: 0.7, delay: 0, options: .curveLinear, animations: {
self.collectionView.transform = CGAffineTransform.identity
self.collectionView.transform = CGAffineTransform.init(rotationAngle: angle)
}) { (true) in
//
}
}
First the animation is not working properly and second when collectionView gets rotated this is what I get
Question1 : What else do I need to add to make this animation smooth and follow user's finger?
Question2 : I want the collectionViewcells to stay as before animation, how can I achieve this, please help
Thanks in advance
I show you an example here. The decor View S1View is a subclass of UICollectionViewCell with the identifier "background".
The code is not hard to understand but tedious to put together. How to control animator is another story.
class TestCollectionViewLayout: UICollectionViewLayout {
lazy var dataSource : UICollectionViewDataSource? = {
self.collectionView?.dataSource
}()
var layouts : [IndexPath: UICollectionViewLayoutAttributes?] = [:]
var itemNumber : Int {
return dataSource!.collectionView(collectionView!, numberOfItemsInSection: 0)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?{
var itemArray = (0..<itemNumber).map{ self.layoutAttributesForItem(at: IndexPath.init(row: $0, section: 0))!}
itemArray.append(self.layoutAttributesForDecorationView(ofKind:"background"
, at: IndexPath.init(row: 0, section: 0)))
return itemArray
}
override var collectionViewContentSize: CGSize { get{
return self.collectionView?.frame.size ?? CGSize.zero
}
}
lazy var dynamicAnimator = {UIDynamicAnimator(collectionViewLayout: self)}()
private func updateCurrentLayoutAttributesForItem(at indexPath: IndexPath, current: UICollectionViewLayoutAttributes?) -> UICollectionViewLayoutAttributes?{
return current
}
private func initLayoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?{
let layoutAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let center = (collectionView?.center)!
let angle = (CGFloat(indexPath.row) / CGFloat(itemNumber) * CGFloat.pi * 2)
layoutAttributes.center = CGPoint.init(x: center.x + cos(angle) * CGFloat(radius) , y: center.y + sin(angle) * CGFloat(radius) )
layoutAttributes.bounds = CGRect.init(x: 0, y: 0, width: 100, height: 100 )
if let decorator = self.decorator {
let itemBehavior =
UIAttachmentBehavior.pinAttachment(with: layoutAttributes, attachedTo: decorator, attachmentAnchor: layoutAttributes.center)
dynamicAnimator.addBehavior(itemBehavior)
layouts[indexPath] = layoutAttributes
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?{
guard let currentLayout = layouts[indexPath] else {
return initLayoutAttributesForItem(at:indexPath)}
return currentLayout
}
private let radius = 200
private var decorator: UICollectionViewLayoutAttributes?
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes{
guard let decorator = self.decorator else {
let layoutAttributes = UICollectionViewLayoutAttributes.init(forDecorationViewOfKind: elementKind, with: indexPath)
layoutAttributes.center = (self.collectionView?.center)!
layoutAttributes.bounds = CGRect.init(x: 0, y: 0, width: radius, height: radius)
self.decorator = layoutAttributes
return layoutAttributes
}
return decorator
}
lazy var s: UIDynamicItemBehavior = {
let decorator = self.decorator!
let s = UIDynamicItemBehavior.init(items: [decorator])
s.angularResistance = 1
dynamicAnimator.addBehavior(s)
return s
}()
func rotate(_ speed: CGFloat){
guard let decorator = self.decorator else {return}
s.addAngularVelocity(speed, for: decorator)
}
}
class TestCollectionViewController: UICollectionViewController {
var startLocation = CGPoint.zero
var endLocation = CGPoint.zero
#objc private func gestureReader(_ gesture: UIPanGestureRecognizer) {
let currentLocation = gesture.location(in: self.collectionView)
if gesture.state == .began {
startLocation = currentLocation
}
else if gesture.state == .ended {
endLocation = currentLocation
self.startRotatingView(start: startLocation, end: endLocation)
}
}
private func startRotatingView(start:CGPoint, end: CGPoint) {
let dx = end.x - start.x
let dy = end.y - start.y
let distance = abs(sqrt(dx*dx + dy*dy))
if start.x < end.x {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}else {
if start.y > end.y {
//positive value of pi
self.circleAnimation(-distance)
}else {
//negitive value of pi
self.circleAnimation(distance)
}
}
}
private func circleAnimation(_ angle:CGFloat) {
(collectionView.collectionViewLayout as? TestCollectionViewLayout).map{
$0.rotate(angle / 100)
}
// UIView.animate(withDuration: 0.7, delay: 0, options: .curveLinear, animations: {
// self.collectionView.transform = CGAffineTransform.identity
// self.collectionView.transform = CGAffineTransform.init(rotationAngle: angle)
// }) { (true) in
// //
// }
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { (Timer) in
// self.rotate()
// }
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = TestCollectionViewLayout()
collectionView.collectionViewLayout.register(UINib.init(nibName: "S1View", bundle: nil) , forDecorationViewOfKind: "background")
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.gestureReader(_:)))
panGesture.cancelsTouchesInView = false
self.collectionView.addGestureRecognizer(panGesture)
}
var data: [Int] = [1,2,3,4,5,6,7]
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
return cell
}
}
Maybe this tutorial will help: https://www.raywenderlich.com/1702-uicollectionview-custom-layout-tutorial-a-spinning-wheel
Your first problem is that you are rotating the whole collection view. Think of it like you are putting those circles on a piece of paper and then rotating that piece of paper. You don't want to rotate the whole collection view. You might not want to rotate the circles around a point because then the rotation affects the image and text in the circle. You just want to change the circle's position in a circular movement.
If the UICollectionView isn't working, you could ditch it and use regular UIViews and position them in a circular pattern (These functions should help: https://gist.github.com/akhilcb/8d03f1f88f87e996aec24748bdf0ce78). Once you have the views laid out in a circle then you just need to update the angle for each view as the user drags their finger. Store the previous angle on the view and add to it whatever you want when the user drags their finger. Little bit of trial and error and it shouldn't be too bad.
Update
The main reason to use collection views is if you have a lot of items and you need to reuse views like a list. If you don't need to reuse views then using a UICollectionView can be pain to understand, customize and change things. Here is a simple example of using regular views that rotate around a circle using a UIPanGestureRecognizer input.
Example:
import UIKit
class ViewController: UIViewController {
var rotatingViews = [RotatingView]()
let numberOfViews = 8
var circle = Circle(center: CGPoint(x: 200, y: 200), radius: 100)
var prevLocation = CGPoint.zero
override func viewDidLoad() {
super.viewDidLoad()
for i in 0...numberOfViews {
let angleBetweenViews = (2 * Double.pi) / Double(numberOfViews)
let viewOnCircle = RotatingView(circle: circle, angle: CGFloat(Double(i) * angleBetweenViews))
rotatingViews.append(viewOnCircle)
view.addSubview(viewOnCircle)
}
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(panGesture:)))
view.addGestureRecognizer(panGesture)
}
#objc func didPan(panGesture: UIPanGestureRecognizer){
switch panGesture.state {
case .began:
prevLocation = panGesture.location(in: view)
case .changed, .ended:
let nextLocation = panGesture.location(in: view)
let angle = circle.angleBetween(firstPoint: prevLocation, secondPoint: nextLocation)
rotatingViews.forEach({ $0.updatePosition(angle: angle)})
prevLocation = nextLocation
default: break
}
}
}
struct Circle {
let center: CGPoint
let radius: CGFloat
func pointOnCircle(angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
func angleBetween(firstPoint: CGPoint, secondPoint: CGPoint) -> CGFloat {
let firstAngle = atan2(firstPoint.y - center.y, firstPoint.x - center.x)
let secondAnlge = atan2(secondPoint.y - center.y, secondPoint.x - center.x)
let angleDiff = (firstAngle - secondAnlge) * -1
return angleDiff
}
}
class RotatingView: UIView {
var currentAngle: CGFloat
let circle: Circle
init(circle: Circle, angle: CGFloat) {
self.currentAngle = angle
self.circle = circle
super.init(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
center = circle.pointOnCircle(angle: currentAngle)
backgroundColor = .blue
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updatePosition(angle: CGFloat) {
currentAngle += angle
center = circle.pointOnCircle(angle: currentAngle)
}
}
Circle is a struct that just holds the center of all the views, how far apart you want them (radius), and helper functions for calculating the angles found in the GitHub link above.
RotatingViews are the views that rotate around the middle.

Swift - why is my Eraser tool pixilated

There are numerous questions about creating an eraser tool in CoreGraphics. I cannot find one that matches "pixilated".
Here's the situation. I'm playing with a simple drawing project. The pen tools work fine. The eraser tool is horribly pixilated. Here's a screen shot of what I mean:
Here's the drawing code I'm using (UPDATED):
// DrawingView
//
//
// Created by David DelMonte on 12/9/16.
// Copyright © 2016 David DelMonte. All rights reserved.
//
import UIKit
public protocol DrawingViewDelegate {
func didBeginDrawing(view: DrawingView)
func isDrawing(view: DrawingView)
func didFinishDrawing(view: DrawingView)
func didCancelDrawing(view: DrawingView)
}
open class DrawingView: UIView {
//initial settings
public var lineColor: UIColor = UIColor.black
public var lineWidth: CGFloat = 10.0
public var lineOpacity: CGFloat = 1.0
//public var lineBlendMode: CGBlendMode = .normal
//used for zoom actions
public var drawingEnabled: Bool = true
public var delegate: DrawingViewDelegate?
private var currentPoint: CGPoint = CGPoint()
private var previousPoint: CGPoint = CGPoint()
private var previousPreviousPoint: CGPoint = CGPoint()
private var pathArray: [Line] = []
private var redoArray: [Line] = []
var toolType: Int = 0
let π = CGFloat(M_PI)
private let forceSensitivity: CGFloat = 4.0
private struct Line {
var path: CGMutablePath
var color: UIColor
var width: CGFloat
var opacity: CGFloat
//var blendMode: CGBlendMode
init(path : CGMutablePath, color: UIColor, width: CGFloat, opacity: CGFloat) {
self.path = path
self.color = color
self.width = width
self.opacity = opacity
//self.blendMode = blendMode
}
}
override public init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = UIColor.clear
}
override open func draw(_ rect: CGRect) {
let context : CGContext = UIGraphicsGetCurrentContext()!
for line in pathArray {
context.setLineWidth(line.width)
context.setAlpha(line.opacity)
context.setLineCap(.round)
switch toolType {
case 0: //pen
context.setStrokeColor(line.color.cgColor)
context.addPath(line.path)
context.setBlendMode(.normal)
break
case 1: //eraser
context.setStrokeColor(UIColor.clear.cgColor)
context.addPath(line.path)
context.setBlendMode(.clear)
break
case 3: //multiply
context.setStrokeColor(line.color.cgColor)
context.addPath(line.path)
context.setBlendMode(.multiply)
break
default:
break
}
context.beginTransparencyLayer(auxiliaryInfo: nil)
context.strokePath()
context.endTransparencyLayer()
}
}
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
self.delegate?.didBeginDrawing(view: self)
if let touch = touches.first as UITouch! {
//setTouchPoints(touch, view: self)
previousPoint = touch.previousLocation(in: self)
previousPreviousPoint = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
let newLine = Line(path: CGMutablePath(), color: self.lineColor, width: self.lineWidth, opacity: self.lineOpacity)
newLine.path.addPath(createNewPath())
pathArray.append(newLine)
}
}
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
self.delegate?.isDrawing(view: self)
if let touch = touches.first as UITouch! {
//updateTouchPoints(touch, view: self)
previousPreviousPoint = previousPoint
previousPoint = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
let newLine = createNewPath()
if let currentPath = pathArray.last {
currentPath.path.addPath(newLine)
}
}
}
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
}
override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
}
public func canUndo() -> Bool {
if pathArray.count > 0 {return true}
return false
}
public func canRedo() -> Bool {
return redoArray.count > 0
}
public func undo() {
if pathArray.count > 0 {
redoArray.append(pathArray.last!)
pathArray.removeLast()
}
setNeedsDisplay()
}
public func redo() {
if redoArray.count > 0 {
pathArray.append(redoArray.last!)
redoArray.removeLast()
}
setNeedsDisplay()
}
public func clearCanvas() {
pathArray = []
setNeedsDisplay()
}
private func createNewPath() -> CGMutablePath {
//print(#function)
let midPoints = getMidPoints()
let subPath = createSubPath(midPoints.0, mid2: midPoints.1)
let newPath = addSubPathToPath(subPath)
return newPath
}
private func calculateMidPoint(_ p1 : CGPoint, p2 : CGPoint) -> CGPoint {
//print(#function)
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5);
}
private func getMidPoints() -> (CGPoint, CGPoint) {
//print(#function)
let mid1 : CGPoint = calculateMidPoint(previousPoint, p2: previousPreviousPoint)
let mid2 : CGPoint = calculateMidPoint(currentPoint, p2: previousPoint)
return (mid1, mid2)
}
private func createSubPath(_ mid1: CGPoint, mid2: CGPoint) -> CGMutablePath {
//print(#function)
let subpath : CGMutablePath = CGMutablePath()
subpath.move(to: CGPoint(x: mid1.x, y: mid1.y))
subpath.addQuadCurve(to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: previousPoint.x, y: previousPoint.y))
return subpath
}
private func addSubPathToPath(_ subpath: CGMutablePath) -> CGMutablePath {
//print(#function)
let bounds : CGRect = subpath.boundingBox
let drawBox : CGRect = bounds.insetBy(dx: -0.54 * lineWidth, dy: -0.54 * lineWidth)
self.setNeedsDisplay(drawBox)
return subpath
}
}
UPDATE:
I notice that each eraser touch is square. Please see the second image to show in more detail:
I then rewrote some code as suggested by Pranal Jaiswal:
override open func draw(_ rect: CGRect) {
print(#function)
let context : CGContext = UIGraphicsGetCurrentContext()!
if isEraserSelected {
for line in undoArray {
//context.beginTransparencyLayer(auxiliaryInfo: nil)
context.setLineWidth(line.width)
context.addPath(line.path)
context.setStrokeColor(UIColor.clear.cgColor)
context.setBlendMode(.clear)
context.setAlpha(line.opacity)
context.setLineCap(.round)
context.strokePath()
}
} else {
for line in undoArray {
context.setLineWidth(line.width)
context.setLineCap(.round)
context.addPath(line.path)
context.setStrokeColor(line.color.cgColor)
context.setBlendMode(.normal)
context.setAlpha(line.opacity)
context.strokePath()
}
}
}
I'm still getting the same result. I'd appreciate any more help.
I couldn't exactly look at ur code But I had done something similar in Swift 2.3 a while ago (I do understand u are looking at swift 3 but right now this is version that I have).
Here is how the drawing class works looks like.
import Foundation
import UIKit
import QuartzCore
class PRSignatureView: UIView
{
var drawingColor:CGColorRef = UIColor.blackColor().CGColor //Col
var drawingThickness:CGFloat = 0.5
var drawingAlpha:CGFloat = 1.0
var isEraserSelected: Bool
private var currentPoint:CGPoint?
private var previousPoint1:CGPoint?
private var previousPoint2:CGPoint?
private var path:CGMutablePathRef = CGPathCreateMutable()
var image:UIImage?
required init?(coder aDecoder: NSCoder) {
//self.backgroundColor = UIColor.clearColor()
self.isEraserSelected = false
super.init(coder: aDecoder)
self.backgroundColor = UIColor.clearColor()
}
override func drawRect(rect: CGRect)
{
self.isEraserSelected ? self.eraseMode() : self.drawingMode()
}
private func drawingMode()
{
if (self.image != nil)
{
self.image!.drawInRect(self.bounds)
}
let context:CGContextRef = UIGraphicsGetCurrentContext()!
CGContextAddPath(context, path)
CGContextSetLineCap(context, CGLineCap.Round)
CGContextSetLineWidth(context, self.drawingThickness)
CGContextSetStrokeColorWithColor(context, drawingColor)
CGContextSetBlendMode(context, CGBlendMode.Normal)
CGContextSetAlpha(context, self.drawingAlpha)
CGContextStrokePath(context);
}
private func eraseMode()
{
if (self.image != nil)
{
self.image!.drawInRect(self.bounds)
}
let context:CGContextRef = UIGraphicsGetCurrentContext()!
CGContextSaveGState(context)
CGContextAddPath(context, path);
CGContextSetLineCap(context, CGLineCap.Round)
CGContextSetLineWidth(context, self.drawingThickness)
CGContextSetBlendMode(context, CGBlendMode.Clear)
CGContextStrokePath(context)
CGContextRestoreGState(context)
}
private func midPoint (p1:CGPoint, p2:CGPoint)->CGPoint
{
return CGPointMake((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5)
}
private func finishDrawing()
{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0);
drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)
self.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
func clearSignature()
{
path = CGPathCreateMutable()
self.image = nil;
self.setNeedsDisplay();
}
// MARK: - Touch Delegates
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
path = CGPathCreateMutable()
let touch = touches.first!
previousPoint1 = touch.previousLocationInView(self)
currentPoint = touch.locationInView(self)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first!
previousPoint2 = previousPoint1
previousPoint1 = touch.previousLocationInView(self)
currentPoint = touch.locationInView(self)
let mid1 = midPoint(previousPoint2!, p2: previousPoint1!)
let mid2 = midPoint(currentPoint!, p2: previousPoint1!)
let subpath:CGMutablePathRef = CGPathCreateMutable()
CGPathMoveToPoint(subpath, nil, mid1.x, mid1.y)
CGPathAddQuadCurveToPoint(subpath, nil, previousPoint1!.x, previousPoint1!.y, mid2.x, mid2.y)
CGPathAddPath(path, nil, subpath);
self.setNeedsDisplay()
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.touchesMoved(touches, withEvent: event)
self.finishDrawing()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.touchesMoved(touches!, withEvent: event)
self.finishDrawing()
}
}
Source Code for test app I created using the above code
Edit: Converting few lines code to swift 3 as requested
subpath.move(to: CGPoint(x: mid1.x, y: mid1.y))
subpath.addQuadCurve(to:CGPoint(x: mid2.x, y: mid2.y) , control: CGPoint(x: previousPoint1!.x, y: previousPoint1!.y))
path.addPath(subpath)
Edit: In response to the updated Question
Here is the updated Drawing Class that must solve the issue for sure. https://drive.google.com/file/d/0B5nqEBSJjCriTU5oRXd5c2hRV28/view?usp=sharing
Issues addressed:
Line Struct did not hold the tool type associated. Whenever setNeedsDislpay() is called you redraw all the objects in pathArray and all Objects were getting redrawn with the current selected tool. I have added a new variable associatedTool to address the issue.
Use of function beginTransparencyLayer will set the blend mode to kCGBlendModeNormal. As this was common for all cases related to tooltype this was causing the mode to be set to normal. I have removed these two lines
//context.beginTransparencyLayer(auxiliaryInfo: nil)
//context.endTransparencyLayer()
Try this it has no error while erasing and it can be us for drawing erasing and clearing your screen. you can even increase or decrease ur size of the pencil and eraser. also u may change color accordingly.
hope this is helpfull for u.....
import UIKit
class DrawingView: UIView {
var lineColor:CGColor = UIColor.black.cgColor
var lineWidth:CGFloat = 5
var drawingAlpha:CGFloat = 1.0
var isEraserSelected: Bool
private var currentPoint:CGPoint?
private var previousPoint1:CGPoint?
private var previousPoint2:CGPoint?
private var path:CGMutablePath = CGMutablePath()
var image:UIImage?
required init?(coder aDecoder: NSCoder) {
//self.backgroundColor = UIColor.clearColor()
self.isEraserSelected = false
super.init(coder: aDecoder)
self.backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect)
{
self.isEraserSelected ? self.eraseMode() : self.drawingMode()
}
private func drawingMode()
{
if (self.image != nil)
{
self.image!.draw(in: self.bounds)
}
let context:CGContext = UIGraphicsGetCurrentContext()!
context.addPath(path)
context.setLineCap(CGLineCap.round)
context.setLineWidth(self.lineWidth)
context.setStrokeColor(lineColor)
context.setBlendMode(CGBlendMode.normal)
context.setAlpha(self.drawingAlpha)
context.strokePath();
}
private func eraseMode()
{
if (self.image != nil)
{
self.image!.draw(in: self.bounds)
}
let context:CGContext = UIGraphicsGetCurrentContext()!
context.saveGState()
context.addPath(path);
context.setLineCap(CGLineCap.round)
context.setLineWidth(self.lineWidth)
context.setBlendMode(CGBlendMode.clear)
context.strokePath()
context.restoreGState()
}
private func midPoint (p1:CGPoint, p2:CGPoint)->CGPoint
{
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5);
}
private func finishDrawing()
{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0);
drawHierarchy(in: self.bounds, afterScreenUpdates: true)
self.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
func clearSignature()
{
path = CGMutablePath()
self.image = nil;
self.setNeedsDisplay();
}
// MARK: - Touch Delegates
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
path = CGMutablePath()
let touch = touches.first!
previousPoint1 = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
previousPoint2 = previousPoint1
previousPoint1 = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
let mid1 = midPoint(p1: previousPoint2!, p2: previousPoint1!)
let mid2 = midPoint(p1: currentPoint!, p2: previousPoint1!)
let subpath:CGMutablePath = CGMutablePath()
subpath.move(to: CGPoint(x: mid1.x, y: mid1.y), transform: .identity)
subpath.addQuadCurve(to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: (previousPoint1?.x)!, y: (previousPoint1?.y)!))
path.addPath(subpath, transform: .identity)
self.setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.touchesMoved(touches, with: event)
self.finishDrawing()
}
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
self.touchesMoved(touches!, with: event)
self.finishDrawing()
}
}

How to restrict multiple arc layer moment in iOS/Swift?

I have requirement to develop below UI screen ios using Swift.
Requirement Screen
In above two circles each segment movement is allowed either to push or pull to other circle.
I have written below code to achieve it
import UIKit
class ArcView: UIView {
let lineWidth: CGFloat = 40
var path: UIBezierPath!
var startAngle:CGFloat?
var endAngle:CGFloat?
var radius:CGFloat?
var strokeColor:UIColor?
var arcCenter:CGPoint?
var tapPoint:CGPoint?
var destPoint:CGPoint?
var initialCenter:CGPoint?
override init(frame: CGRect) {
super.init(frame: frame)
}
func setProps(startAngle:CGFloat,endAngle:CGFloat,center:CGPoint, radius:CGFloat,color:UIColor) {
self.radius=radius
self.startAngle=startAngle
self.endAngle=endAngle
self.strokeColor=color
self.arcCenter=center
self.backgroundColor = UIColor.clearColor()
initGestureRecognizers()
self.initialCenter=self.center
}
func initGestureRecognizers() {
let panGR = UIPanGestureRecognizer(target: self, action: "didPan:")
addGestureRecognizer(panGR)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
tapPoint = touch!.locationInView(self)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.reverse().first
destPoint = touch!.locationInView(self)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.reverse().first
destPoint = touch!.locationInView(self)
}
func didPan(panGR: UIPanGestureRecognizer) {
self.superview!.bringSubviewToFront(self)
var translation = panGR.translationInView(self)
translation = CGPointApplyAffineTransform(translation, self.transform)
updateLocation(translation)
panGR.setTranslation(CGPointZero, inView: self)
// self.superview!.sendSubviewToBack(self)
}
func isMoveAllowed() -> Bool {
if(arcCenter?.distanceFrom(tapPoint!)<=((arcCenter?.distanceFrom(destPoint!))!+(destPoint?.distanceFrom(tapPoint!))!)) {
return true
}
return false
}
func updateLocation(location:CGPoint) {
if isMoveAllowed() {
self.center.x += location.x
self.center.y += location.y
}
}
override func drawRect(rect: CGRect) {
self.path = UIBezierPath(arcCenter: arcCenter!, radius: radius!, startAngle: startAngle!, endAngle:endAngle!, clockwise: true)
let circleLayer :CAShapeLayer = CAShapeLayer()
circleLayer.path = self.path.CGPath
circleLayer.lineWidth = 40
circleLayer.strokeColor = strokeColor!.CGColor
circleLayer.fillColor = UIColor.clearColor().CGColor
self.layer.addSublayer(circleLayer)
self.path.fill()
self.path.stroke()
}
}
extension CGPoint {
func distanceFrom(p:CGPoint) -> CGFloat {
return sqrt(pow((p.x - self.x),2) + pow((p.y - self.y),2))
}
}
And View Controller code is
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let center:CGPoint = CGPoint(x: 215,y: 325)
let innerRadius:CGFloat = CGFloat(55)
let outerRadius:CGFloat = CGFloat(98)
let innerCirclePath = UIBezierPath(arcCenter: center, radius: innerRadius, startAngle: CGFloat(M_PI * 1.5), endAngle:CGFloat(M_PI * 3.5), clockwise: true)
let outerCirclePath = UIBezierPath(arcCenter: center, radius: outerRadius, startAngle: CGFloat(M_PI * 1.5), endAngle:CGFloat(M_PI * 3.5), clockwise: true)
addBlankColorLayer(0,endHour: totalHours,eachMinute:eachMinute,innerCirclePath: innerCirclePath,blankColor: UIColor.grayColor())
addBlankColorLayer(0,endHour: totalHours,eachMinute:eachMinute,innerCirclePath: outerCirclePath,blankColor: UIColor.grayColor())
var eventTimes:[(CGFloat,CGFloat)] = [(0,1),(1.5,2),(2.5,3.0)]
for times in eventTimes {
let arc:ArcView=ArcView(frame:self.view.frame)
arc.setProps(times.0,endAngle:times.1,center:center, radius:outerRadius,color:UIColor.redColor())
self.view.addSubview(arc)
}
// Do any additional setup after loading the view, typically from a nib.
}
func addBlankColorLayer(startHour:CGFloat,endHour:CGFloat,eachMinute:CGFloat,innerCirclePath:UIBezierPath,blankColor:UIColor) {
let circleLayer :CAShapeLayer = CAShapeLayer()
circleLayer.path = innerCirclePath.CGPath
circleLayer.strokeStart = eachMinute*startHour*60;
circleLayer.strokeEnd = eachMinute * 60 * (endHour)
circleLayer.lineWidth = 40
circleLayer.strokeColor = blankColor.CGColor
circleLayer.fillColor = UIColor.clearColor().CGColor
view.layer.addSublayer(circleLayer)
}
}
I am able to achieve this
In this I am able to achieve Pan gesture moment also like below
But now I have multiple problems.
Unable to select Each layer/segment/view independently. What I approach I should follow to achieve it?
How to restrict Each layer/segment/view moment towards center only? Below is wrong moment image
Any help is appreciated.

Drawing class drawing straight lines instead of curved lines

I have the below code that draws lines using UIBezierPath.
The code uses addCurveToPoint which should draw curved lines using a cubic bezier path, however the end result of the code is instead drawing connected straight lines but addLineToPoint isn't being used.
What could be going on, why isn't the code drawing curved lines?
import UIKit
class DrawingView: UIView, UITextFieldDelegate {
// Modifiable values within the code
let lineWidth : CGFloat = 2.0
let lineColor = UIColor.redColor()
let lineColorAlpha : CGFloat = 0.4
let shouldAllowUserChangeLineWidth = true
let maximumUndoRedoChances = 10
var path = UIBezierPath()
var previousImages : [UIImage] = [UIImage]()
// Represents current image index
var currentImageIndex = 0
// Control points for drawing curve smoothly
private var controlPoint1 : CGPoint?
private var controlPoint2 : CGPoint?
private var undoButton : UIButton!
private var redoButton : UIButton!
private var textField : UITextField!
//MARK: Init methods
override init(frame: CGRect) {
super.init(frame: frame)
setDefaultValues()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setDefaultValues()
}
// Draw the path when needed
override func drawRect(rect: CGRect) {
if currentImageIndex > 0 {
previousImages[currentImageIndex - 1].drawInRect(rect)
}
lineColor.setStroke()
path.strokeWithBlendMode(CGBlendMode.Normal, alpha: lineColorAlpha)
}
override func layoutSubviews() {
super.layoutSubviews()
redoButton.frame = CGRectMake(bounds.size.width - 58, 30, 50, 44)
if shouldAllowUserChangeLineWidth {
textField.center = CGPointMake(center.x, 52)
}
}
func setDefaultValues() {
multipleTouchEnabled = false
backgroundColor = UIColor.whiteColor()
path.lineWidth = lineWidth
addButtonsAndField()
}
func addButtonsAndField() {
undoButton = UIButton(frame: CGRectMake(8, 30, 50, 44))
undoButton.setTitle("Undo", forState: UIControlState.Normal)
undoButton.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal)
undoButton.backgroundColor = UIColor.lightGrayColor()
undoButton.addTarget(self, action: "undoButtonTapped:", forControlEvents: UIControlEvents.TouchUpInside)
addSubview(undoButton)
redoButton = UIButton(frame: CGRectMake(bounds.size.width - 58, 30, 50, 44))
redoButton.setTitle("Redo", forState: UIControlState.Normal)
redoButton.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal)
redoButton.backgroundColor = UIColor.lightGrayColor()
redoButton.addTarget(self, action: "redoButtonTapped:", forControlEvents: UIControlEvents.TouchUpInside)
addSubview(redoButton)
if shouldAllowUserChangeLineWidth {
textField = UITextField(frame: CGRectMake(0, 0, 50, 40))
textField.backgroundColor = UIColor.lightGrayColor()
textField.center = CGPointMake(center.x, 52)
textField.keyboardType = UIKeyboardType.NumberPad
textField.delegate = self
addSubview(textField)
}
}
//MARK: Touches methods
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
// Find the start point and move the path there
endEditing(true)
let touchPoint = touches.first?.locationInView(self)
path.moveToPoint(touchPoint!)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touchPoint = touches.first?.locationInView(self)
controlPoint1 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2)
controlPoint2 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2)
path.addCurveToPoint(touchPoint!, controlPoint1: controlPoint1!, controlPoint2: controlPoint2!)
setNeedsDisplay()
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touchPoint = touches.first?.locationInView(self)
controlPoint1 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2)
controlPoint2 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2)
path.addCurveToPoint(touchPoint!, controlPoint1: controlPoint1!, controlPoint2: controlPoint2!)
savePreviousImage()
setNeedsDisplay()
// Remove all points to optimize the drawing speed
path.removeAllPoints()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
touchesEnded(touches!, withEvent: event)
}
//MARK: Selector methods
func undoButtonTapped(sender : UIButton) {
if currentImageIndex > 0 {
setNeedsDisplay()
currentImageIndex--
}
}
func redoButtonTapped(sender : UIButton) {
if currentImageIndex != previousImages.count {
setNeedsDisplay()
currentImageIndex++
}
}
//MARK: UITextFieldDelegate
func textFieldDidEndEditing(textField: UITextField) {
if let n = NSNumberFormatter().numberFromString(textField.text!) {
if n.integerValue > 0 {
path.lineWidth = CGFloat(n)
}
}
}
//MARK: Saving images for reloading when undo or redo called
private func savePreviousImage() {
UIGraphicsBeginImageContextWithOptions(bounds.size, true, UIScreen.mainScreen().scale)
lineColor.setStroke()
// Create a image with white color
let rectPath = UIBezierPath(rect: bounds)
backgroundColor?.setFill()
rectPath.fill()
if currentImageIndex > 0 {
previousImages[currentImageIndex - 1].drawInRect(bounds)
}
path.strokeWithBlendMode(CGBlendMode.Normal, alpha: lineColorAlpha)
if previousImages.count >= currentImageIndex {
previousImages.removeRange(currentImageIndex..<previousImages.count)
}
if previousImages.count >= maximumUndoRedoChances {
previousImages.removeFirst()
}
else {
currentImageIndex++
}
previousImages.append(UIGraphicsGetImageFromCurrentImageContext())
UIGraphicsEndImageContext()
}
}
There are a few issues:
You are using control points that are midpoints between the two points, resulting in line segments. You probably want to choose control points that smooth the curve. See http://spin.atomicobject.com/2014/05/28/ios-interpolating-points/.
Here is a Swift 3 implementation of a simple smoothing algorithm, as well as Swift renditions of the above Hermite and Catmull-Rom Spline approaches:
extension UIBezierPath {
/// Simple smoothing algorithm
///
/// This iterates through the points in the array, drawing cubic bezier
/// from the first to the fourth points, using the second and third as
/// control points.
///
/// This takes every third point and moves it so that it is exactly inbetween
/// the points before and after it, which ensures that there is no discontinuity
/// in the first derivative as you join these cubic beziers together.
///
/// Note, if, at the end, there are not enough points for a cubic bezier, it
/// will perform a quadratic bezier, or if not enough points for that, a line.
///
/// - parameter points: The array of `CGPoint`.
convenience init?(simpleSmooth points: [CGPoint]) {
guard points.count > 1 else { return nil }
self.init()
move(to: points[0])
var index = 0
while index < (points.count - 1) {
switch (points.count - index) {
case 2:
index += 1
addLine(to: points[index])
case 3:
index += 2
addQuadCurve(to: points[index], controlPoint: points[index-1])
case 4:
index += 3
addCurve(to: points[index], controlPoint1: points[index-2], controlPoint2: points[index-1])
default:
index += 3
let point = CGPoint(x: (points[index-1].x + points[index+1].x) / 2,
y: (points[index-1].y + points[index+1].y) / 2)
addCurve(to: point, controlPoint1: points[index-2], controlPoint2: points[index-1])
}
}
}
/// Create smooth UIBezierPath using Hermite Spline
///
/// This requires at least two points.
///
/// Adapted from https://github.com/jnfisher/ios-curve-interpolation
/// See http://spin.atomicobject.com/2014/05/28/ios-interpolating-points/
///
/// - parameter hermiteInterpolatedPoints: The array of CGPoint values.
/// - parameter closed: Whether the path should be closed or not
///
/// - returns: An initialized `UIBezierPath`, or `nil` if an object could not be created for some reason (e.g. not enough points).
convenience init?(hermiteInterpolatedPoints points: [CGPoint], closed: Bool) {
self.init()
guard points.count > 1 else { return nil }
let numberOfCurves = closed ? points.count : points.count - 1
var previousPoint: CGPoint? = closed ? points.last : nil
var currentPoint: CGPoint = points[0]
var nextPoint: CGPoint? = points[1]
move(to: currentPoint)
for index in 0 ..< numberOfCurves {
let endPt = nextPoint!
var mx: CGFloat
var my: CGFloat
if previousPoint != nil {
mx = (nextPoint!.x - currentPoint.x) * 0.5 + (currentPoint.x - previousPoint!.x)*0.5
my = (nextPoint!.y - currentPoint.y) * 0.5 + (currentPoint.y - previousPoint!.y)*0.5
} else {
mx = (nextPoint!.x - currentPoint.x) * 0.5
my = (nextPoint!.y - currentPoint.y) * 0.5
}
let ctrlPt1 = CGPoint(x: currentPoint.x + mx / 3.0, y: currentPoint.y + my / 3.0)
previousPoint = currentPoint
currentPoint = nextPoint!
let nextIndex = index + 2
if closed {
nextPoint = points[nextIndex % points.count]
} else {
nextPoint = nextIndex < points.count ? points[nextIndex % points.count] : nil
}
if nextPoint != nil {
mx = (nextPoint!.x - currentPoint.x) * 0.5 + (currentPoint.x - previousPoint!.x) * 0.5
my = (nextPoint!.y - currentPoint.y) * 0.5 + (currentPoint.y - previousPoint!.y) * 0.5
}
else {
mx = (currentPoint.x - previousPoint!.x) * 0.5
my = (currentPoint.y - previousPoint!.y) * 0.5
}
let ctrlPt2 = CGPoint(x: currentPoint.x - mx / 3.0, y: currentPoint.y - my / 3.0)
addCurve(to: endPt, controlPoint1: ctrlPt1, controlPoint2: ctrlPt2)
}
if closed { close() }
}
/// Create smooth UIBezierPath using Catmull-Rom Splines
///
/// This requires at least four points.
///
/// Adapted from https://github.com/jnfisher/ios-curve-interpolation
/// See http://spin.atomicobject.com/2014/05/28/ios-interpolating-points/
///
/// - parameter catmullRomInterpolatedPoints: The array of CGPoint values.
/// - parameter closed: Whether the path should be closed or not
/// - parameter alpha: The alpha factor to be applied to Catmull-Rom spline.
///
/// - returns: An initialized `UIBezierPath`, or `nil` if an object could not be created for some reason (e.g. not enough points).
convenience init?(catmullRomInterpolatedPoints points: [CGPoint], closed: Bool, alpha: CGFloat) {
self.init()
guard points.count > 3 else { return nil }
assert(alpha >= 0 && alpha <= 1.0, "Alpha must be between 0 and 1")
let endIndex = closed ? points.count : points.count - 2
let startIndex = closed ? 0 : 1
let kEPSILON: CGFloat = 1.0e-5
move(to: points[startIndex])
for index in startIndex ..< endIndex {
let nextIndex = (index + 1) % points.count
let nextNextIndex = (nextIndex + 1) % points.count
let previousIndex = index < 1 ? points.count - 1 : index - 1
let point0 = points[previousIndex]
let point1 = points[index]
let point2 = points[nextIndex]
let point3 = points[nextNextIndex]
let d1 = hypot(CGFloat(point1.x - point0.x), CGFloat(point1.y - point0.y))
let d2 = hypot(CGFloat(point2.x - point1.x), CGFloat(point2.y - point1.y))
let d3 = hypot(CGFloat(point3.x - point2.x), CGFloat(point3.y - point2.y))
let d1a2 = pow(d1, alpha * 2)
let d1a = pow(d1, alpha)
let d2a2 = pow(d2, alpha * 2)
let d2a = pow(d2, alpha)
let d3a2 = pow(d3, alpha * 2)
let d3a = pow(d3, alpha)
var controlPoint1: CGPoint, controlPoint2: CGPoint
if abs(d1) < kEPSILON {
controlPoint1 = point2
} else {
controlPoint1 = (point2 * d1a2 - point0 * d2a2 + point1 * (2 * d1a2 + 3 * d1a * d2a + d2a2)) / (3 * d1a * (d1a + d2a))
}
if abs(d3) < kEPSILON {
controlPoint2 = point2
} else {
controlPoint2 = (point1 * d3a2 - point3 * d2a2 + point2 * (2 * d3a2 + 3 * d3a * d2a + d2a2)) / (3 * d3a * (d3a + d2a))
}
addCurve(to: point2, controlPoint1: controlPoint1, controlPoint2: controlPoint2)
}
if closed { close() }
}
}
// Some functions to make the Catmull-Rom splice code a little more readable.
// These multiply/divide a `CGPoint` by a scalar and add/subtract one `CGPoint`
// from another.
func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
return CGPoint(x: lhs.x * rhs, y: lhs.y * CGFloat(rhs))
}
func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
return CGPoint(x: lhs.x / rhs, y: lhs.y / CGFloat(rhs))
}
func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
Here are the "simple" smoothing algorithm, "Hermite" spline, and "Catmull Rom" spline curves in red, blue, and green, respectively. As you can see, the "simple" smoothing algorithm is computationally more simple, but generally doesn't pass through many of the points (but offers a more dramatic smoothing that eliminates any unsteadiness in the stroke). The points jumping around like this are exaggerating the behavior, whereas in a standard "gesture", it offers a pretty decent smoothing effect. The splines, on the other hand smooth the curve while passing through the points in the array.
If targeting iOS 9 and later, it introduces some nice features, notably:
Coalesced touches in case the user is using a device capable of such, notably the newer iPads. Bottom line, these devices (but not the simulators for them) are capable of generating more than 60 touches per second, and thus you can get multiple touches reported for each call to touchesMoved.
Predicted touches, where the device can show you where it anticipates the user's touches will progress (resulting in less latency in your drawing).
Pulling those together, you might do something like:
var points: [CGPoint]?
var path: UIBezierPath?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
points = [touch.location(in: view)]
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if #available(iOS 9.0, *) {
if let coalescedTouches = event?.coalescedTouches(for: touch) {
points? += coalescedTouches.map { $0.location(in: view) }
} else {
points?.append(touch.location(in: view))
}
if let predictedTouches = event?.predictedTouches(for: touch) {
let predictedPoints = predictedTouches.map { $0.location(in: view) }
pathLayer.path = UIBezierPath(catmullRomInterpolatedPoints: points! + predictedPoints, closed: false, alpha: 0.5)?.cgPath
} else {
pathLayer.path = UIBezierPath(catmullRomInterpolatedPoints: points!, closed: false, alpha: 0.5)?.cgPath
}
} else {
points?.append(touch.location(in: view))
pathLayer.path = UIBezierPath(catmullRomInterpolatedPoints: points!, closed: false, alpha: 0.5)?.cgPath
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
path = UIBezierPath(catmullRomInterpolatedPoints: points!, closed: false, alpha: 0.5)
pathLayer.path = path?.cgPath
}
In this code snippet, I'm rendering the path by updating a CAShapeLayer, but if you want to render it some other way, feel free. For example, using your drawRect approach, you'd update path, and then call setNeedsDisplay().
And, the above illustrates the if #available(iOS 9, *) { ... } else { ... } syntax if you need to support iOS versions prior to 9.0, but obviously, if you are only supporting iOS 9 and later, you can remove that check and lose the else clause.
For more information, see WWDC 2015 video Advanced Touch Input on iOS.
Anyway, that yields something like:
(For Swift 2.3 rendition of the above, please see the previous version of this answer.)

Animate variable [duplicate]

On UIView you can change the backgroundColour animated. And on a UISlideView you can change the value animated.
Can you add a custom property to your own UIView subclass so that it can be animated?
If I have a CGPath within my UIView then I can animate the drawing of it by changing the percentage drawn of the path.
Can I encapsulate that animation into the subclass.
i.e. I have a UIView with a CGPath that creates a circle.
If the circle is not there it represents 0%. If the circle is full it represents 100%. I can draw any value by changing the percentage drawn of the path. I can also animate the change (within the UIView subclass) by animating the percentage of the CGPath and redrawing the path.
Can I set some property (i.e. percentage) on the UIView so that I can stick the change into a UIView animateWithDuration block and it animate the change of the percentage of the path?
I hope I have explained what I would like to do well.
Essentially, all I want to do is something like...
[UIView animateWithDuration:1.0
animations:^{
myCircleView.percentage = 0.7;
}
completion:nil];
and the circle path animate to the given percentage.
If you extend CALayer and implement your custom
- (void) drawInContext:(CGContextRef) context
You can make an animatable property by overriding needsDisplayForKey (in your custom CALayer class) like this:
+ (BOOL) needsDisplayForKey:(NSString *) key {
if ([key isEqualToString:#"percentage"]) {
return YES;
}
return [super needsDisplayForKey:key];
}
Of course, you also need to have a #property called percentage. From now on you can animate the percentage property using core animation. I did not check whether it works using the [UIView animateWithDuration...] call as well. It might work. But this worked for me:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"percentage"];
animation.duration = 1.0;
animation.fromValue = [NSNumber numberWithDouble:0];
animation.toValue = [NSNumber numberWithDouble:100];
[myCustomLayer addAnimation:animation forKey:#"animatePercentage"];
Oh and to use yourCustomLayer with myCircleView, do this:
[myCircleView.layer addSublayer:myCustomLayer];
Complete Swift 3 example:
public class CircularProgressView: UIView {
public dynamic var progress: CGFloat = 0 {
didSet {
progressLayer.progress = progress
}
}
fileprivate var progressLayer: CircularProgressLayer {
return layer as! CircularProgressLayer
}
override public class var layerClass: AnyClass {
return CircularProgressLayer.self
}
override public func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == #keyPath(CircularProgressLayer.progress),
let action = action(for: layer, forKey: #keyPath(backgroundColor)) as? CAAnimation,
let animation: CABasicAnimation = (action.copy() as? CABasicAnimation) {
animation.keyPath = #keyPath(CircularProgressLayer.progress)
animation.fromValue = progressLayer.progress
animation.toValue = progress
self.layer.add(animation, forKey: #keyPath(CircularProgressLayer.progress))
return animation
}
return super.action(for: layer, forKey: event)
}
}
/*
* Concepts taken from:
* https://stackoverflow.com/a/37470079
*/
fileprivate class CircularProgressLayer: CALayer {
#NSManaged var progress: CGFloat
let startAngle: CGFloat = 1.5 * .pi
let twoPi: CGFloat = 2 * .pi
let halfPi: CGFloat = .pi / 2
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(progress) {
return true
}
return super.needsDisplay(forKey: key)
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
UIGraphicsPushContext(ctx)
//Light Grey
UIColor.lightGray.setStroke()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let strokeWidth: CGFloat = 4
let radius = (bounds.size.width / 2) - strokeWidth
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true)
path.lineWidth = strokeWidth
path.stroke()
//Red
UIColor.red.setStroke()
let endAngle = (twoPi * progress) - halfPi
let pathProgress = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle , clockwise: true)
pathProgress.lineWidth = strokeWidth
pathProgress.lineCapStyle = .round
pathProgress.stroke()
UIGraphicsPopContext()
}
}
let circularProgress = CircularProgressView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut, animations: {
circularProgress.progress = 0.76
}, completion: nil)
There is a great objc article here, which goes into details about how this works
As well as a objc project that uses the same concepts here:
Essentially action(for layer:) will be called when an object is being animated from an animation block, we can start our own animations with the same properties (stolen from the backgroundColor property) and animate the changes.
For the ones who needs more details on that like I did:
there is a cool example from Apple covering this question.
E.g. thanks to it I found that you don't actually need to add your custom layer as sublayer (as #Tom van Zummeren suggests). Instead it's enough to add a class method to your View class:
+ (Class)layerClass
{
return [CustomLayer class];
}
Hope it helps somebody.
you will have to implement the percentage part yourself. you can override layer drawing code to draw your cgpath accroding to the set percentage value. checkout the core animation programming guide and animation types and timing guide
#David Rees answer get me on the right track, but there is one issue. In my case
completion of animation always returns false, right after animation has began.
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut, animations: {
circularProgress.progress = 0.76
}, completion: { finished in
// finished - always false
})
This is the way it've worked for me - action of animation is handled inside of CALayer.
I have also included small example how to make layer with additional properties like "color".
In this case, without initializer that copies the values, changing the color would take affect only on non-animating view. During animation it would be visble with "default setting".
public class CircularProgressView: UIView {
#objc public dynamic var progress: CGFloat {
get {
return progressLayer.progress
}
set {
progressLayer.progress = newValue
}
}
fileprivate var progressLayer: CircularProgressLayer {
return layer as! CircularProgressLayer
}
override public class var layerClass: AnyClass {
return CircularProgressLayer.self
}
}
/*
* Concepts taken from:
* https://stackoverflow.com/a/37470079
*/
fileprivate class CircularProgressLayer: CALayer {
#NSManaged var progress: CGFloat
let startAngle: CGFloat = 1.5 * .pi
let twoPi: CGFloat = 2 * .pi
let halfPi: CGFloat = .pi / 2
var color: UIColor = .red
// preserve layer properties
// without this specyfic init, if color was changed to sth else
// animation would still use .red
override init(layer: Any) {
super.init(layer: layer)
if let layer = layer as? CircularProgressLayer {
self.color = layer.color
self.progress = layer.progress
}
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(progress) {
return true
}
return super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> CAAction? {
if event == #keyPath(CircularProgressLayer.progress) {
guard let animation = action(forKey: #keyPath(backgroundColor)) as? CABasicAnimation else {
setNeedsDisplay()
return nil
}
if let presentation = presentation() {
animation.keyPath = event
animation.fromValue = presentation.value(forKeyPath: event)
animation.toValue = nil
} else {
return nil
}
return animation
}
return super.action(forKey: event)
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
UIGraphicsPushContext(ctx)
//Light Gray
UIColor.lightGray.setStroke()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let strokeWidth: CGFloat = 4
let radius = (bounds.size.width / 2) - strokeWidth
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true)
path.lineWidth = strokeWidth
path.stroke()
// Red - default
self.color.setStroke()
let endAngle = (twoPi * progress) - halfPi
let pathProgress = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle , clockwise: true)
pathProgress.lineWidth = strokeWidth
pathProgress.lineCapStyle = .round
pathProgress.stroke()
UIGraphicsPopContext()
}
}
The way to handle animations differently and copy layer properties I have found in this article:
https://medium.com/better-programming/make-apis-like-apple-animatable-view-properties-in-swift-4349b2244cea

Resources