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

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.

Related

CALayer drawing outside of view

I have this behaviour with this code:
import UIKit
class Page: UIView {
var bezierMemory = [BezierRecord]()
var currentBezier: UIBezierPath = UIBezierPath()
var firstPoint: CGPoint = CGPoint()
var previousPoint: CGPoint = CGPoint()
var morePreviousPoint: CGPoint = CGPoint()
var previousCALayer: CALayer = CALayer()
var pointCounter = 0
var selectedPen: Pen = Pen(width: 3.0, strokeOpacity: 1, strokeColor: .red, fillColor: .init(gray: 0, alpha: 0.5), isPencil: true, connectsToStart: true, fillPencil: true)
enum StandardPageSizes {
case A4, LEGAL, LETTER
}
var firstCALayer = true
var pointsTotal = 0
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else { return }
let point = touch.location(in: self)
firstPoint = point
pointCounter = 1
currentBezier = UIBezierPath()
currentBezier.lineWidth = selectedPen.width
selectedPen.getStroke().setStroke()
currentBezier.move(to: point)
previousPoint = point
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let touch = touches.first else { return }
let point = touch.location(in: self)
pointCounter += 1
if (pointCounter == 3) {
let midpoint = CGPoint(x: (morePreviousPoint.x + point.x)/2.0, y: (morePreviousPoint.y + point.y)/2.0)
currentBezier.addQuadCurve(to: midpoint, controlPoint: morePreviousPoint)
let updatedCALayer = CAShapeLayer()
updatedCALayer.path = currentBezier.cgPath
updatedCALayer.lineWidth = selectedPen.width
updatedCALayer.opacity = selectedPen.strokeOpacity
updatedCALayer.strokeColor = selectedPen.getStroke().cgColor
updatedCALayer.fillColor = selectedPen.getFill()
if (firstCALayer) {
layer.addSublayer(updatedCALayer)
firstCALayer = false
} else {
layer.replaceSublayer(previousCALayer, with: updatedCALayer)
}
previousCALayer = updatedCALayer
pointCounter = 1
}
morePreviousPoint = previousPoint
previousPoint = point
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard let touch = touches.first else { return }
let point = touch.location(in: self)
if (pointCounter != 3) {
if (selectedPen.connectsToStart) {
currentBezier.addQuadCurve(to: firstPoint, controlPoint: previousPoint)
} else {
currentBezier.addQuadCurve(to: point, controlPoint: previousPoint)
}
let updatedCALayer = CAShapeLayer()
updatedCALayer.path = currentBezier.cgPath
updatedCALayer.lineWidth = selectedPen.width
updatedCALayer.opacity = selectedPen.strokeOpacity
updatedCALayer.strokeColor = selectedPen.getStroke().cgColor
updatedCALayer.fillColor = selectedPen.getFill()
if (firstCALayer) {
layer.addSublayer(updatedCALayer)
firstCALayer = false
} else {
// layer.setNeedsDisplay()
layer.replaceSublayer(previousCALayer, with: updatedCALayer)
}
}
firstCALayer = true
let bezierRecord = BezierRecord(bezier: currentBezier, strokeColor: selectedPen.getStroke(), fillColor: selectedPen.getFill(), strokeWidth: selectedPen.width)
bezierMemory.append(bezierRecord)
}
private func normPoint(point: CGPoint) -> CGPoint {
return CGPoint(x: point.x/frame.width, y: point.y/frame.height)
}
public class BezierRecord {
var bezier: UIBezierPath
var strokeColor: UIColor
var strokeWidth: CGFloat
var fillColor: CGColor
init(bezier: UIBezierPath, strokeColor: UIColor, fillColor: CGColor, strokeWidth: CGFloat) {
self.bezier = bezier
self.strokeColor = strokeColor
self.strokeWidth = strokeWidth
self.fillColor = fillColor
}
}
}
Really the only relevant parts are touchesMoved and touchesEnded, where CALayer's are dealt with. As you can see from the gif, I can draw outside the bounds of the Page (UIView) as long as I start drawing inside the bounds. I do not want this - what I would like is something where you can maintain a stroke outside of the bounds of the Page (as long as you start it on the Page), but the stroke wont appear outside of the Page. Any ideas?
EDIT: I should add that for these UIBezierCurves, (0, 0) is considered to be the top left of the Page (white), and not the entire view. Thus, for example, beziers that start on the page and continue on, are negative.
All you should need to do is set .clipsToBounds = true on the view.
One approach:
override init(frame: CGRect) {
super.init(frame: .zero)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
self.clipsToBounds = true
}
You could put self.clipsToBounds = true in both of the init funcs, but it is common practice (no pun intended) to add a "common init" func like this (it can be named whatever... this is how I do it). Frequently we have other "initial setup" code that we want called, and this avoids duplicating the code.

