Add inverted circle overlay to map view - ios

(Using iOS 5 and Xcode 4.2.)
I've followed the instructions here: http://developer.apple.com/library/ios/#documentation/UserExperience/Conceptual/LocationAwarenessPG/AnnotatingMaps/AnnotatingMaps.html#//apple_ref/doc/uid/TP40009497-CH6-SW15 and used the MKCircle and MKCircleView classes to add a circle overlay on my MKMapView.
However what I actually want is an inverted circle overlay, like the left map in the sketch below (currently I have a circle overlay like the one on the right):
For the inverted circle, the overlay should cover the entire map - apart from the visible circle.
Is there an easy way to accomplish this using the MKCircle/MKCircleView classes? Or will I have to go deeper and define a custom overlay object/view?
Thank you for your help :)

I had the same task and here is how I solve it:
NOTE: this code will only work starting from iOS7
Add an overlay to the map, somewhere in your view controller:
MyMapOverlay *overlay = [[MyMapOverlay alloc] initWithCoordinate:coordinate];
[self.mapView addOverlay:overlay level:MKOverlayLevelAboveLabels];
In the MKMapViewDelegate methods write next:
- (MKOverlayRenderer *)mapView:(MKMapView *)map rendererForOverlay:(id<MKOverlay>)overlay {
/// we need to draw overlay on the map in a way when everything except the area in radius of 500 should be grayed
/// to do that there is special renderer implemented - NearbyMapOverlay
if ([overlay isKindOfClass:[NearbyMapOverlay class]]) {
MyMapOverlayRenderer *renderer = [[MyMapOverlayRenderer alloc] initWithOverlay:overlay];
renderer.fillColor = [UIColor whateverColor];/// specify color which you want to use for gray out everything out of radius
renderer.diameterInMeters = 1000;/// choose whatever diameter you need
return renderer;
}
return nil;
}
The MyMapOverlay itself should be something like followed:
#interface MyMapOverlay : NSObject<MKOverlay>
- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate;
#end
#implementation MyMapOverlay
#synthesize coordinate = _coordinate;
- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate {
self = [super init];
if (self) {
_coordinate = coordinate;
}
return self;
}
- (MKMapRect)boundingMapRect {
return MKMapRectWorld;
}
#end
And the MyMapOverlayRenderer:
#interface MyMapOverlayRenderer : MKOverlayRenderer
#property (nonatomic, assign) double diameterInMeters;
#property (nonatomic, copy) UIColor *fillColor;
#end
#implementation MyMapOverlayRenderer
/// this method is called as a part of rendering the map, and it draws the overlay polygon by polygon
/// which means that it renders overlay by square pieces
- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)context {
/// main path - whole area
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(mapRect.origin.x, mapRect.origin.y, mapRect.size.width, mapRect.size.height)];
/// converting to the 'world' coordinates
double radiusInMapPoints = self.diameterInMeters * MKMapPointsPerMeterAtLatitude(self.overlay.coordinate.latitude);
MKMapSize radiusSquared = {radiusInMapPoints, radiusInMapPoints};
MKMapPoint regionOrigin = MKMapPointForCoordinate(self.overlay.coordinate);
MKMapRect regionRect = (MKMapRect){regionOrigin, radiusSquared}; //origin is the top-left corner
regionRect = MKMapRectOffset(regionRect, -radiusInMapPoints/2, -radiusInMapPoints/2);
// clamp the rect to be within the world
regionRect = MKMapRectIntersection(regionRect, MKMapRectWorld);
/// next path is used for excluding the area within the specific radius from current user location, so it will not be filled by overlay fill color
UIBezierPath *excludePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(regionRect.origin.x, regionRect.origin.y, regionRect.size.width, regionRect.size.height) cornerRadius:regionRect.size.width / 2];
[path appendPath:excludePath];
/// setting overlay fill color
CGContextSetFillColorWithColor(context, self.fillColor.CGColor);
/// adding main path. NOTE that exclusionPath was appended to main path, so we should only add 'path'
CGContextAddPath(context, path.CGPath);
/// tells the context to fill the path but with regards to even odd rule
CGContextEOFillPath(context);
}
As a result you will have exact same view like on the left image that was posted in the question.

