UIBezierPath stroke redraws it multiple times in touchesMoved - ios

I am implementing a coloring app in Swift 3.0 which uses UIBezierPath for touch to fill coloring style. In touchesBegan I create the path:
touchFillPath = UIBezierPath()
and in touchesMoved I stroke it for each new point:
touchFillPath.addLine(to: location)
touchFillPath.stroke()
but this results in the path stroked multiple times above the same area, and hence the selected color opacity changes.
I need to stroke the path for each touchesMoved call to allow the user to see the area colored while he is moving touches.
How can I stroke the same path multiple times without overriding the same area multiple times?

After touchFillPath.stroke() you'll need to reset the path:
touchFillPath.removeAllPoints()
touchFillPath.move(to: location)

#joeybladb I have tried your solution, but it draws small segments for each 'touchesMoved' action.
So to fix this, I save all points touched in an array:
pathPoints.append(NSValue(cgPoint:location))
and after calling touchFillPath.removeAllPoints(), I add all these points again to the path:
for (index, element) in (pathPoints.enumerated())! {
let point = element.cgPointValue
if index == 0 {
touchFillPath.move(to: point)
} else {
touchFillPath.addLine(to: point)
}
}
so that the next time I call touchFillPath.stroke(), it strokes the whole path:
touchFillPath.move(to: lastPoint)
touchFillPath.addLine(to: location)
touchFillPath.stroke()

Related

How to detect intersection of a UIBezierPath and erase it

I am making an iOS drawing app and would like to add functionality where erasing a UIBezierPath will erase the whole path and not just overlay the segment with a white color.
I have an array of UIBezierPaths stored, but I am not sure how efficient it is to loop through the entire array for each touch point, detect if it intersects with the current touch point, and remove it from the array.
Any suggestions?
If I understand correctly you need to loop through path at least. They are independent after all.
An UIBezierPaths has method contains which would tell you if a point is inside the path. If that is usable then what you need to do is simply
paths = paths.filter { !$0.contains(touchPoint) }
But since this is drawing app it is safe to assume you are using stroke and not fill which will most likely not work with contains the way you want it to.
You could do the intersection manually and it should have relatively good performance up until you have too many points. But when you have too many points I wager drawing performance will be more of your concern. Still the intersection may be a bit problematic with including some stroke line width; user would need to cross the center of your line to detect a hit.
There is a way though to convert stroked path to filled path. Doing that will then enable you to use contains method. The method is called replacePathWithStrokedPath but the thing is that it only exists on CGContext (At least I have never managed to find an equivalent on UIBezierPath). So procedure to do so includes creating a context. For instance something like this will work:
static func convertStrokePathToFillPath(_ path: UIBezierPath) throws -> UIBezierPath {
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), true, 1.0)
guard let context = UIGraphicsGetCurrentContext() else { throw NSError(domain: "convertStrokePathToFillPath", code: 500, userInfo: ["dev_message":"Could not generate image context"]) }
context.addPath(path.cgPath)
context.setLineWidth(path.lineWidth)
// TODO: apply all possible settings from path to context
context.replacePathWithStrokedPath()
guard let returnedPath = context.path else { throw NSError(domain: "convertStrokePathToFillPath", code: 500, userInfo: ["dev_message":"Could not get path from context"]) }
UIGraphicsEndImageContext()
return UIBezierPath(cgPath: returnedPath)
}
So the result should look something like:
static func filterPaths(_ paths: [UIBezierPath], containingPoint point: CGPoint) -> [UIBezierPath] {
return paths.filter { !(try! convertStrokePathToFillPath($0).contains(point)) }
}

Is it possible to add new UIBezierPath object to an array each time a taps and drags on the screen so that it can be undone?