How to remove only one line from LineView?

by my code I can draw multiple straight lines one by one on a view.
now what I want is, to remove only one line when click Undo button.
I have write single line to remove last draw line in undoButtonAction, but it removes whole view.
i have search a lot and trying different solutions
what can I do? please suggest some hint.
#view Controller
import UIKit
class Line {
var startPoint : CGPoint?
var endPoint : CGPoint?
init(startPoint : CGPoint?, endPoint : CGPoint?) {
self.startPoint = startPoint
self.endPoint = endPoint
}
}
class ViewController: UIViewController {
var counter = 0
var lastPosition : CGPoint?
var firstPosition : CGPoint?
let shapeLayer = CAShapeLayer()
let path = UIBezierPath()
let canvas = CanvasView()
var lines = [Line]()
func setup(){
self.view.backgroundColor = UIColor.cyan
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 2.0
self.view.layer.addSublayer(shapeLayer)
let undoButton = UIButton(frame: CGRect(x: self.view.center.x - 50, y: self.view.frame.maxY - 100, width: 100, height: 30))
undoButton.setTitle("Undo", for: .normal)
undoButton.backgroundColor = .systemYellow
undoButton.addTarget(self, action: #selector(undoButtonAction), for: .touchUpInside)
self.view.addSubview(undoButton)
}
#objc func undoButtonAction(){
lines.popLast()
self.view.layer.removeFromSuperlayer()
}
override func viewDidLoad() {
super.viewDidLoad()
self.setup()
}
func drawLineFromPoint(start : CGPoint, toPoint end:CGPoint, ofColor lineColor: UIColor, inView view:UIView) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = lineColor.cgColor
shapeLayer.lineWidth = 2.0
view.layer.addSublayer(shapeLayer)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let position = touch.location(in: view)
self.firstPosition = position
self.lastPosition = position
path.move(to: firstPosition!)
path.addLine(to: lastPosition!)
print(position)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let lastlocation = touch.location(in: view)
path.removeAllPoints()
lastPosition = lastlocation
path.move(to: firstPosition!)
path.addLine(to: lastPosition!)
shapeLayer.path = path.cgPath
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let line = Line(startPoint: firstPosition, endPoint: lastPosition)
lines.append(line)
drawLineFromPoint(start: line.startPoint!, toPoint: line.endPoint!, ofColor: .systemBlue, inView: view)
}
}
#LineView
import Foundation
import UIKit
class LineView: UIView {
var points = [CGPoint]()
var lines = [[CGPoint]]()
var fpoint : CGPoint?
func clearLine(){
lines.append([CGPoint]())
}
}
Search your lines array for the line you want to remove i.e lines[2] and call removeFromSuperView()
lines[2].removeFromSuperView()
First add new variable in Line
class Line {
var startPoint : CGPoint?
var endPoint : CGPoint?
var name : String?
init(startPoint : CGPoint?, endPoint : CGPoint?, name : String?) {
self.startPoint = startPoint
self.endPoint = endPoint
self.name = name
}
}
Now when you are adding a line, provide random name using below function as randomString(length: 16)
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
Also update below methods
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
var randomString : String = randomString(length: 16)
let line = Line(startPoint: firstPosition, endPoint: lastPosition, randomString)
lines.append(line)
drawLineFromPoint(start: line.startPoint!, toPoint: line.endPoint!, ofColor: .systemBlue, inView: view, name : randomString)
}
func drawLineFromPoint(start : CGPoint, toPoint end:CGPoint, ofColor lineColor: UIColor, inView view:UIView, name : String) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = lineColor.cgColor
shapeLayer.name = name
shapeLayer.lineWidth = 2.0
view.layer.addSublayer(shapeLayer)
}
So each layer, you have name identifier.
Now while doing undo, do below
#objc func undoButtonAction(){
var lastLineObject : Line = lines[lines.count-1]
guard let sublayers = self.view.layer.sublayers else { return }
if let tempLayer = sublayers.first(where: {$0.name == lastLineObject.name ?? ""}) {
print("Found it: \(layer)")
tempLayer.removeFromSuperlayer()
lines.remove(at : lines.count - 1)
}
}
Note this is just idea and not real code.