The best way to do it, would be to subclass MKMapView and override the drawRect method call super, then paint over the map with the color you want.
Then each time the user moves, drawRect should respond by drawing appropriately.

Here a Swift version. Thanks Valerii.
https://github.com/dariopellegrini/MKInvertedCircle

i tried to use this swift version and it didn't work, so im posting my implementation (tested on iOS 12)
import Foundation
import UIKit
import MapKit
class MKInvertedCircleOverlayRenderer: MKOverlayRenderer {
var fillColor: UIColor = UIColor.red
var strokeColor: UIColor = UIColor.blue
var lineWidth: CGFloat = 3
var circle: MKCircle
init(circle: MKCircle) {
self.circle = circle
super.init(overlay: circle)
}
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
let path = UIBezierPath(rect: rect(for: MKMapRectWorld))
let excludePath: UIBezierPath = UIBezierPath(roundedRect: CGRect(x: circle.coordinate.latitude,
y: circle.coordinate.longitude,
width: circle.boundingMapRect.size.width,
height: circle.boundingMapRect.size.height),
cornerRadius: CGFloat(circle.boundingMapRect.size.width))
context.setFillColor(fillColor.cgColor)
path.append(excludePath)
context.addPath(path.cgPath)
context.fillPath(using: .evenOdd)
context.addPath(excludePath.cgPath)
context.setLineWidth(9 / zoomScale)
context.setStrokeColor(strokeColor.cgColor)
context.strokePath()
//line showing circle radius
let lineBeginPoint = CGPoint(x: excludePath.bounds.midX, y: excludePath.bounds.midY)
let lineEndPoint = CGPoint(x: excludePath.bounds.maxX, y: excludePath.bounds.midY)
let linePath: UIBezierPath = UIBezierPath()
linePath.move(to: lineBeginPoint)
linePath.addLine(to: lineEndPoint)
context.addPath(linePath.cgPath)
context.setLineWidth(6/zoomScale)
context.setStrokeColor(UIColor.black.cgColor)
context.setLineDash(phase: 1, lengths: [20 / zoomScale, 10 / zoomScale])
context.strokePath()
// circle at the end of the line above
let circleSize: CGFloat = 30/zoomScale
let circleRect = CGRect(origin: CGPoint(x: lineEndPoint.x - (circleSize/2), y: lineEndPoint.y - (circleSize/2)),
size: CGSize(width: circleSize, height: circleSize))
let circlePath: UIBezierPath =
UIBezierPath(roundedRect: circleRect, cornerRadius: circleSize)
context.addPath(circlePath.cgPath)
context.setFillColor(UIColor.black.cgColor)
context.fillPath()
}

Related

Accessing properties of class defined as IBDesignable in drawRect: method

