How to create several UIButtons along a path/BezierCurve? - ios

How can I create objects along a path/BezierCurve? In other words, how can I create several UIButtons along a given path, with a given interval along that same path?
I have seen dozens of questions about moving objects. But I need a solution to actually create them.
I would like to go along the path and create an object for every X points/distance. Like this:
....#....#....#....#....
In this case, for every 4 points, get the position, and create a UIButton there.

iOS doesn't have a public API that directly gives you points spaced along a path. But there is a roundabout way to do it. Suppose you want points along the path spaced a distance of X apart.
First, create a CGPathRef containing your path. (You can construct a UIBezierPath if you prefer and then gets its CGPath property.)
Then, call CGPathCreateCopyByDashingPath, using a dash pattern of { X, X }. For example:
static CGFloat const kSpace = 10;
CGPathRef dashedPath = CGPathCreateCopyByDashingPath(path, NULL, 0,
(CGFloat const []){ kSpace, kSpace }, 2);
This returns a new path containing multiple subpaths. Each subpath is a length X segment of the original path, and is separated from its neighboring subpaths by a distance of X along the original path. Thus the endpoints of the subpaths are spaced along the original path at an interval of length X.
So, finally, enumerate the dashed path using CGPathApply, picking the endpoints and creating buttons there. First, you'll want to wrap it in a function that takes a block:
static void applyBlockToPathElement(void *info, const CGPathElement *element) {
void (^block)(const CGPathElement *) = (__bridge void (^)(const CGPathElement *))(info);
block(element);
}
void MyCGPathApplyBlock(CGPathRef path, void (^block)(const CGPathElement *element)) {
CGPathApply(path, (__bridge void *)(block), applyBlockToPathElement);
}
Then you can apply a block that finds the each subpath endpoint and creates a button there. Assuming you have a method named createButtonAtPoint:, something like this should work:
__block BOOL isInSubpath = NO;
__block CGPoint subpathStart = CGPointZero;
__block CGPoint currentPoint = CGPointZero;
MyCGPathApplyBlock(dashedPath, ^(const CGPathElement *element) {
switch (element->type) {
case kCGPathElementMoveToPoint:
if (isInSubpath) {
[self createButtonAtPoint:currentPoint];
isInSubpath = NO;
}
currentPoint = element->points[0];
break;
case kCGPathElementCloseSubpath:
// This should not appear in a dashed path.
break;
case kCGPathElementAddLineToPoint:
case kCGPathElementAddQuadCurveToPoint:
case kCGPathElementAddCurveToPoint:
if (!isInSubpath) {
[self createButtonAtPoint:currentPoint];
isInSubpath = YES;
}
int pointIndex =
element->type == kCGPathElementAddLineToPoint ? 0
: element->type == kCGPathElementAddQuadCurveToPoint ? 1
: /* element->type == kCGPathElementAddCurveToPoint ? */ 2;
currentPoint = element->points[pointIndex];
break;
}
});

Have you solved your problem yet? if no, see this if it could help
//if the interval is kown as float, suggesting it named padding
//then you can
for(i=0;i<numOfPaddings;i++){
//create a button
UIButton *aButton = [UIButton buttonWithType:UIButtonRoundRect/*I forgot how to spell,but it does not metter*/];
//Set your button's position base on padding
[aButton setFrame:CGRectMake(padding+padding*i,20,50,20)];
}

Related

How to know when all physics bodies have stopped moving in Cocos2d V3.0 with Chipmunk