I am trying to add a UIBezierPath to an array of tuples each time the user drags their fingers on the screen to make a drawing.
I initialize my variables
var arrayListFingerBezierPath: [(UIBezierPath, CGColor)] = []
var arrayListFingerPathUndoStore: [(UIBezierPath, CGColor)] = []
and in touchesBegan I have the lines
fingerPath?.move(to: currentTouchPoint)
arrayListFingerBezierPath.append((fingerPath!, strokeColor))
setNeedsDisplay()
In my touchesMoved I have the lines
fingerPath?.addLine(to: currentTouchPoint)
arrayListFingerBezierPath.append((fingerPath!, strokeColor))
setNeedsDisplay()
In touchesEnded I have the lines
fingerPath?.addLine(to: currentTouchPoint)
arrayListFingerBezierPath.append((fingerPath!, strokeColor))
setNeedsDisplay()
I am adding the fingerPath to an array so I can undo and redo paths.
My Undo function works like this
func undo()
{
if(!arrayListFingerBezierPath.isEmpty)
{
arrayListFingerPathUndoStore.append(arrayListFingerBezierPath.removeLast())
setNeedsDisplay()
}
}
Is it possible to add new UIBezierPath object to an array each time the user touches and drags on the screen in such a way that the click of an undo button will remove the whole of the last stroke the user made?
I can post all the class code if you need it all.

Obtain Bezier Path of CALayer

CALayer objects have a property accessibilityPath which as stated is supposedly
Returns the path of the element in screen coordinates.
Of course as expected this does not return the path of the layer.
Is there a way to access the physical path of a given CALayer that has already been created? For instance, how would you grab the path of a UIButton's layer property once the button has been initialized?
EDIT
For reference, I am trying to detect whether a rotated button contains a point. The reason for the difficulty here is due to the fact that the buttons are drawn in a curved view...
My initial approach was to create bezier slices then pass them as a property to the button to check if the path contains the point. For whatever reason, there seems to be an ugly offset from the path and the button.
They are both added to the same view and use the same coordinates / values to determine their frame, but the registered path seems to be offset to the left from the actual drawn shape from the path. Below is an image of the shapes I have drawn. The green outline is where the path is drawn (and displayed....) where the red is approximately the area which registers as inside the path...
I'm having a hard time understanding how the registered area is different.
If anyone has any ideas on why this offset would be occurring would be most appreciated.
UPDATE
Here is a snippet of me adding the shapes. self in this case is simply a UIView added to a controller. it's frame is the full size of the controller which is `{0, height_of_device - controllerHeight, width_of_device, controllerHeight}
UIBezierPath *slicePath = UIBezierPath.new;
[slicePath moveToPoint:self.archedCenterRef];
[slicePath addArcWithCenter:self.archedCenterRef radius:outerShapeDiameter/2 startAngle:shapeStartAngle endAngle:shapeEndAngle clockwise:clockwise];
[slicePath addArcWithCenter:self.archedCenterRef radius:(outerShapeDiameter/2 - self.rowHeight) startAngle:shapeEndAngle endAngle:shapeStartAngle clockwise:!clockwise];
[slicePath closePath];
CAShapeLayer *sliceShape = CAShapeLayer.new;
sliceShape.path = slicePath.CGPath;
sliceShape.fillColor = [UIColor colorWithWhite:0 alpha:.4].CGColor;
[self.layer addSublayer:sliceShape];
...
...
button.hitTestPath = slicePath;
In a separate method in my button subclass to detect if it contains the point or not: (self here is the button of course)
...
if ([self.hitTestPath containsPoint:touchPosition]) {
if (key.alpha > 0 && !key.isHidden) return YES;
else return NO;
}
else return NO;
You completely missunderstood the property, this is for assistive technology, from the docs:
Excerpt:
"The default value of this property is nil. If no path is set, the accessibility frame rectangle is used to highlight the element.
When you specify a value for this property, the assistive technology uses the path object you specify (in addition to the accessibility frame, and not in place of it) to highlight the element."
You can only get the path from a CAShapeLayer, alls other CALayers don't need to be drawn with a path at all.
Update to your update:
I think the offset is due to a missing
UIView convert(_ point: CGPoint, to view: UIView?)
The point needs to be converted to the buttons coordinate systems.

Why does SKShapeNode tear?