I am having class called Circle (a subclass of UIView) and it is defined as IBDesignable, and some of its properties are defined as IBInspectable. Also there are some standard properties on a Circle class that I am trying to set and to see the changes at design time same as I do for inspectable properties. In the case of inspectable properties defined on Circle class, say strokeThickness, if they are changed through IB, everything works and I see the changes immediately on my view / xib.
When I say xib, I mean that I've created MyXib.xib file and dragged UIView on it, and set a custom class to XibOwnerClass. Also, when I load that xib I see changes at design time if change some of inspectable properties of Circle class.
How do I use this:
I have a view controller on a storyboard, and then I drag an UIView and change its class to XibOwnerClass. Imagine XibOwnerClass as a class that has one Circle. Now here, if I have set in my xib file that circle have red stroke, all circles that I add further in a XibOWnerClass will have red stroke, and that's fine. But the thing is, I want to set startAngle and endAngle per circle.
Now even this works, if I override prepareForInterfaceBuilder method and do something like this:
_circle.startAngle = 0.0f;
_circle.endAngle = 360.0f;
_anotherCircle.startAngle = 0.0f;
_circle.endAngle = 270.0f;
But still, if I click on my xib file, I can't see the changes at design time, but rather on runtime. So how to see these changes at design time with this setup?
Here is drawRect: method of a Circle class, but I guess that is not even needed, except it shows the usage of those non-inspectable properties:
In Circle.m file:
#import "Circle.h"
#define getRadians(angle) ((angle) / 180.0 * M_PI)
#define getDegrees(radians) ((radians) * (180.0 / M_PI))
#implementation Circle
- (void)drawRect:(CGRect)rect {
// Drawing code
CGPoint centerPoint = CGPointMake(rect.size.width/2.0f, rect.size.height / 2.0f);
UIBezierPath *path = [UIBezierPath
bezierPathWithArcCenter: centerPoint
radius:(rect.size.height / 2.0f)
startAngle: getRadians(self.startAngle)
endAngle: getRadians(self.endAngle)
clockwise:YES];
CAShapeLayer *shape = [CAShapeLayer new];
shape.path = path.CGPath;
shape.fillColor = self.mainColor.CGColor;
shape.strokeColor = self.strokeColor.CGColor;
shape.lineWidth = self.ringThicknes;
[self.layer addSublayer:shape];
}
And properties are defined in .h as:
#property(nonatomic) CGFloat startAngle;
#property(nonatomic) CGFloat endAngle;
//Some more IBInspectable properties go here, and those are properties that I set when i want to affect all circles.
I have implemented the same and the below solution is working, please make changes in your .h file:
#ifndef IB_DESIGNABLE
#define IB_DESIGNABLE
#endif
#import <UIKit/UIKit.h>
IB_DESIGNABLE #interface Circle : UIView
#property(nonatomic)IBInspectable CGFloat startAngle;
#property(nonatomic)IBInspectable CGFloat endAngle;
#property(nonatomic)IBInspectable CGFloat ringThicknes;
#property(nonatomic)IBInspectable UIColor* mainColor;
#property(nonatomic)IBInspectable UIColor *strokeColor;
#end
And in .m file
#import "Circle.h"
#define getRadians(angle) ((angle) / 180.0 * M_PI)
#define getDegrees(radians) ((radians) * (180.0 / M_PI))
#implementation Circle
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self drawCircle];
}
return self;
}
-(void)drawCircle {
CGPoint centerPoint = CGPointMake(self.frame.size.width/2.0f, self.frame.size.height / 2.0f);
UIBezierPath *path = [UIBezierPath
bezierPathWithArcCenter: centerPoint
radius:(self.frame.size.height / 2.0f)
startAngle: getRadians(self.startAngle)
endAngle: getRadians(self.endAngle)
clockwise:YES];
CAShapeLayer *shape = [CAShapeLayer new];
shape.path = path.CGPath;
shape.fillColor = self.mainColor.CGColor;
shape.strokeColor = self.strokeColor.CGColor;
shape.lineWidth = self.ringThicknes;
[self.layer addSublayer:shape];
}
- (void)drawRect:(CGRect)rect {
// Drawing code
[self drawCircle];
}
If you still face the same issue then make sure to turn on "Automatically Refresh Views" (in: Editor > Automatically Refresh Views) or manually update view by "Refresh All Views" (in: Editor > Refresh All Views) (when you need to update in IB).

UIKit Dynamics: recognize rounded Shapes and Boundaries