The only way I can think to do it is to check velocities for all physics bodies during every collisions.
- (BOOL)ccPhysicsCollisionBegin:(CCPhysicsCollisionPair *)pair piece:(CCNode *)pieceA piece:(CCNode *)pieceB{
float x = 0;
float y = 0;
for (int i = 0; i < [[_physicsWorld children] count]; i++) {
x = x + [[[_physicsWorld children][i] physicsBody] velocity].x;
y = y + [[[_physicsWorld children][i] physicsBody] velocity].y;
}
if ( x == 0 && y == 0 ) {
NSLog(#"stopped");
}
return YES;
}
This logs “stopped” multiple times when the scene first loads, then doesn’t log “stopped” again, even after physics bodies have clearly started moving and colliding and then come to a stop.
Ideally I'd like a delegate method that would notify me when all physics bodies have stopped moving, but I can't seem to find one.
FYI: I'm using the standard Chipmunk physics engine that's baked into Cocos2d V3.0
Chipmunk has a internal mechanism, which can, if activated, automatically deactivate physics bodies. My approach (I am using cocos2dx 3.11.1 and not -obj version with chipmunk 7.0.1) is:
activate the chipmunk idle mechanism (0.5 second - meaning, if an object is not moving for longer than 0.5 second it will be deactivated):
cpSpaceSetSleepTimeThreshold(space, 0.5f);
You do not need to use
cpSpaceSetIdleSpeedThreshold(space, <speed>);
because chipmunk calculates the threshold speed for you (according the gravitation used).
use this code for determination if all objects are not moving (static and kinetic bodies never sleep):
bool isAnyPhysicsBodyMoving(){
int i = 0; bool isMoving = false;
const Vector<PhysicsBody*>& bodies = getPhysicsWorld()->getAllBodies();
while( i < bodies.size() && !isMoving){
PhysicsBody *body = bodies.at(i);
isMoving = cpBodyGetType(body->getCPBody()) == CP_BODY_TYPE_DYNAMIC
&& !body->isResting();
i++;
}
return isMoving;
}
use static (and not kinetic) body for walls, in order to let objects sleep:
// wall
Size visibleSize = Director::getInstance()->getWinSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
float border = 10.0f;
Size wallBodySize = Size(visibleSize.width+2*border, visibleSize.height+2*border);
PhysicsBody *wallBody = PhysicsBody::createEdgeBox(wallBodySize, PhysicsMaterial(1.0f, 1.0f, 0.5f), border);
Node* wall = Node::create();
wall->addComponent(wallBody);
wall->setAnchorPoint(Vec2(0.5f, 0.5f));
wall->setPosition(Point(visibleSize.width/2+origin.x, visibleSize.height/2+origin.y));
cpVect tt;
tt.x = wall->getPosition().x; tt.y = wall->getPosition().y;
//set position manually and BEFORE adding the object into the space
cpBodySetPosition(wallBody->getCPBody(), tt);
cpBodySetType(wallBody->getCPBody(), CP_BODY_TYPE_STATIC);
addChild(wall);
Any dynamic body connected to a kinetic body (for example laying on) will never sleep.
test it with DEBUG activated
getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
the boxes (their content) must become grey (=sleeping) and not red (=active):
In order to let it work, I have:
added an access method (to get cpSpace) in CCPhysicsWorld.h:
inline cpSpace* getSpace() const { return _cpSpace; }
Fix call of
cpBodySetTorque(body, 0.0f);`
in CCPhysicsBody.cpp to
if (body->t != 0.0f){
cpBodySetTorque(body, 0.0f);
}
Fix call of
cpBodySetPosition(_cpBody, tt);`
in CCPhysicsBody.cpp to
if (!cpveql(tt, cpBodyGetPosition(_cpBody))){
cpBodySetPosition(_cpBody, tt);
}
Steps 2. and 3. are necessary to avoid setting of the same physics body properties, which wake up a sleeping body.
The advantage of this approach is, that the chipmunk does not make any calculations for such physical bodies - saving CPU and battery.
I found something that works.
tl;dr
The basic idea is to keep track of the positions of the sprites myself, and then periodically check them to see if any of them have moved since they were last checked.
Longer version
I created a subclass of CCNode with the class name Piece.
These are my objects that are added to the physics world.
#implementation Piece {
float _previousX;
float _previousY;
}
-(void)updatePreviousScreenXandY{
_previousX = self.position.x;
_previousY = self.position.y;
}
-(BOOL)hasntMoved{
float currentX = self.position.x;
float currentY = self.position.y;
if ( currentX == _previousX && currentY == _previousY ) {
return TRUE;
}else{
return FALSE;
}
}
This is in my CCNode that acts as the game scene
-(void)doStuffAfterPiecesStopMoving:(NSTimer*)timer{
BOOL noPiecesHaveMoved = TRUE;
for (int i = 0; i < [[_physicsWorld children] count]; i++) {
if ( [[_physicsWorld children][i] hasntMoved] == FALSE ) {
noPiecesHaveMoved = FALSE;
break;
}
}
if ( noPiecesHaveMoved ) {
[timer invalidate];
NSLog(“Pieces have stopped moving”);
}else{
NSLog(“Pieces are still moving”);
[self updateAllPreviousPiecePositions];
}
}
-(void)updateAllPreviousPiecePositions{
for (int i=0; i < [[_physicsWorld children] count]; i++) {
Piece *piece = (Piece*)[_physicsWorld children][i];
[piece updatePreviousScreenXandY];
}
}
All I have to do is
[NSTimer scheduledTimerWithTimeInterval:TIME_BETWEEN_CHECKS
target:_gamePlay
selector:#selector(doStuffAfterPiecesStopMoving:)
userInfo:nil
repeats:YES];
and it’ll run whatever code I want after all Piece nodes have stopped moving.
The key to getting it to work well is to get the values for the Chipmunk space’s sleepTimeThreshold and the timer above’s time as low as possible.
My experimenting suggests the following settings work okay, but anything lower will cause problems (i.e. collisions not taking place properly):
sleepTimeThreshold = 0.15
my timer = 0.05
If anyone has a different/better solution or improvements to the above code, please post.