As a user traces along a line, a SKShapeNode path is built from line segments (which come from underlying 'tiles'). The actual path is built by locking in lines the user has previously dragged over, and finding the closest point on the closest line to the latest touch point, then building a path like so:
var activatedPointPath: UIBezierPath {
let activatedPointPath = UIBezierPath()
// self.state.activatedPoints contains the end point
// of each full line segment the user has traced through
if let firstActivatedPoint = self.state.activatedPoints.first {
activatedPointPath.move(to: firstActivatedPoint)
for activatedPoint in self.state.activatedPoints {
activatedPointPath.addLine(to: activatedPoint)
}
// self.state.endOfLine contains the last point the user
// has touched, which may or may not be at the end of a line
// segment
if let lastPoint = self.state.endOfLine {
activatedPointPath.addLine(to: lastPoint)
}
}
return activatedPointPath
}
this is then assigned to the SKShapeNode as:
self.lineNode.path = self.activatedPointPath.cgPath
however when the user is tracing the line, previous segments flicker with triangles missing while being drawn to the screen, like so:
The images above are from a simple 3x3 tile grid allowing the user to trace a simple straight line:
Here is a more complex example, where the line starts on the bottom left and ends at the top right:
What could be causing these artifacts?
edit while I have found a solution to this issue, I would still welcome any explanation for why the original code didn't work and the new code does.
My fix for this issue was to change the activatedPointPath calculated variable to build the line out of segments combined together, rather than drawing the line in one pass.
The new code looked like this:
var previousPoint = firstActivatedPoint
for activatedPoint in self.state.activatedPoints {
let newSegment = UIBezierPath()
newSegment.move(to: previousPoint)
newSegment.addLine(to: activatedPoint)
activatedPointPath.append(newSegment)
previousPoint = activatedPoint
}
if let lastPoint = self.state.endOfLine {
let newSegment = UIBezierPath()
newSegment.move(to: previousPoint)
newSegment.addLine(to: lastPoint)
activatedPointPath.append(newSegment)
} else {
print("No last point")
}
which now produces smooth lines.

Create clickable body diagram with Swift (iOS)

I'm trying to recreate something for a iOS app (Swift) which I already made in HTML5 (using map area-coords).
I want to show a human body diagram that interacts with user clicks/touches. The body exists of let's say 20 different parts, and the user can select one or more body-parts. For every body-part there is a selected-state image that should appear when a part is selected. Clicking on a selected part will deselect it. After selecting one or more parts, the user can continue and will get some information about these parts in a next viewcontroller. I am attaching a simplified image to explain my goal.
Can somebody explain what the best way is to achieve this goal? Is there a comparable technique that can be used in Swift to create such an interactive image?
Thanks!
There is no built-in mechanism for detecting taps in irregular shapes. The standard UIView tap detection uses frame rectangles. (Likewise with CALayers, as suggested by sketchyTech in his comment above.)
I would suggest drawing your body regions as bezier paths. You could create a custom subclass of UIView (BodyView) that would manage an array of BodyRegion objects each of which include a bezier path.
The UIBezierPath class includes a method containsPoint that lets you tell if a point is inside the path. Your BodyView could us that to decide which path object was tapped.
Bezier paths would handle both drawing your body regions and figuring out which part was tapped.
A very simple way to achieve this, would be to place invisible buttons over the areas, then hook every one up to an IBAction and put anything you want to happen inside.
#IBAction func shinTapped(sender: UIButton) {
tappedCounter = 0
if tappedCounter == 0 {
// set property to keep track
shinSelected = true
// TODO: set button image to selected state
// increment counter
tappedCounter++
}else{
// set property to keep track
shinSelected = false
// TODO: set button image to nil
// reset counter
tappedCounter = 0
}
This might get a little tricky to layout, so that every button sits in the right spot, but if you work with size classes it is totally doable.
I am sure there is a more elegant way to do it, but this is one way.
Fixed!
First I added sublayers to the UIImageView like this
var path = UIBezierPath()
path.moveToPoint(CGPointMake(20, 30))
path.addLineToPoint(CGPointMake(40, 30))
// add as many coordinates you need...
path.closePath()
var layer = CAShapeLayer()
layer.path = path.CGPath
layer.fillColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0.5).CGColor
layer.hidden = true
bodyImage.layer.addSublayer(layer)
Than I overrided the touchesbegan function in order to show and hide when the shapes are tapped.
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first! as? UITouch {
// get tapped position
let position = touch.locationInView(self.bodyImage)
// loop all sublayers
for layer in self.bodyImage.layer.sublayers! as! [CAShapeLayer] {
// check if tapped position is inside shape-path
if CGPathContainsPoint(layer.path, nil, position, false) {
if (layer.hidden) {
layer.hidden = false
}
else {
layer.hidden = true
}
}
}
}
}

Resources