i am writing an App where i use UIKit Dynamics to simulate the interactions of different circles with one another.
I create my circles with the following code:
self = [super initWithFrame:CGRectMake(location.x - radius/2.0, location.y - radius/2, radius, radius)];
if (self) {
[self.layer setCornerRadius: radius /2.0f];
self.clipsToBounds = YES;
self.layer.masksToBounds = YES;
self.backgroundColor = color;
self.userInteractionEnabled = NO;
}
return self;
where location represents the desired location of the circle, and radius its radius.
I then add these circles to different UIBehaviours, by doing:
[_collision addItem:circle];
[_gravity addItem:circle];
[_itemBehaviour addItem:circle];
The itemBaviour is defined as follows:
_itemBehaviour = [[UIDynamicItemBehavior alloc] initWithItems:#[square]];
_itemBehaviour.elasticity = 1;
_itemBehaviour.friction = 0;
_itemBehaviour.resistance = 0;
_itemBehaviour.angularResistance = 0;
_itemBehaviour.allowsRotation = NO;
The problem i am having, is that my circles are behaving as squares. When hit in certain ways they gain angular momentum and lose speed. If they collide again, sometimes the angular momentum is again reverted to speed. This looks normal for squares, but when the view is round, like in my case, this behaviour looks weird and unnatural.
Turning on some debug options, i made this screenshot:
As you can see, the circle is appearently a square.
So my question is, how can i create an UIVIew that is truly a circle and will behave as such in UIKit Dynamics?
I know this question predated iOS 9, but for the benefit of future readers, you can now define a view with collisionBoundsType of UIDynamicItemCollisionBoundsTypePath and a circular collisionBoundingPath.
So, while you cannot "create an UIView that is truly a circle", you can define a path that defines both the shape that is rendered inside the view as well as the collision boundaries for the animator, yielding an effect of a round view (even though the view, itself, is obviously still rectangular, as all views are):
#interface CircleView: UIView
#property (nonatomic) CGFloat lineWidth;
#property (nonatomic, strong) CAShapeLayer *shapeLayer;
#end
#implementation CircleView
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self configure];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self configure];
}
return self;
}
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (void)configure {
self.translatesAutoresizingMaskIntoConstraints = false;
// create shape layer for circle
self.shapeLayer = [CAShapeLayer layer];
self.shapeLayer.strokeColor = [[UIColor blueColor] CGColor];
self.shapeLayer.fillColor = [[[UIColor blueColor] colorWithAlphaComponent:0.5] CGColor];
self.lineWidth = 3;
[self.layer addSublayer:self.shapeLayer];
}
- (void)layoutSubviews {
[super layoutSubviews];
// path of shape layer is with respect to center of the `bounds`
CGPoint center = CGPointMake(self.bounds.origin.x + self.bounds.size.width / 2, self.bounds.origin.y + self.bounds.size.height / 2);
self.shapeLayer.path = [[self circularPathWithLineWidth:self.lineWidth center:center] CGPath];
}
- (UIDynamicItemCollisionBoundsType)collisionBoundsType {
return UIDynamicItemCollisionBoundsTypePath;
}
- (UIBezierPath *)collisionBoundingPath {
// path of collision bounding path is with respect to center of the dynamic item, so center of this path will be CGPointZero
return [self circularPathWithLineWidth:0 center:CGPointZero];
}
- (UIBezierPath *)circularPathWithLineWidth:(CGFloat)lineWidth center:(CGPoint)center {
CGFloat radius = (MIN(self.bounds.size.width, self.bounds.size.height) - self.lineWidth) / 2;
return [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:true];
}
#end
Then, when you do your collision, it will honor the collisionBoundingPath values:
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
// create circle views
CircleView *circle1 = [[CircleView alloc] initWithFrame:CGRectMake(60, 100, 80, 80)];
[self.view addSubview:circle1];
CircleView *circle2 = [[CircleView alloc] initWithFrame:CGRectMake(250, 150, 120, 120)];
[self.view addSubview:circle2];
// have them collide with each other
UICollisionBehavior *collision = [[UICollisionBehavior alloc] initWithItems:#[circle1, circle2]];
[self.animator addBehavior:collision];
// with perfect elasticity
UIDynamicItemBehavior *behavior = [[UIDynamicItemBehavior alloc] initWithItems:#[circle1, circle2]];
behavior.elasticity = 1;
[self.animator addBehavior:behavior];
// and push one of the circles
UIPushBehavior *push = [[UIPushBehavior alloc] initWithItems:#[circle1] mode:UIPushBehaviorModeInstantaneous];
[push setAngle:0 magnitude:1];
[self.animator addBehavior:push];
That yields:
By the way, it should be noted that the documentation outlines a few limitations to the path:
The path object you create must represent a convex polygon with counter-clockwise or clockwise winding, and the path must not intersect itself. The (0, 0) point of the path must be located at the center point of the corresponding dynamic item. If the center point does not match the path’s origin, collision behaviors may not work as expected.
But a simple circle path easily meets those criteria.
Or, for Swift users:
class CircleView: UIView {
var lineWidth: CGFloat = 3
var shapeLayer: CAShapeLayer = {
let _shapeLayer = CAShapeLayer()
_shapeLayer.strokeColor = UIColor.blue.cgColor
_shapeLayer.fillColor = UIColor.blue.withAlphaComponent(0.5).cgColor
return _shapeLayer
}()
override func layoutSubviews() {
super.layoutSubviews()
layer.addSublayer(shapeLayer)
shapeLayer.lineWidth = lineWidth
let center = CGPoint(x: bounds.midX, y: bounds.midY)
shapeLayer.path = circularPath(lineWidth: lineWidth, center: center).cgPath
}
private func circularPath(lineWidth: CGFloat = 0, center: CGPoint = .zero) -> UIBezierPath {
let radius = (min(bounds.width, bounds.height) - lineWidth) / 2
return UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
}
override var collisionBoundsType: UIDynamicItemCollisionBoundsType { return .path }
override var collisionBoundingPath: UIBezierPath { return circularPath() }
}
class ViewController: UIViewController {
let animator = UIDynamicAnimator()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let circle1 = CircleView(frame: CGRect(x: 60, y: 100, width: 80, height: 80))
view.addSubview(circle1)
let circle2 = CircleView(frame: CGRect(x: 250, y: 150, width: 120, height: 120))
view.addSubview(circle2)
animator.addBehavior(UICollisionBehavior(items: [circle1, circle2]))
let behavior = UIDynamicItemBehavior(items: [circle1, circle2])
behavior.elasticity = 1
animator.addBehavior(behavior)
let push = UIPushBehavior(items: [circle1], mode: .instantaneous)
push.setAngle(0, magnitude: 1)
animator.addBehavior(push)
}
}
Ok, well first off.
The debug options you enabled show areas of transparent cells. The view that is the circle is actually a square with rounded edges.
All views are rectangular. The way they appear circular is by making the corners transparent (hence corner radius).
Second, what is it you're trying to do with UIKit Dynamics? What is on the screen looks like you're trying to create a game of some sort.
Dynamics is meant to be used for more natural and real looking animation of UI. It isn't meant to be a full-on physics engine.
If you want something like that then you're best using Sprite Kit.

Detecting tap inside a bezier path

I have a UIView which is added as a subview to my view controller. I have drawn a bezier path on that view. My drawRect implementation is below
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath *bpath = [UIBezierPath bezierPath];
[bpath moveToPoint:CGPointMake(50, 50)];
[bpath addLineToPoint:CGPointMake(100, 50)];
[bpath addLineToPoint:CGPointMake(100, 100)];
[bpath addLineToPoint:CGPointMake(50, 100)];
[bpath closePath];
CGContextAddPath(context, bpath.CGPath);
CGContextSetStrokeColorWithColor(context,[UIColor blackColor].CGColor);
CGContextSetLineWidth(context, 2.5);
CGContextStrokePath(context);
UIColor *fillColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.5 alpha:0.7];
[fillColor setFill];
[bpath fill];
}
I want detect tap inside this bezier path but not the point which is inside the UIView and outside the path. For example in this case if my touch coordinate is (10, 10), it should not be detected. I know about CGContextPathContainsPoint but it does not help when touch is inside the path. Is there a way to detect touch events inside bezier path?
There is a function CGPathContainsPoint() it may be useful in your case.
Also be careful if you get gesture point from superview, the coordinate may not be correct with your test. You have a method to convertPoint from or to a particular view's coordinate system:
- (CGPoint)convertPoint:(CGPoint)point toView:(UIView *)view
- (CGPoint)convertPoint:(CGPoint)point fromView:(UIView *)view
Try UIBezierPath's method :
func contains(_ point: CGPoint) -> Bool
Returns a Boolean value indicating whether the area enclosed by the
receiver contains the specified point.
Detecting tap inside a bezier path in swift :-
It's simple in latest swift ,follow these steps and you will get your UIBezierPath touch event.
Step 1 :- Initialize Tap Event on view where your UIBeizerPath Added.
///Catch layer by tap detection
let tapRecognizer:UITapGestureRecognizer = UITapGestureRecognizer.init(target: self, action: #selector(YourClass.tapDetected(_:)))
viewSlices.addGestureRecognizer(tapRecognizer)
Step 2 :- Make "tapDetected" method
//MARK:- Hit TAP
public func tapDetected(tapRecognizer:UITapGestureRecognizer){
let tapLocation:CGPoint = tapRecognizer.locationInView(viewSlices)
self.hitTest(CGPointMake(tapLocation.x, tapLocation.y))
}
Step 3 :- Make "hitTest" final method
public func hitTest(tapLocation:CGPoint){
let path:UIBezierPath = yourPath
if path.containsPoint(tapLocation){
//tap detected do what ever you want ..;)
}else{
//ooops you taped on other position in view
}
}
Update: Swift 4
Step 1 :- Initialize Tap Event on view where your UIBeizerPath Added.
///Catch layer by tap detection
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(YourClass.tapDetected(tapRecognizer:)))
viewSlices.addGestureRecognizer(tapRecognizer)
Step 2 :- Make "tapDetected" method
public func tapDetected(tapRecognizer:UITapGestureRecognizer){
let tapLocation:CGPoint = tapRecognizer.location(in: viewSlices)
self.hitTest(tapLocation: CGPoint(x: tapLocation.x, y: tapLocation.y))
}
Step 3 :- Make "hitTest" final method
private func hitTest(tapLocation:CGPoint){
let path:UIBezierPath = yourPath
if path.contains(tapLocation){
//tap detected do what ever you want ..;)
}else{
//ooops you taped on other position in view
}
}
A solution in Swift 3.1 (porting over the Apple recommended solution from here)
func containsPoint(_ point: CGPoint, path: UIBezierPath, inFillArea: Bool) -> Bool {
UIGraphicsBeginImageContext(self.size)
let context: CGContext? = UIGraphicsGetCurrentContext()
let pathToTest = path.cgPath
var isHit = false
var mode: CGPathDrawingMode = CGPathDrawingMode.stroke
if inFillArea {
// check if UIBezierPath uses EO fill
if path.usesEvenOddFillRule {
mode = CGPathDrawingMode.eoFill
} else {
mode = CGPathDrawingMode.fill
}
} // else mode == stroke
context?.saveGState()
context?.addPath(pathToTest)
isHit = (context?.pathContains(point, mode: mode))!
context?.restoreGState()
return isHit
}