Rectangle and Circle collision detection

I am trying to do collision detection between a rectangle and a circle. I came up with this method:
-(BOOL) isCollidingRect:(CCSprite *) spriteOne WithSphere:(CCSprite *) spriteTwo {
float diff = ccpDistance(spriteOne.position, spriteTwo.position);
float obj1Radii = [spriteOne boundingBox].size.width/2;
float obj2Radii = [spriteTwo boundingBox].size.width/2;
if (diff < obj1Radii + obj2Radii) {
return YES;
} else {
return NO;
}
}
and this is how I check it:
if ([self isCollidingRect:player WithSphere:blocker] == true) {
[self playerdeathstart];
}
This seems to work properly on the side of the rectangle but it doesn't above or below it. On the top and bottom, the collision occurs too early.
Is there a way I can get this collision to detected properly? Thank you for your help.
You can use CGRectIntersectsRect to achieve this.
-(BOOL) isCollidingRect:(CCSprite *) spriteOne WithSphere:(CCSprite *) spriteTwo {
return CGRectIntersectsRect([spriteOne boundingBox],[spriteTwo boundingBox]);
}
It is not pixel perfect but as i understand that is not necessary in this case.
This is not a solution for those who use Cocos2d-ObjC, but will help for Cocos2d-x devs (for instance, personally I found this topic because was searching for the same for my c++ game).
Cocos2d-x has method "intersectsCircle" for Rect class.
Here is how I solved in my c++ project almost the same problem as one described by you:
bool ObstacleEntity::hasCollisionAgainst(cocos2d::Sprite *spr)
{
cocos2d::Rect rect = cocos2d::Rect( spr->getPositionX(), spr->getPositionY(), spr->getBoundingBox().size.width, spr->getBoundingBox().size.height);
float rw = this->getBoundingBox().size.width / 2;
float rh = this->getBoundingBox().size.height / 2;
float radius = ( rw > rh ) ? rw : rh;
cocos2d::Vec2 center( this->getPositionX() + rw, this->getPositionY() + rh );
return rect.intersectsCircle( center, radius );
}
Passed Sprite here is rectangle, while ObstacleEntity always is almost ideally round.
Note that anchor points for all entities are set to lower left corner in my case.