How can I select a specific circle?

I have created Circle using "draw" and can place it anywhere on view. but when I want to move a specific circle by clicking it. it selects "LAST" created circle.
how can I select a specific circle? please guide me for the same.
if anything else you need let me know.
**CircleView**
import UIKit
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
}
required init(coder aDecoder : NSCoder) {
fatalError("init(coder : ) has not been implemented")
}
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext(){
context.setLineWidth(2)
UIColor.yellow.set()
let circleCenter = CGPoint(x: frame.size.width / 2, y: frame.self.height / 2)
let circleRadius = (frame.size.width - 10) / 2
context.addArc(center: circleCenter, radius: circleRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
context.strokePath()
}
}
}
**ViewController**
import UIKit
class ViewController: UIViewController {
var circleView = CircleView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
let circleWidth = CGFloat(100)
var lastCircleCenter = CGPoint()
var currentCenter = CGPoint()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.lightGray
circleView.isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
lastCircleCenter = touch.location(in: view)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let circleCenter = touch.location(in: view)
if circleCenter == lastCircleCenter{
let circleHeight = circleWidth
circleView = CircleView(frame: CGRect(x: circleCenter.x - circleWidth / 2, y: circleCenter.y - circleWidth / 2, width: 100, height: 100))
view.addSubview(circleView)
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let location = touch.location(in: view)
circleView.center = location
}
}
how can I select a specific circle? please guide me for the same.
if anything else you need let me know.
how can I select a specific circle? please guide me for the same.
if anything else you need let me know.
If you are creating multiple circles and adding them to your view, I would suggest to keep track of the created circles in a collection. That way on each touch you can check if the coordinate matches any of the created circles. Based on that you can determine if to create a new circle or to move an existing one.
Example:
class ViewController: UIViewController {
var circleViews: [CircleView] = []
let circleWidth = CGFloat(100)
var draggedCircle: CircleView?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Do nothing if a circle is being dragged
// or if we do not have a coordinate
guard draggedCircle == nil, let point = touches.first?.location(in: view) else {
return
}
// Do not create new circle if touch is in an existing circle
// Keep the reference of the (potentially) dragged circle
if let draggedCircle = circleViews.filter({ UIBezierPath(ovalIn: $0.frame).contains(point) }).first {
self.draggedCircle = draggedCircle
return
}
// Create new circle and store in an array
let offset = circleWidth / 2
let rect = CGRect(x: point.x - offset, y: point.y - offset, width: circleWidth, height: circleWidth)
let circleView = CircleView(frame: rect)
circleViews.append(circleView)
view.addSubview(circleView)
// The newly created view can be immediately dragged
draggedCircle = circleView
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// If touches end then a circle is never dragged
draggedCircle = nil
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let draggedCircle = draggedCircle, let point = touches.first?.location(in: view) else {
return
}
draggedCircle.center = point
}
}

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

Swift ios rotate custom control from touch point

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

Resources