HitTest with CAShapeLayer doesn't work

I use the following code to check wether a point of a player is in a circle area:
if ([circle.presentationLayer hitTest:player.position])
{
NSLog(#"hit");
}
My circle is a CAShapeLayer:
CAShapeLayer *circle = [CAShapeLayer layer];
CGFloat radius = 50;
[circle setMasksToBounds:YES];
[circle setBackgroundColor:[UIColor redColor].CGColor];
[circle setCornerRadius:radius1];
[circle setBounds:CGRectMake(0.0f, 0.0f, radius *2, radius *2)];
[self.view.layer addSublayer:circle];
The collision detection works very well that way.
Now I don't want to hittest the player's position with a circle layer but with a CAShapeLayer drawn along a custom path:
CAShapeLayer *customLayer = [CAShapeLayer layer];
customLayer.path = customPath.CGPath;
customLayer.fillColor = [UIColor yellowColor].CGColor;
customLayer.shouldRasterize = YES;
customLayer.opacity = 0.2;
[self.view.layer addSublayer: customLayer];
When I want to hittest the player's position with the custom layer the hittest doesn't work anymore.
How can I solve this problem?
Are you converting the position to the correct relative position?
For example:
CGPoint layerPoint = [[dynamicView layer] convertPoint:touchLocation toLayer:sublayer];
Also, perhaps you need CGPathContainsPoint:
if(CGPathContainsPoint(shapeLayer.path, 0, layerPoint, YES))
{
You are assigning the bounds/frame of your circle, but not that of your bezier path. I did not know that until I stumbled onto it myself, but when you create a shape and assign a path to a CAShapeLayer(), you must assign the CALayer's frame (or separately the position and the bounds) because the frame of a CALayer(), as per Apple's documentation, defaults to a CGZeroRect and does not get automatically updated when you set the path. hitTest() is based on the position+bounds (or frame) and therefore fails. The documentation says:
/* Returns the farthest descendant of the layer containing point 'p'.
* Siblings are searched in top-to-bottom order. 'p' is defined to be
* in the coordinate space of the receiver's nearest ancestor that
* isn't a CATransformLayer (transform layers don't have a 2D
* coordinate space in which the point could be specified). */
(nullable CALayer *)hitTest:(CGPoint)p;
/* Returns true if the bounds of the layer contains point 'p'. */
(BOOL)containsPoint:(CGPoint)p;
A quick unit test shows this (extension CGPath is useful and included after):
func testShapeLayer() {
let layer = CAShapeLayer()
let bezier = NSBezierPath(rect: NSMakeRect(0,0,10,10))
layer.path = bezier.CGPath
// remove the following line to FAIL the test
layer.frame = NSMakeRect(0,0,10,10)
XCTAssertNotNil(layer.hitTest(CGPoint(x:5,y:5)))
XCTAssertTrue(layer.contains(CGPoint(x:5,y:5)))
}
extension NSBezierPath {
public var CGPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)
for i in 0 ..< self.elementCount {
let type = self.element(at: i, associatedPoints: &points)
switch type {
case .moveToBezierPathElement: path.move(to: CGPoint(x: points[0].x, y: points[0].y) )
case .lineToBezierPathElement: path.addLine(to: CGPoint(x: points[0].x, y: points[0].y) )
case .curveToBezierPathElement: path.addCurve( to: CGPoint(x: points[2].x, y: points[2].y),
control1: CGPoint(x: points[0].x, y: points[0].y),
control2: CGPoint(x: points[1].x, y: points[1].y) )
case .closePathBezierPathElement: path.closeSubpath()
}
}
return path
}
}
Yet, that's probably not what you want: you want the path to be used for hitTesting(), not just the frame. I would therefore probably add an extension to your hitTesting routine: if the CAShapeLayer() is hit, it means that it is within the frame, then you can be more specific and check with CGPathContainsPoint() like the other answer mentioned.

