restrict UIPanGestureRecognizer in UIView Mask - ios

I am masking UIView(240 * 240) in tringular shape using UIBezierPath as follows:
path = [UIBezierPath new];
[path moveToPoint:(CGPoint){0, 240}];
[path addLineToPoint:(CGPoint){120,0}];
[path addLineToPoint:(CGPoint){240,240}];
[path addLineToPoint:(CGPoint){0,240}];
[path closePath];
CAShapeLayer *mask = [CAShapeLayer new];
mask.frame = self.viewShape.bounds;
mask.path = path.CGPath;
self.viewShape.layer.mask = mask;
In above Image triangular area is mask. Now I have image of "Coca-Cola" which is to be moved only in triangular mask. So, for that I have apply UIPanGestureRecognizer to UIIMageView and restrict its frame in following way.
- (void)handlePanGesture:(UIPanGestureRecognizer *)gestureRecognizer
{
CGPoint touchLocation = [gestureRecognizer locationInView:self.viewShape];
CGRect boundsRect;
BOOL isInside = [path containsPoint:CGPointMake(self.innerView.center.x, self.innerView.center.y)];
NSLog(#"value:%d",isInside);
if (isInside) {
NSLog(#"inside");
self.innerView.center = touchLocation;
}else{
NSLog(#"outside");
}
}
My above if condition executes successfully but when control goes into else condition I am not able to drag back my ImageView inside mask frame.
So, My question is when else block(outside) called I should be able to drag imageView again inside the Mask's frame.
How can I achieve this?

saving reference to last center of imageview is a way to achieve this.
in your customView;
CGPoint lastValidCenter; //initially it is the center of imageview;
and in the code
NSLog(#"value:%d",isInside);
if (isInside) {
NSLog(#"inside");
self.innerView.center = touchLocation;
lastValidCenter = self.innerView.center;
}else{
NSLog(#"outside");
self.innerView.center = lastValidCenter;
}

There is a problem in your calculation. What you need to do is to check the top-left and top-right corner of the UIImageView for contains point with UIBezirePath instance.
It is so because when the image will move both of these corners are the ones which will try to go out first. So just put a check and you'll get your desired output.
BOOL isInside = [path containsPoint:CGPointMake(CGRectGetMinX(self.innerView.bounds), CGRectGetMinY(self.innerView.bounds))];
isInside = isInside || [path containsPoint:CGPointMake(CGRectGetMaxX(self.innerView.bounds), CGRectGetMinY(self.innerView.bounds))];
if (isInside) {
//Move your UIImageView
}
else {
//Don't move
}

I modified some portion of Meth's code. The code is as follows:
self.innerView.center = touchLocation;
BOOL isInside = [path containsPoint:CGPointMake(self.innerView.center.x, self.innerView.center.y)];
if (isInside) {
NSLog(#"inside");
self.innerView.center = touchLocation;
lastValidCenter = self.innerView.center;
}else{
NSLog(#"outside");
self.innerView.center = lastValidCenter;
}

Related

Drawing / Editing a line in iOS

I am trying to draw and edit multiple lines on iOS. Each line has a UIView at each end acting as handles so once the line is drawn a user can drag each end and the line will redraw.
Im currently using a UIBezierPath to draw a CAShapelayer on the view. The issue I have is working the best way to then draw another one and which ever line is tapped on the user can edit this one.
Does anyone have any ideas about the best way for this? Is CAShapelayer the best option?
Video Link that might show better what I'm trying to achieve.
https://www.dropbox.com/s/8rpt2azrs3uk6vr/Line%20Example.mov?dl=0
An example of the code I have done so far is below:
//Drawing a Line
Here I create a path from two touch points and the draw a CAShapeLayer on the view. I also create a custom object 'Line' to store the path and shapelayer.
-(void)DrawLineFrom:(CGPoint)pointA to:(CGPoint)pointB
{
NSLog(#"Drawing line X:%f Y:%f - X:%f Y:%f", pointA.x, pointA.y, pointB.x, pointB.y);
UIBezierPath* path = [[UIBezierPath alloc]init];
[path moveToPoint:pointA];
[path addLineToPoint:pointB];
[path addLineToPoint:CGPointMake(pointB.x, pointB.y+2)];
[path addLineToPoint:CGPointMake(pointA.x, pointA.y+2)];
[path addLineToPoint:pointA];
[path closePath];
currentLine.bPath = path;
if (!shapeLayer)
{
shapeLayer = [LineLayer layer];
[shapeLayer setFrame:self.view.frame];
shapeLayer.path = path.CGPath;
shapeLayer.strokeColor = [UIColor redColor].CGColor; //etc...
shapeLayer.lineWidth = 2.0; //etc...
shapeLayer.parent = currentLine;
currentLine.shapeLayer = shapeLayer;
[self.view.layer addSublayer:shapeLayer];
}
else
{
shapeLayer.path = path.CGPath;
}
[self ExitDrawMode];
}
//Creating a Handle
This creates the two views at either end and adds the Long Press Gesture.
-(HandleView *)MakeLineHandleForPoint:(int)point atLocation:(CGPoint)loc
{
HandleView *pointView = [[HandleView alloc]initWithFrame:CGRectMake(loc.x-10, loc.y-10, 20, 20)];
pointView.layer.cornerRadius = 10;
pointView.backgroundColor = [UIColor redColor];
UILongPressGestureRecognizer *LP = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleLp:)];
[pointView addGestureRecognizer:LP];
LP.delegate = self;
LP.minimumPressDuration = 0.0;
pointView.userInteractionEnabled = YES;
pointView.tag = point;
pointView.lineParent = currentLine;
return pointView;
}
Finally the gesture is handled here. This works fine however will always move the last line drawn. Even if I have selected the first one.
-(void)handleLp:(UILongPressGestureRecognizer *)sender
{
CGPoint loc = [sender locationInView:self.view];
[sender view].center = loc;
HandleView *handleView = [sender view];
if ([sender view].tag == 0) {
currentLine.pointA = loc;
[self DrawLineFrom:loc to:currentLine.pointB];
}
if ([sender view].tag == 1) {
currentLine.pointB = loc;
[self DrawLineFrom:currentLine.pointA to:loc];
}
}
Any Help much appreciated. Thanks in advance.

iOS Clear UIView and redraw drawRect:

I want my custom view to clear everything (subviews, sublayers, drawings, etc.) in it and call drawRect: again when pressing a button, so it will be like getting a clean slate and painting on it again.
Right now, I've tried using setNeedsDisplay and it just draws over the whole thing without cleaning the previous work.
A snippet of code in my drawRect:
if (_displayYAxisAverageLine) {
// Check if y axis data are numbers
for (id yValue in yAxisValues) {
if (![yValue isKindOfClass:[NSNumber class]]) {
return;
}
}
NSNumber *average = [yAxisValues valueForKeyPath:#"#avg.self"];
CGFloat scale = [self scaleForYValue:average];
CGFloat avgLineY = roundf(graphHeight * scale)+_graphInset;
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, avgLineY)];
[path addLineToPoint:CGPointMake(contentWidth, avgLineY)];
CAShapeLayer *avgLine = [CAShapeLayer layer];
avgLine.path = path.CGPath;
avgLine.fillColor = [UIColor clearColor].CGColor;
avgLine.strokeColor = _borderColor.CGColor;
avgLine.lineWidth = 1.f;
avgLine.lineDashPattern = #[#2, #2];
[self.contentView.layer addSublayer:avgLine];
}

Restrict movement of shape to dimensions of UIImageView

Using some code I came across that draws a circle shape over an image. Pan and pinch gestures are used on the circle shape being drawn. The movement of the shape crosses over the dimensions of the UIImageView, onto the rest of the UI that is not displaying the image. I've tried to incorporate methods used in other examples where pan and pinch restrictions were needed in order to restrict the circle shape's movement to the bounds of the UIImageView, but could not get any of them to work. I understand that I would need to control the center and radius values of the pan and pinch so that they don't cross outside the UIImageView's border, but haven't the slightest clue as to how to execute this. Attached is the code I'm using. Thanks for your help!
-(void)setUpCropTool:(id)sender{
// create layer mask for the image
CAShapeLayer *maskLayer = [CAShapeLayer layer];
self.imageToBeCropped.layer.mask = maskLayer;
self.maskLayer = maskLayer;
// create shape layer for circle we'll draw on top of image (the boundary of the circle)
CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.lineWidth = 50;
circleLayer.fillColor = [[UIColor clearColor] CGColor];
circleLayer.strokeColor = [[UIColor redColor] CGColor];
[self.imageToBeCropped.layer addSublayer:circleLayer];
self.circleLayer = circleLayer;
// create circle path
[self updateCirclePathAtLocation:CGPointMake(self.imageToBeCropped.bounds.size.width / 2.0, self.imageToBeCropped.bounds.size.height / 2.0) radius:self.imageToBeCropped.bounds.size.width/2.5];
// create pan gesture
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
pan.delegate = self;
[self.imageToBeCropped addGestureRecognizer:pan];
self.imageToBeCropped.userInteractionEnabled = YES;
self.pan = pan;
// create pan gesture
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handlePinch:)];
pinch.delegate = self;
[self.imageToBeCropped addGestureRecognizer:pinch];
self.pinch = pinch;
}
- (void)updateCirclePathAtLocation:(CGPoint)location radius:(CGFloat)radius
{
self.circleCenter = location;
self.circleRadius = radius;
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:self.circleCenter
radius:self.circleRadius
startAngle:0.0
endAngle:M_PI * 2.0
clockwise:YES];
self.maskLayer.path = [path CGPath];
self.circleLayer.path = [path CGPath];
}
#pragma mark - Gesture recognizers
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static CGPoint oldCenter;
CGPoint tranlation = [gesture translationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan)
{
oldCenter = self.circleCenter;
}
CGPoint newCenter = CGPointMake(oldCenter.x + tranlation.x, oldCenter.y + tranlation.y);
[self updateCirclePathAtLocation:newCenter radius:self.circleRadius];
}
- (void)handlePinch:(UIPinchGestureRecognizer *)gesture
{
static CGFloat oldRadius;
CGFloat scale = [gesture scale];
if (gesture.state == UIGestureRecognizerStateBegan)
{
oldRadius = self.circleRadius;
}
CGFloat newRadius = oldRadius * scale;
[self updateCirclePathAtLocation:self.circleCenter radius:newRadius];
}
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if ((gestureRecognizer == self.pan && otherGestureRecognizer == self.pinch) ||
(gestureRecognizer == self.pinch && otherGestureRecognizer == self.pan))
{
return YES;
}
return NO;
}
Try adding some additional constraints to the pan handling code. Maybe something like:
if (oldCenter.x + tranlation.x >= superView.frame.size.width - self.circleRadius
|| oldCenter.x + tranlation.x <= self.circleRadius)
{
newCenter.x = oldCenter.x;
}
else
{
newCenter.x = oldCenter.x + tranlation.x;
}
This is a pretty simple example, hope it helps.

iOS hitTest withEvent

I'm attempting to get the reference to a UImageView that is underneath a masked UIImageView using hitTest withEvent. Here is what I have that is not working:
UIView A that contains 3 UIImageViews as subviews: panelOne, panelTwo, and panelThree. panelThree is takes up the entire frame but is masked into a triangle, revealing parts of panels one and two. So I need to detect when a user taps outside of that rectangle and send the touch to the appropriate UIImageView.
Code: (CollagePanel is a subclass of UIImageView)
-(void)triangleInASquare
{
CGSize size = self.frame.size;
CollagePanel *panelOne = [[CollagePanel alloc] initWithFrame:CGRectMake(0,0, size.width/2, size.height)];
panelOne.panelScale = panelOne.frame.size.width/self.frame.size.width;
panelOne.backgroundColor = [UIColor greenColor];
CollagePanel *panelTwo = [[CollagePanel alloc] initWithFrame:CGRectMake(size.width/2,0, size.width/2, size.height)];
panelTwo.panelScale = panelOne.frame.size.width/self.frame.size.width;
panelTwo.backgroundColor = [UIColor purpleColor];
CollagePanel *panelThree = [[CollagePanel alloc] initWithFrame:CGRectMake(0,0, size.width, size.height)];
panelThree.backgroundColor = [UIColor orangeColor];
UIBezierPath* trianglePath = [UIBezierPath bezierPath];
[trianglePath moveToPoint:CGPointMake(0, panelThree.frame.size.height)];
[trianglePath addLineToPoint:CGPointMake(panelThree.frame.size.width/2,0)];
[trianglePath addLineToPoint:CGPointMake(panelThree.frame.size.width, panelTwo.frame.size.height)];
[trianglePath closePath];
// Mask the panels's layer to triangle.
CAShapeLayer *triangleMaskLayer = [CAShapeLayer layer];
[triangleMaskLayer setPath:trianglePath.CGPath];
triangleMaskLayer.strokeColor = [[UIColor whiteColor] CGColor];
panelThree.layer.mask = triangleMaskLayer;
//Add border
CAShapeLayer *borderLayer = [CAShapeLayer layer];
borderLayer.strokeColor = [[UIColor whiteColor] CGColor];
borderLayer.fillColor = [[UIColor clearColor] CGColor];
borderLayer.lineWidth = 6;
[borderLayer setPath:trianglePath.CGPath];
[panelThree.layer addSublayer:borderLayer];
NSMutableArray *tempArray = [[NSMutableArray alloc] init];
[tempArray addObject:panelOne];
[tempArray addObject:panelTwo];
[tempArray addObject:panelThree];
[self addGestureRecognizersToPanelsInArray:tempArray];
[self addPanelsFromArray:tempArray];
self.panelArray = tempArray;
}
-(void)handleTap: (UITapGestureRecognizer*) recognizer //coming from panel.imageView
{
CGPoint tapPoint = [recognizer locationInView:self];
NSLog(#"Location in self: %#", NSStringFromCGPoint(tapPoint));
NSLog(#"self.subviews: %#", self.subviews);
UIView *bottomView = [self hitTest:tapPoint withEvent:nil];
NSLog(#"Bottom View: %#", bottomView);
}
The NSLog of bottomView is always panelThree (the topmost panel). From what I understand the hit test should be returning the "bottom most" subview.
you understand wrong. it will return the view that recognizes itself as touched and is nearest to your finger, nearer to the top.
If a view shall not recognize itself as touch for a certain point, you need to overwrite
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
for that view.
I think Ole Begemann's Shapped Button is a great example how to do so.
In your project this method could determine, if a point lies within the paths: CGPathContainsPoint.
Your pointInside:withEvent: might look like this:
#import "CollagePanel.h"
#import <QuartzCore/QuartzCore.h>
#implementation CollagePanel
//
// ...
//
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint p = [self convertPoint:point toView:[self superview]];
if(self.layer.mask){
if (CGPathContainsPoint([(CAShapeLayer *)self.layer.mask path], NULL, p, YES) )
return YES;
}else {
if(CGRectContainsPoint(self.layer.frame, p))
return YES;
}
return NO;
}
#end

How to bring UIBezierPath to the back of a MKAnnotation object?

In my app, user draws a shape on map and using UIBeizerPath i am drawing that path. Then based on the coordinates of the path i am displaying the results which are only in that area. Everything works great except that now when Annotations drops on the Map view the pins looks like they are behind the path which means path looks in the front.
I am using this code to display the Annotation and path :
-(void)clearAnnotationAndPath:(id)sender {
[_mapView removeAnnotations:_mapView.annotations];
path = [UIBezierPath bezierPath];
[shapeLayer removeFromSuperlayer];
}
- (void)handleGesture:(UIPanGestureRecognizer *)gesture
{
CGPoint location = [gesture locationInView:_pathOverlay];
if (gesture.state == UIGestureRecognizerStateBegan)
{
shapeLayer = [[CAShapeLayer alloc] init];
shapeLayer.fillColor = [[UIColor clearColor] CGColor];
shapeLayer.strokeColor = [[UIColor greenColor] CGColor];
shapeLayer.lineWidth = 5.0;
//[_mapView.layer addSublayer:shapeLayer];
[pathOverlay.layer addSublayer:shapeLayer];
path = [UIBezierPath bezierPath];
[path moveToPoint:location];
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
[path addLineToPoint:location];
shapeLayer.path = [path CGPath];
}
else if (gesture.state == UIGestureRecognizerStateEnded)
{
// MKMapView *mapView = (MKMapView *)gesture.view;
[path addLineToPoint:location];
[path closePath];
allStations = [RoadmapData sharedInstance].data;
for (int i=0; i<[allStations count]; i++) {
NSDictionary * itemNo = [allStations objectAtIndex:i];
NSString * fullAddress = [NSString stringWithFormat:#"%#,%#,%#,%#",[itemNo objectForKey:#"address"],[itemNo objectForKey:#"city"],[itemNo objectForKey:#"state"],[itemNo objectForKey:#"zip"]];
CLGeocoder * geoCoder = [[CLGeocoder alloc]init];
[geoCoder geocodeAddressString:fullAddress completionHandler:^(NSArray *placemarks, NSError *error) {
if (error) {
NSLog(#"Geocode failed with error: %#", error);
return;
}
if(placemarks && placemarks.count > 0)
{
CLPlacemark *placemark = placemarks[0];
CLLocation *location = placemark.location;
CLLocationCoordinate2D coords = location.coordinate;
CGPoint loc = [_mapView convertCoordinate:coords toPointToView:_pathOverlay];
if ([path containsPoint:loc])
{
NSString * name = [itemNo objectForKey:#"name"];
stationAnn = [[LocationAnnotation alloc]initWithCoordinate:coords Title:name subTitle:#"Wells Fargo Offer" annIndex:i];
stationAnn.tag = i;
[_mapView addAnnotation:stationAnn];
}
else{
NSLog(#"Out of boundary");
}
}
}];
[self turnOffGesture:gesture];
}
}
}
- (void)mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views{
if (views.count > 0) {
UIView* firstAnnotation = [views objectAtIndex:0];
UIView* parentView = [firstAnnotation superview];
if (_pathOverlay == nil){
// create a transparent view to add bezier paths to
pathOverlay = [[UIView alloc] initWithFrame: parentView.frame];
pathOverlay.opaque = NO;
pathOverlay.backgroundColor = [UIColor clearColor];
[parentView addSubview:pathOverlay];
}
// make sure annotations stay above pathOverlay
for (UIView* view in views) {
[parentView bringSubviewToFront:view];
}
}
}
Also once i go back from this and view and come again its not even drawing the Path.
Please help.
Thanks,
Apparently, when you add your bezier path to the map via:
[_mapView.layer addSublayer:shapeLayer];
it is getting added above some internal layer that MKMapView uses to draw the annotations. If you take a look at this somewhat related question, you'll see that you can implement the MKMapViewDelegate protocol, and get callbacks when new station annotations are added. When this happens, you basically inspect the view heirarchy of the newly added annotations, and insert a new, transparent UIView layer underneath them. You take care to bring all the annotations in front of this transparent UIView.
// always remember to assign the delegate to get callbacks!
_mapView.delegate = self;
...
#pragma mark - MKMapViewDelegate
- (void)mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views{
if (views.count > 0) {
UIView* firstAnnotation = [views objectAtIndex:0];
UIView* parentView = [firstAnnotation superview];
// NOTE: could perform this initialization in viewDidLoad, too
if (self.pathOverlay == nil){
// create a transparent view to add bezier paths to
pathOverlay = [[UIView alloc] initWithFrame: parentView.frame];
pathOverlay.opaque = NO;
pathOverlay.backgroundColor = [UIColor clearColor];
[parentView addSubview:pathOverlay];
}
// make sure annotations stay above pathOverlay
for (UIView* view in views) {
[parentView bringSubviewToFront:view];
}
}
}
Then, instead of adding your shape layer to _mapView.layer, you add it to your transparent view layer, also using this new layer in the coordinate conversion:
- (void)handleGesture:(UIPanGestureRecognizer*)gesture
{
CGPoint location = [gesture locationInView: self.pathOverlay];
if (gesture.state == UIGestureRecognizerStateBegan)
{
if (!shapeLayer)
{
shapeLayer = [[CAShapeLayer alloc] init];
shapeLayer.fillColor = [[UIColor clearColor] CGColor];
shapeLayer.strokeColor = [[UIColor greenColor] CGColor];
shapeLayer.lineWidth = 5.0;
[pathOverlay.layer addSublayer:shapeLayer]; // <- change here !!!
}
self.path = [[UIBezierPath alloc] init];
[path moveToPoint:location];
}
else if (gesture.state == UIGestureRecognizerStateChanged)
{
[path addLineToPoint:location];
shapeLayer.path = [path CGPath];
}
else if (gesture.state == UIGestureRecognizerStateEnded)
{
/*
* This code is the same as what you already have ...
*/
// But replace this next line with the following line ...
//CGPoint loc = [_mapView convertCoordinate:coords toPointToView:self];
CGPoint loc = [_mapView convertCoordinate:coords toPointToView: self.pathOverlay];
/*
* And again use the rest of your original code
*/
}
}
where I also added an ivar (and property) for the new transparent layer:
UIView* pathOverlay;
I tested this with a bogus grid of stations and got the following results:
P.S. I'd also recommend getting rid of your static variables. Just make them ivars/properties of your class.

Resources