Moving Platforms? Cocos2d and Tiled

I'm trying to make some moving tiles from a Tiled map editor tmx file.
I have the moving tiles in their own layer, and I just want to simply have them move up, and then when they reach a certain y, move back down, and etc.
I have been looking around for a bit on a clear way of accomplishing this, but my efforts have been unsuccessful.
I tried using some of the methods here.
I'm still really new to cocos2d development in general, so I wold appreciate any insight on this. Thank you very much for your time. If you have any questions, please ask! :)
Also if it helps, the tiles I'm trying to move are in a big T shape.
FINAL UPDATE:
(Removed more irrelevant code so anyone in the future can easily find my solution (the full answer is below), you can find where I got my layer iterate method at the link above).
Okay, so I have finally got it working close to how I want.. I don't think this is exactly the most ideal way of doing it, but this is what I've got.
Note: In order for this to work for you, you have to run your app out of debug mode or it will lag/make the player fall through the ground (at least it did for me..).
I have an update function that calls certain functions every frame. (Checking collisions, moving platforms, etc).
That update function calls my move platforms function..
like this:
[self movePlatforms:0.1];
this is my movePlatforms function..
-(void)movePlatforms: (ccTime) dt{
if(goingDown){
moveCount++;
}else{
moveCount--;
}
CGSize s = [movingTiles layerSize];
for( int x=0; x<s.width;x++) {
for( int y=0; y< s.height; y++ ) {
CCSprite *tile = [movingTiles tileAt:ccp(x,y)];
if(goingDown){
CGPoint newPosition = ccp(tile.position.x, tile.position.y - 1);
tile.position = newPosition;
if(moveCount >= 100){
goingDown = false;
}
}else{
CGPoint newPosition = ccp(tile.position.x, tile.position.y + 1);
tile.position = newPosition;
if(moveCount <= 0){
goingDown = true;
}
}
}
}
}
So basically, I created a int moveCount and a BOOL goingDown to keep track of how many times my movePlatform function has been called. So after 100 calls, it switches direction.
(This works fine for me, you might need something else like a collision detecter if that is the case use this).
if (CGRectIntersectsRect([someSprite boundingBox], [someSprite boundingBox])) {
//Do something
}
Hopefully this works for someone in the future, I know this was quite the headache for me, and it probably isn't even done correctly or there is a much better way to do it, but if this helps you, that is awesome!
Creating and removing tiles will effect your performance.
Instead of it, try to move the tile changing their position:
CCSprite *tile = [movingTiles tileAt:ccp(92,platformY)];
[movingTiles removeTileAt:ccp(92,platformY)];
CGPoint newTilePosition = tile.position;
if (goingDown){
newTilePosition.y ++;
if(newTilePosition.y >= 20){
goingDown = false;
}
}else{
newTilePosition.y --;
if(newTilePosition.y <= 10){
goingDown = true;
}
}
tile.position = newTilePosition;
Here is the (kind of) step by step of how I got my moving tiles working, this is only related to the moving tiles, and nothing else.
Note: You will need to run this as a release (not debug) in order to get everything running smoothly, and not having your character fall through the ground.
In the interface I created these variables:
#interface HelloWorldLayer(){
CCTMXTiledMap *map;
BOOL goingDown;
int moveCount;
}
The CCTMXTiledMap is the instance of my map.
The BOOL and int are two variables I use to keep track of my moving tiles.
-(id) init {
if( (self=[super init]) ) {
// add our map
map = [[CCTMXTiledMap alloc] initWithTMXFile:#"level1-1.tmx"];
map.position = ccp(0,0);
[self addChild:map];
//add our moving platforms layer
movingTiles = [map layerNamed:#"moving_platforms"];
//set the variables I use to keep track of the moving platforms
goingDown = true;
moveCount = 0;
//schedule my update method
[self schedule:#selector(update:)];
}
return self;
}
After the init method, I then create my move platforms method:
-(void)movePlatforms: (ccTime) dt{
if(goingDown){
moveCount++;
}else{
moveCount--;
}
CGSize s = [movingTiles layerSize];
for( int x=0; x<s.width;x++) {
for( int y=0; y< s.height; y++ ) {
CCSprite *tile = [movingTiles tileAt:ccp(x,y)];
if(goingDown){
CGPoint newPosition = ccp(tile.position.x, tile.position.y - 1);
tile.position = newPosition;
if(moveCount >= 100){
goingDown = false;
}
}else{
CGPoint newPosition = ccp(tile.position.x, tile.position.y + 1);
tile.position = newPosition;
if(moveCount <= 0){
goingDown = true;
}
}
}
}
}
So this is where the magic happens, I use methods I got from here, and the gentleman Mauricio Tollin told me I could update a tile position rather than destroy and recreate them.
So I iterate through every tile in my moving platforms layer, and tell them to go down 1 every call, until moveCount >= 100, then it says goingDown is now false, and it switches its direction. From there it just goes back and forth, counting to 100, and then back down.
If you want it to move longer, just increase 100 to 200 or whatever you want. (Or you can use a check to detect collision, and when it collides with a specified sprite, you can have it change then. If that is more of what you want, use this).
if (CGRectIntersectsRect([someSprite boundingBox], [someSprite boundingBox])) {
//Do something
}
After all of that, I create my update method:
-(void)update:(ccTime)dt{
[self movePlatforms:0.1];
}
In the init method it schedules the update method to be called every frame, and then the update method will run the movePlatforms method (or any other function that needs to be checked frequently, such as hazard detection, etc).
You can also make the platforms move slower by changing the time passed into movePlatforms, or you can schedule a slower update interval in the init method.
I hope this helps someone out in the future, I just wanted to create this answer with a more in depth process of how I got this working, since my question post was really unorganized and heavily edited while I was learning.

GB2ShapeCache Shape Scale? (Physics Editor) [Box2D]

There is a class associated with the program Physics Editor called GB2ShapeCache that loads shapes that I make in the program. I noticed that it is not currently possible to change the scale of the shapes on the fly so I would like to be able to scale the fixtures for the shapes that I made in Physics Editor. Now the scale of my CCSprite in my app can be random so currently in the addShapesWithFile method, I do this for polygons:
vertices[vindex].x = (offset.x * sprite.scaleX) / ptmRatio_;
vertices[vindex].y = (offset.y * sprite.scaleY) / ptmRatio_;
and this for circles:
circleShape->m_radius = ([[circleData objectForKey:#"radius"] floatValue] / ptmRatio_) *sprite.scale;
I also changed the method so that I can pass in my sprite so I can get the scale to:
-(void) addShapesWithFile:(NSString*)plist forSprite:(CCSprite*)sprite
so that I can pass in my sprite so I can get the scale.
HOWEVER, I find this to be inefficient because I should not have to reload ALL my shapes in my plist since they are already added.
So is there any way to do what I am doing now but in the addFixturesToBody method? This way I do not re-create the already added plist shapes and I only scale the fixtures when it is ready to be added to my body.
If anyone needs to see more code or needs more info, feel free to ask. I know this issue must be simple!!!
Thanks!
I would recommend implementing it in the addFixturesToBody method.
(see https://github.com/AndreasLoew/GBox2D/blob/master/GBox2D/GB2ShapeCache.mm)
Try this method below, this should scale the shapes accordingly to the sprite's they are for. Just pass in your CCSprite and this method will handle the rest.
- (void)addFixturesToBody:(b2Body*)body forShapeName:(NSString*)shape forSprite:(CCSprite*)sprite {
BodyDef *so = [shapeObjects_ objectForKey:shape];
assert(so);
FixtureDef *fix = so->fixtures;
if ((sprite.scaleX == 1.0f) && (sprite.scaleY == 1.0f)) {
// simple case - so do not waste any energy on this
while(fix) {
body->CreateFixture(&fix->fixture);
fix = fix->next;
}
} else {
b2Vec2 vertices[b2_maxPolygonVertices];
while(fix) {
// make local copy of the fixture def
b2FixtureDef fix2 = fix->fixture;
// get the shape
const b2Shape *s = fix2.shape;
// clone & scale polygon
const b2PolygonShape *p = dynamic_cast<const b2PolygonShape*>(s);
if(p)
{
b2PolygonShape p2;
for(int i=0; i<p->m_vertexCount; i++)
{
vertices[i].x = p->m_vertices[i].x * sprite.scaleX;
vertices[i].y = p->m_vertices[i].y * sprite.scaleY;
}
p2.Set(vertices, p->m_vertexCount);
fix2.shape = &p2;
}
// clone & scale circle
const b2CircleShape *c = dynamic_cast<const b2CircleShape *>(s);
if(c) {
b2CircleShape c2;
c2.m_radius = c->m_radius * sprite.scale;
c2.m_p.x = c->m_p.x * sprite.scaleX;
c2.m_p.y = c->m_p.y * sprite.scaleY;
fix2.shape = &c2;
}
// add to body
body->CreateFixture(&fix2);
fix = fix->next;
}
}
}

Hit detection when drawing lines in iOS

I would like to allow the user to draw curves in such a way that no line can cross another line or even itself. Drawing the curves is no problem, and I even found that I can create a path that is closed and still pretty line-like by tracing the nodes of the line forwards and back and then closing the path.
Unfortunately, iOS only provides a test for whether a point is contained in a closed path (containsPoint: and CGPathContainsPoint). Unfortunately, a user can pretty easily move their finger fast enough that the touch points land on both sides of an existing path without actually being contained by that path, so testing the touch points is pretty pointless.
I can't find any "intersection" of paths method.
Any other thoughts on how to accomplish this task?
Well, I did come up with a way to do this. It is imperfect, but I thought others might want to see the technique since this question was upvoted a few times. The technique I used draws all the items to be tested against into a bitmap context and then draws the new segment of the progressing line into another bitmap context. The data in those contexts is compared using bitwise operators and if any overlap is found, a hit is declared.
The idea behind this technique is to test each segment of a newly drawn line against all the previously drawn lines and even against earlier pieces of the same line. In other words, this technique will detect when a line crosses another line and also when it crosses over itself.
A sample app demonstrating the technique is available: LineSample.zip.
The core of hit testing is done in my LineView object. Here are two key methods:
- (CGContextRef)newBitmapContext {
// creating b&w bitmaps to do hit testing
// based on: http://robnapier.net/blog/clipping-cgrect-cgpath-531
// see "Supported Pixel Formats" in Quartz 2D Programming Guide
CGContextRef bitmapContext =
CGBitmapContextCreate(NULL, // data automatically allocated
self.bounds.size.width,
self.bounds.size.height,
8,
self.bounds.size.width,
NULL,
kCGImageAlphaOnly);
CGContextSetShouldAntialias(bitmapContext, NO);
// use CGBitmapContextGetData to get at this data
return bitmapContext;
}
- (BOOL)line:(Line *)line canExtendToPoint:(CGPoint) newPoint {
// Lines are made up of segments that go from node to node. If we want to test for self-crossing, then we can't just test the whole in progress line against the completed line, we actually have to test each segment since one segment of the in progress line may cross another segment of the same line (think of a loop in the line). We also have to avoid checking the first point of the new segment against the last point of the previous segment (which is the same point). Luckily, a line cannot curve back on itself in just one segment (think about it, it takes at least two segments to reach yourself again). This means that we can both test progressive segments and avoid false hits by NOT drawing the last segment of the line into the test! So we will put everything up to the last segment into the hitProgressLayer, we will put the new segment into the segmentLayer, and then we will test for overlap among those two and the hitTestLayer. Any point that is in all three layers will indicate a hit, otherwise we are OK.
if (line.failed) {
// shortcut in case a failed line is retested
return NO;
}
BOOL ok = YES; // thinking positively
// set up a context to hold the new segment and stroke it in
CGContextRef segmentContext = [self newBitmapContext];
CGContextSetLineWidth(segmentContext, 2); // bit thicker to facilitate hits
CGPoint lastPoint = [[[line nodes] lastObject] point];
CGContextMoveToPoint(segmentContext, lastPoint.x, lastPoint.y);
CGContextAddLineToPoint(segmentContext, newPoint.x, newPoint.y);
CGContextStrokePath(segmentContext);
// now we actually test
// based on code from benzado: http://stackoverflow.com/questions/6515885/how-to-do-comparisons-of-bitmaps-in-ios/6515999#6515999
unsigned char *completedData = CGBitmapContextGetData(hitCompletedContext);
unsigned char *progressData = CGBitmapContextGetData(hitProgressContext);
unsigned char *segmentData = CGBitmapContextGetData(segmentContext);
size_t bytesPerRow = CGBitmapContextGetBytesPerRow(segmentContext);
size_t height = CGBitmapContextGetHeight(segmentContext);
size_t len = bytesPerRow * height;
for (int i = 0; i < len; i++) {
if ((completedData[i] | progressData[i]) & segmentData[i]) {
ok = NO;
break;
}
}
CGContextRelease(segmentContext);
if (ok) {
// now that we know we are good to go,
// we will add the last segment onto the hitProgressLayer
int numberOfSegments = [[line nodes] count] - 1;
if (numberOfSegments > 0) {
// but only if there is a segment there!
CGPoint secondToLastPoint = [[[line nodes] objectAtIndex:numberOfSegments-1] point];
CGContextSetLineWidth(hitProgressContext, 1); // but thinner
CGContextMoveToPoint(hitProgressContext, secondToLastPoint.x, secondToLastPoint.y);
CGContextAddLineToPoint(hitProgressContext, lastPoint.x, lastPoint.y);
CGContextStrokePath(hitProgressContext);
}
} else {
line.failed = YES;
[linesFailed addObject:line];
}
return ok;
}
I'd love to hear suggestions or see improvements. For one thing, it would be a lot faster to only check the bounding rect of the new segment instead of the whole view.
Swift 4, answer is based on CGPath Hit Testing - Ole Begemann (2012)
From Ole Begemann blog:
contains(point: CGPoint)
This function is helpful if you want to hit test on the entire region
the path covers. As such, contains(point: CGPoint) doesn’t work with
unclosed paths because those don’t have an interior that would be
filled.
copy(strokingWithWidth lineWidth: CGFloat, lineCap: CGLineCap, lineJoin: CGLineJoin, miterLimit: CGFloat, transform: CGAffineTransform = default) -> CGPath
This function creates a mirroring tap target object that only covers
the stroked area of the path. When the user taps on the screen, we
iterate over the tap targets rather than the actual shapes.
My solution in code
I use a UITapGestureRecognizer linked to the function tap():
var bezierPaths = [UIBezierPath]() // containing all lines already drawn
var tappedPaths = [CAShapeLayer]()
#IBAction func tap(_ sender: UITapGestureRecognizer) {
let point = sender.location(in: imageView)
for path in bezierPaths {
// create tapTarget for path
if let target = tapTarget(for: path) {
if target.contains(point) {
tappedPaths.append(layer)
}
}
}
}
fileprivate func tapTarget(for path: UIBezierPath) -> UIBezierPath {
let targetPath = path.copy(strokingWithWidth: path.lineWidth, lineCap: path..lineCapStyle, lineJoin: path..lineJoinStyle, miterLimit: path.miterLimit)
return UIBezierPath.init(cgPath: targetPath)
}

Resources