Draw line in UIView

I need to draw a horizontal line in a UIView. What is the easiest way to do it. For example, I want to draw a black horizontal line at y-coord=200.
I am NOT using Interface Builder.
Maybe this is a bit late, but I want to add that there is a better way.
Using UIView is simple, but relatively slow. This method overrides how the view draws itself and is faster:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(context, 2.0f);
CGContextMoveToPoint(context, 0.0f, 0.0f); //start at this point
CGContextAddLineToPoint(context, 20.0f, 20.0f); //draw to this point
// and now draw the Path!
CGContextStrokePath(context);
}
The easiest way in your case (horizontal line) is to add a subview with black background color and frame [0, 200, 320, 1].
Code sample (I hope there are no errors - I wrote it without Xcode):
UIView *lineView = [[UIView alloc] initWithFrame:CGRectMake(0, 200, self.view.bounds.size.width, 1)];
lineView.backgroundColor = [UIColor blackColor];
[self.view addSubview:lineView];
[lineView release];
// You might also keep a reference to this view
// if you are about to change its coordinates.
// Just create a member and a property for this...
Another way is to create a class that will draw a line in its drawRect method (you can see my code sample for this here).
Swift 3 and Swift 4
This is how you can draw a gray line at the end of your view (same idea as b123400's answer)
class CustomView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
if let context = UIGraphicsGetCurrentContext() {
context.setStrokeColor(UIColor.gray.cgColor)
context.setLineWidth(1)
context.move(to: CGPoint(x: 0, y: bounds.height))
context.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
context.strokePath()
}
}
}
Just add a Label without text and with background color.
Set the Coordinates of your choice and also height and width.
You can do it manually or with Interface Builder.
You can user UIBezierPath Class for this:
And can draw as many lines as you want:
I have subclassed UIView :
#interface MyLineDrawingView()
{
NSMutableArray *pathArray;
NSMutableDictionary *dict_path;
CGPoint startPoint, endPoint;
}
#property (nonatomic,retain) UIBezierPath *myPath;
#end
And initialized the pathArray and dictPAth objects which will be used for line drawing. I am writing the main portion of the code from my own project:
- (void)drawRect:(CGRect)rect
{
for(NSDictionary *_pathDict in pathArray)
{
[((UIColor *)[_pathDict valueForKey:#"color"]) setStroke]; // this method will choose the color from the receiver color object (in this case this object is :strokeColor)
[[_pathDict valueForKey:#"path"] strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
[[dict_path objectForKey:#"color"] setStroke]; // this method will choose the color from the receiver color object (in this case this object is :strokeColor)
[[dict_path objectForKey:#"path"] strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
touchesBegin method :
UITouch *touch = [touches anyObject];
startPoint = [touch locationInView:self];
myPath=[[UIBezierPath alloc]init];
myPath.lineWidth = currentSliderValue*2;
dict_path = [[NSMutableDictionary alloc] init];
touchesMoved Method:
UITouch *touch = [touches anyObject];
endPoint = [touch locationInView:self];
[myPath removeAllPoints];
[dict_path removeAllObjects];// remove prev object in dict (this dict is used for current drawing, All past drawings are managed by pathArry)
// actual drawing
[myPath moveToPoint:startPoint];
[myPath addLineToPoint:endPoint];
[dict_path setValue:myPath forKey:#"path"];
[dict_path setValue:strokeColor forKey:#"color"];
// NSDictionary *tempDict = [NSDictionary dictionaryWithDictionary:dict_path];
// [pathArray addObject:tempDict];
// [dict_path removeAllObjects];
[self setNeedsDisplay];
touchesEnded Method:
NSDictionary *tempDict = [NSDictionary dictionaryWithDictionary:dict_path];
[pathArray addObject:tempDict];
[dict_path removeAllObjects];
[self setNeedsDisplay];
One other (and an even shorter) possibility. If you're inside drawRect, something like the following:
[[UIColor blackColor] setFill];
UIRectFill((CGRect){0,200,rect.size.width,1});
Swift 3, 4, 5
This is the simplest of all I could find...
let lineView = UIView(frame: CGRect(x: 4, y: 50, width: self.view.frame.width, height: 1))
lineView.backgroundColor = .lightGray
self.view.addSubview(lineView)
Based on Guy Daher's answer.
I try to avoid using ? because it can cause an application crash if the GetCurrentContext() returns nil.
I would do nil check if statement:
class CustomView: UIView
{
override func draw(_ rect: CGRect)
{
super.draw(rect)
if let context = UIGraphicsGetCurrentContext()
{
context.setStrokeColor(UIColor.gray.cgColor)
context.setLineWidth(1)
context.move(to: CGPoint(x: 0, y: bounds.height))
context.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
context.strokePath()
}
}
}
Add label without text and with background color corresponding frame size(ex:height=1). Do it through code or in interface builder.

Resources