diagonal lines from the top right trying to draw on a UIView - ios

I've got a tablet called a Slate that I can process x,y points from. I can pass these x,y points to my "drawShape" function, the equivalent of a touchesMoved function. I have a custom UIView class designed for this purpose with 4 main functions. The first three are touchesbegan/moved/ended and there is a drawRect.
Here's the source to the UIView subclass...
//
// DrawingLayerView.m
//
//
// Created by Monica Kachlon on 12/3/17.
//
#import "DrawingLayerView.h"
#include "UIBezierPath+Interpolation.h"
#interface StrokeA : NSObject
#property (assign, nonatomic) CGPoint startPoint;
- (id)initWithStartPoint:(CGPoint)point;
#property (strong, nonatomic) NSMutableArray * points;
#end
#implementation StrokeA
- (id)initWithStartPoint:(CGPoint)point {
self = [super init];
self.startPoint = point;
self.points = [NSMutableArray array];
return self;
}
#end
#implementation DrawingLayerView
NSMutableArray * strokes;
NSMutableArray * points;
StrokeA * currentStroke;
UIBezierPath * currentPath;
UIBezierPath *path;
UIBezierPath *pathTwo;
UIBezierPath *Newpath;
CAShapeLayer * currentLayer;
- (void) noodlez
{
path = [UIBezierPath bezierPath];
currentPath = [UIBezierPath bezierPath];
pathTwo = [UIBezierPath bezierPath];
Newpath = [UIBezierPath bezierPath];
strokes = [NSMutableArray array];
points = [NSMutableArray array];
}
- (void)startTouch: (CGPoint)point
{
currentStroke = [[StrokeA alloc] initWithStartPoint:point];
[strokes addObject:currentStroke];
}
- (UIBezierPath *)createPath {
UIBezierPath * bezierPath = [UIBezierPath bezierPath];
for (StrokeA * stroke in strokes) {
[bezierPath moveToPoint:stroke.startPoint];
for (NSValue * value in stroke.points) {
[bezierPath addLineToPoint:[value CGPointValue]];
}
}
return bezierPath;
}
- (void)endTouch: (CGPoint)point
{
[points removeAllObjects];
[self setNeedsDisplay];
}
- (void)drawShape: (CGPoint)point
{
if (!currentStroke) {
[self startTouch:point];
}
[currentStroke.points addObject:[NSValue valueWithCGPoint:point]];
pathTwo = [self createPath];
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
[[UIColor blackColor] setStroke];
[pathTwo stroke];
}
#end
And here's the screenshot.
I tried writing "Ok?" Just to illustrate the issue.

Despite the fact that the code is lacking some detail and the question is poorly worded, the screenshot is clearly showing that the initial point for the path is (0,0) in each case. Dump the points in the paths out to the console and you'll see it. Figure out how those (0,0)'s are getting in there and you'll solve the problem.

Related

Zoom UIView inside UIScrollView with drawing

I have a scroll view (gray) with a zooming view inside (orange). The problem is if I zoom this view the red shape drawn on it gets zoomed too including lines width and blue squares size. What I want is to keep constant lines width and blue squares size (like on first picture) scaling just the area of the shape itself according to zoom level (drawn text is just for reference, I don't care about its size)
before zoom
after zoom
view controller
#import "ViewController.h"
#import "ZoomingView.h"
#interface ViewController ()
#property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
#end
#implementation ViewController
{
ZoomingView *_zoomingView;
}
- (void)viewDidLayoutSubviews
{
[self setup];
}
- (void)setup
{
CGFloat kViewSize = self.scrollView.frame.size.width - 40;
self.scrollView.minimumZoomScale = 1;
self.scrollView.maximumZoomScale = 10;
self.scrollView.delegate = self;
self.scrollView.contentSize = self.scrollView.bounds.size;
_zoomingView = [[ZoomingView alloc] initWithFrame:
CGRectMake((self.scrollView.frame.size.width - kViewSize) / 2,
(self.scrollView.frame.size.height - kViewSize) / 2,
kViewSize,
kViewSize)];
[self.scrollView addSubview:_zoomingView];
}
#pragma mark - UIScrollViewDelegate
- (UIView*)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return _zoomingView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
// zooming view position fix
UIView *zoomView = [scrollView.delegate viewForZoomingInScrollView:scrollView];
CGRect zvf = zoomView.frame;
if (zvf.size.width < scrollView.bounds.size.width) {
zvf.origin.x = (scrollView.bounds.size.width - zvf.size.width) / 2.0f;
} else {
zvf.origin.x = 0.0;
}
if (zvf.size.height < scrollView.bounds.size.height) {
zvf.origin.y = (scrollView.bounds.size.height - zvf.size.height) / 2.0f;
} else {
zvf.origin.y = 0.0;
}
zoomView.frame = zvf;
[_zoomingView updateWithZoomScale:scrollView.zoomScale];
}
#end
zooming view
#import "ZoomingView.h"
#implementation ZoomingView
{
CGFloat _zoomScale;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
- (void)setup
{
self.backgroundColor = [UIColor orangeColor];
_zoomScale = 1;
}
- (void)drawRect:(CGRect)rect
{
const CGFloat kPointSize = 10;
NSArray *points = #[[NSValue valueWithCGPoint:CGPointMake(30, 30)],
[NSValue valueWithCGPoint:CGPointMake(200, 40)],
[NSValue valueWithCGPoint:CGPointMake(180, 200)],
[NSValue valueWithCGPoint:CGPointMake(70, 180)]];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 1);
// points
[[UIColor blueColor] setStroke];
for (NSValue *value in points) {
CGPoint point = [value CGPointValue];
CGContextStrokeRect(context, CGRectMake(point.x - kPointSize / 2,
point.y - kPointSize / 2,
kPointSize,
kPointSize));
}
// lines
[[UIColor redColor] setStroke];
for (NSUInteger i = 0; i < points.count; i++) {
CGPoint point = [points[i] CGPointValue];
if (i == 0) {
CGContextMoveToPoint(context, point.x, point.y);
} else {
CGContextAddLineToPoint(context, point.x, point.y);
}
}
CGContextClosePath(context);
CGContextStrokePath(context);
// text
NSAttributedString *str = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"%f", _zoomScale] attributes:#{NSFontAttributeName : [UIFont systemFontOfSize:12]}];
[str drawAtPoint:CGPointMake(5, 5)];
}
- (void)updateWithZoomScale:(CGFloat)zoomScale
{
_zoomScale = zoomScale;
[self setNeedsDisplay];
}
#end
EDIT
Based on proposed solution (which works for sure) I was interested if I could make it work using my drawRect routine and Core Graphics methods.
So I changed my code this way, applying proposed scaling and contentsScale approach from this answer. As a result, without contentsScale drawing looks very blurry and with it much better, but a light blurriness persists anyway.
So the approach with layers gives the best result, although I don't get why.
- (void)drawRect:(CGRect)rect
{
const CGFloat kPointSize = 10;
NSArray *points = #[[NSValue valueWithCGPoint:CGPointMake(30, 30)],
[NSValue valueWithCGPoint:CGPointMake(200, 40)],
[NSValue valueWithCGPoint:CGPointMake(180, 200)],
[NSValue valueWithCGPoint:CGPointMake(70, 180)]];
CGFloat scaledPointSize = kPointSize * (1.0 / _zoomScale);
CGFloat lineWidth = 1.0 / _zoomScale;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, lineWidth);
// points
[[UIColor blueColor] setStroke];
for (NSValue *value in points) {
CGPoint point = [value CGPointValue];
CGContextStrokeRect(context, CGRectMake(point.x - scaledPointSize / 2,
point.y - scaledPointSize / 2,
scaledPointSize,
scaledPointSize));
}
// lines
[[UIColor redColor] setStroke];
for (NSUInteger i = 0; i < points.count; i++) {
CGPoint point = [points[i] CGPointValue];
if (i == 0) {
CGContextMoveToPoint(context, point.x, point.y);
} else {
CGContextAddLineToPoint(context, point.x, point.y);
}
}
CGContextClosePath(context);
CGContextStrokePath(context);
// text
NSAttributedString *str = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"%f", _zoomScale] attributes:#{NSFontAttributeName : [UIFont systemFontOfSize:12]}];
[str drawAtPoint:CGPointMake(5, 5)];
}
- (void)updateWithZoomScale:(CGFloat)zoomScale
{
_zoomScale = zoomScale;
[self setNeedsDisplay];
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithBool:YES]
forKey:kCATransactionDisableActions];
self.layer.contentsScale = zoomScale;
[CATransaction commit];
}
You may be better off putting your boxes and line-shape on CAShapeLayers, where you can update the line-width based on the zoom scale.
You only need to create and define your line-shape once. For your boxes, though, you'll need to re-create the path when you change the zoom (to keep the width/height of the boxes at a constant non-zoomed point size.
Give this a try. You should be able to simply replace your current ZoomingView.m class - no changes to the view controller necessary.
//
// ZoomingView.m
//
// modified by Don Mag
//
#import "ZoomingView.h"
#interface ZoomingView()
#property (strong, nonatomic) CAShapeLayer *shapeLayer;
#property (strong, nonatomic) CAShapeLayer *boxesLayer;
#property (strong, nonatomic) NSArray *points;
#end
#implementation ZoomingView
{
CGFloat _zoomScale;
CGFloat _kPointSize;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setup];
}
return self;
}
- (void)setup
{
self.backgroundColor = [UIColor orangeColor];
_points = #[[NSValue valueWithCGPoint:CGPointMake(30, 30)],
[NSValue valueWithCGPoint:CGPointMake(200, 40)],
[NSValue valueWithCGPoint:CGPointMake(180, 200)],
[NSValue valueWithCGPoint:CGPointMake(70, 180)]];
_zoomScale = 1;
_kPointSize = 10.0;
// create and setup boxes layer
_boxesLayer = [CAShapeLayer new];
[self.layer addSublayer:_boxesLayer];
_boxesLayer.strokeColor = [UIColor redColor].CGColor;
_boxesLayer.fillColor = [UIColor clearColor].CGColor;
_boxesLayer.lineWidth = 1.0;
_boxesLayer.frame = self.bounds;
// create and setup shape layer
_shapeLayer = [CAShapeLayer new];
[self.layer addSublayer:_shapeLayer];
_shapeLayer.strokeColor = [UIColor greenColor].CGColor;
_shapeLayer.fillColor = [UIColor clearColor].CGColor;
_shapeLayer.lineWidth = 1.0;
_shapeLayer.frame = self.bounds;
// new path for shape
UIBezierPath *thePath = [UIBezierPath new];
for (NSValue *value in _points) {
CGPoint point = [value CGPointValue];
if ([value isEqualToValue:_points.firstObject]) {
[thePath moveToPoint:point];
} else {
[thePath addLineToPoint:point];
}
}
[thePath closePath];
[_shapeLayer setPath:thePath.CGPath];
// trigger the boxes update
[self updateWithZoomScale:_zoomScale];
}
- (void)drawRect:(CGRect)rect
{
// text
NSAttributedString *str = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"%f", _zoomScale] attributes:#{NSFontAttributeName : [UIFont systemFontOfSize:12]}];
[str drawAtPoint:CGPointMake(5, 5)];
}
- (void)updateWithZoomScale:(CGFloat)zoomScale
{
_zoomScale = zoomScale;
CGFloat scaledPointSize = _kPointSize * (1.0 / zoomScale);
// create a path for the boxes
// needs to be done here, because the width/height of the boxes
// must change with the scale
UIBezierPath *thePath = [UIBezierPath new];
for (NSValue *value in _points) {
CGPoint point = [value CGPointValue];
CGRect r = CGRectMake(point.x - scaledPointSize / 2.0,
point.y - scaledPointSize / 2.0,
scaledPointSize,
scaledPointSize);
[thePath appendPath:[UIBezierPath bezierPathWithRect:r]];
}
[_boxesLayer setPath:thePath.CGPath];
_boxesLayer.lineWidth = 1.0 / zoomScale;
_shapeLayer.lineWidth = 1.0 / zoomScale;
[self setNeedsDisplay];
}
#end
Results:
Note: Should go without saying, but... This is intended to be a starting point for you to work with, not "production code."

Creating a circular progress bar around an image in swift

I am trying to create a circular progress bar around an image as shown in the screenshot below. So far I have managed to create a round image with a green border using the code below:
self.image.layer.cornerRadius = self.image.frame.size.width / 2
self.image.clipsToBounds = true
self.image.layer.borderWidth = 6.0
self.image.layer.borderColor = UIColor.greenColor.CGColor
My question is, how do I create a circular progress bar from the border than I have set? Or do I need to remove this border and take a different approach?
I understood your problem and I suggest you to use some library to create such a loader you can find many of the swift libraries. On of them is FPActivityIndicator, it is the same circular loader which you are searching for, you only have to adjust the radius and position of the loader to move it around the image
if you need further help you can send me message and if it helps please accept the answer. Thanks
I have customized the answer of WDUK in stack overflow post according to your need,
It is something like,
TestView.h
#import <UIKit/UIKit.h>
#interface TestView : UIView
#property (nonatomic) double percent;
#end
TestView.m
#import "TestView.h"
#interface TestView () {
CGFloat startAngle;
CGFloat endAngle;
}
#end
#implementation TestView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor whiteColor];
// Determine our start and stop angles for the arc (in radians)
startAngle = M_PI * 1.5;
endAngle = startAngle + (M_PI * 2);
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGFloat constant = rect.size.width/ 5;
UIImage *img = [UIImage imageNamed:#"lion.jpg"]; // lion.jpg is image name
UIImageView *imgView = [[UIImageView alloc]initWithFrame:CGRectMake(rect.origin.x + constant/2, rect.origin.y + constant/2, rect.size.width-constant, rect.size.height - constant)];
imgView.image = img;
imgView.layer.masksToBounds = YES;
imgView.layer.cornerRadius = imgView.frame.size.width / 2;
UIBezierPath* bezierPath = [UIBezierPath bezierPath];
// Create our arc, with the correct angles
[bezierPath addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
radius:constant*2
startAngle:startAngle
endAngle:(endAngle - startAngle) * (_percent / 100.0) + startAngle
clockwise:YES];
// Set the display for the path, and stroke it
bezierPath.lineWidth = 20;
[[UIColor redColor] setStroke];
[bezierPath stroke];
[self addSubview:imgView];
}
#end
ViewController.m
#import "ViewController.h"
#import "TestView.h"
#interface ViewController (){
TestView* m_testView;
NSTimer* m_timer;
}
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Init our view
CGRect frame = CGRectMake(50, 50, 200, 200);
m_testView = [[TestView alloc] initWithFrame:frame];
m_testView.percent = 0;
[self.view addSubview:m_testView];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)viewDidAppear:(BOOL)animated
{
// Kick off a timer to count it down
m_timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(increamentSpin) userInfo:nil repeats:YES];
}
- (void)increamentSpin
{
// increament our percentage, do so, and redraw the view
if (m_testView.percent < 100) {
m_testView.percent = m_testView.percent + 1;
[m_testView setNeedsDisplay];
}
else {
[m_timer invalidate];
m_timer = nil;
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
If you are decreasing frame size from viewcontroller then you should reduce bezierPath.lineWidth accordingly to show respectively thin progress around imageview.
And this is working perfact, i have tested it!!!

iOS method containsPoint is not working

I have a view like this:
// .h file
#interface LayerBubbleView : UIView
/**
* click handler
*/
#property (nonatomic, copy) void (^clickHandler)(NSString* identifier, CAShapeLayer* layer);
-(instancetype)initWithFrame:(CGRect)frame withDataArray:(NSArray *)dataArray;
#end
// .m file
#import "BDPLayerBubbleView.h"
#interface BDPLayerBubbleView ()
#property (nonatomic, strong) NSMutableArray *paths;
#property (nonatomic, strong) NSMutableArray *identifiers;
#end
#implementation BDPLayerBubbleView
-(instancetype)initWithFrame:(CGRect)frame withDataArray:(NSArray *)dataArray {
self = [self initWithFrame:frame];
if (self) {
_paths = [[NSMutableArray alloc] init];
_identifiers = [[NSMutableArray alloc] init];
for (int i = 0; i < dataArray.count; i++) {
NSArray *data = dataArray[i];
CGRect frame = [data[0] CGRectValue]; // please ignore here, just for demo
CGFloat radius = [data[1] doubleValue];
CGPoint center = [data[2] CGPointValue];
UIColor *fillColor = data[3];
UIColor *borderColor = data[4];
NSString *identifier = data[5];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:radius];
[_paths addObject:bezierPath];
[_identifiers addObject:identifier];
shapeLayer.path = [bezierPath CGPath];
shapeLayer.strokeColor = [borderColor CGColor];
shapeLayer.fillColor = [fillColor CGColor];
shapeLayer.lineWidth = 0.5;
shapeLayer.bounds = frame;
shapeLayer.position = center;
[self.layer addSublayer:shapeLayer];
}
}
return self;
}
#pragma mark - Touch handling
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint touchPoint = [touch locationInView:self];
for(int i=0;i<[_paths count];i++) {
UIBezierPath* path = _paths[i];
CAShapeLayer *shapeLayer = self.layer.sublayers[i];
if ([path containsPoint:touchPoint] || [shapeLayer containsPoint:touchPoint])
{
NSString *identifier = _identifiers[i];
if([self.layer.sublayers[i] isKindOfClass:CAShapeLayer.class] && [identifier length] > 0) {
CAShapeLayer* l = self.layer.sublayers[i];
if(_clickHandler) {
_clickHandler(identifier, l);
}
}
}
}
}
The logic is, there are lots of bubble layers on the view, and when user taps on the bubble, I can detect which bubble layer user taps, so I can trigger the corresponding clickHandler and highlight the bubble layer.
However, neither [path containsPoint:touchPoint] nor [shapeLayer containsPoint:touchPoint] return YES for all the sub layers. I am not sure what's wrong with my code?
So I was suggest to use CGPathContainsPoint and CGRectContainsPoint to detect. I tested, CGPathContainsPoint DOES NOT work while CGRectContainsPointDID work.
I am now confused the differences among the CG APIs and the UIKit wrapped APIs. Could any expert to explain? I still hope containsPoint can work.

iOS Custom Annotation: A view below the annotation pin

I need to replace the default annotation view with my custom annotation view.
I need the do following things:
Custom Annotation view with an image view embedded in it.
A view below it which contains a label in it.
For more clarification see the image:
In the above image I need to place an image view in the white space which you can see in the image in circular form, next I also need to add a view which contains a label on which I can set any text like me, friends, etc...
So, for this I searched number of questions on stack overflow but didn't got my answer. I don't want it on call out, I just want it simply as annotation when map is rendered. I have tried to make a custom class for this but not getting any idea how to deal with this.
Any help will be highly appreciated
You could just create your own annotation view:
#import MapKit;
#interface CustomAnnotationView : MKAnnotationView
#end
#interface CustomAnnotationView ()
#property (nonatomic) CGSize textSize;
#property (nonatomic) CGSize textBubbleSize;
#property (nonatomic, weak) UILabel *label;
#property (nonatomic) CGFloat lineWidth;
#property (nonatomic) CGFloat pinRadius;
#property (nonatomic) CGFloat pinHeight;
#property (nonatomic, strong) UIBezierPath *pinPath;
#property (nonatomic, strong) UIBezierPath *textBubblePath;
#end
#implementation CustomAnnotationView
- (instancetype)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
if (self) {
self.lineWidth = 1.0;
self.pinHeight = 40;
self.pinRadius = 15;
UILabel *label = [[UILabel alloc] init];
label.textAlignment = NSTextAlignmentCenter;
label.font = [UIFont preferredFontForTextStyle:UIFontTextStyleCallout];
label.textColor = [UIColor whiteColor];
[self addSubview:label];
self.label = label;
[self adjustLabelWidth:annotation];
self.opaque = false;
}
return self;
}
- (void)setAnnotation:(id<MKAnnotation>)annotation {
[super setAnnotation:annotation];
if (annotation) [self adjustLabelWidth:annotation];
}
- (void)adjustLabelWidth:(id<MKAnnotation>)annotation {
NSString *title = [annotation title];
NSDictionary *attributes = #{NSFontAttributeName : self.label.font};
self.textSize = [title sizeWithAttributes:attributes];
CGFloat delta = self.textSize.height * (1.0 - sinf(M_PI_4)) * 0.55;
self.textBubbleSize = CGSizeMake(self.textSize.width + delta * 2, self.textSize.height + delta * 2);
self.label.frame = CGRectMake(0, self.pinHeight, self.textBubbleSize.width, self.textBubbleSize.height);
self.label.text = title;
self.frame = CGRectMake(0, 0, self.textBubbleSize.width, self.pinHeight + self.textBubbleSize.height);
self.centerOffset = CGPointMake(0, self.frame.size.height / 2.0 - self.pinHeight);
}
- (void)drawRect:(CGRect)rect {
CGFloat radius = self.pinRadius - self.lineWidth / 2.0;
CGPoint startPoint = CGPointMake(self.textBubbleSize.width / 2.0, self.pinHeight);
CGPoint center = CGPointMake(self.textBubbleSize.width / 2, self.pinRadius);
CGPoint nextPoint;
// pin
self.pinPath = [UIBezierPath bezierPath];
[self.pinPath moveToPoint:startPoint];
nextPoint = CGPointMake(self.textBubbleSize.width / 2 - radius, self.pinRadius);
[self.pinPath addCurveToPoint:nextPoint
controlPoint1:CGPointMake(startPoint.x, startPoint.y - (startPoint.y - nextPoint.y) / 2.0)
controlPoint2:CGPointMake(nextPoint.x, nextPoint.y + (startPoint.y - nextPoint.y) / 2.0)];
[self.pinPath addArcWithCenter:center radius:radius startAngle:M_PI endAngle:0 clockwise:TRUE];
nextPoint = startPoint;
startPoint = self.pinPath.currentPoint;
[self.pinPath addCurveToPoint:nextPoint
controlPoint1:CGPointMake(startPoint.x, startPoint.y - (startPoint.y - nextPoint.y) / 2.0)
controlPoint2:CGPointMake(nextPoint.x, nextPoint.y + (startPoint.y - nextPoint.y) / 2.0)];
[[UIColor blackColor] setStroke];
[[UIColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.8] setFill];
self.pinPath.lineWidth = self.lineWidth;
[self.pinPath fill];
[self.pinPath stroke];
[self.pinPath closePath];
// bubble around label
if ([self.annotation.title length] > 0) {
self.textBubblePath = [UIBezierPath bezierPath];
CGRect bubbleRect = CGRectInset(CGRectMake(0, self.pinHeight, self.textBubbleSize.width, self.textBubbleSize.height), self.lineWidth / 2, self.lineWidth / 2);
self.textBubblePath = [UIBezierPath bezierPathWithRoundedRect:bubbleRect
cornerRadius:bubbleRect.size.height / 2];
self.textBubblePath.lineWidth = self.lineWidth;
[self.textBubblePath fill];
[self.textBubblePath stroke];
} else {
self.textBubblePath = nil;
}
// center white dot
self.pinPath = [UIBezierPath bezierPathWithArcCenter:center radius:radius / 3.0 startAngle:0 endAngle:M_PI * 2.0 clockwise:TRUE];
self.pinPath.lineWidth = self.lineWidth;
[[UIColor whiteColor] setFill];
[self.pinPath fill];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
if ([self.pinPath containsPoint:point] || [self.textBubblePath containsPoint:point])
return self;
return nil;
}
#end
That yields something like:
Clearly, you can customize this to your heart's content, but it illustrates the basic idea: Write a MKAnnotationView subclass that overrides initWithAnnotation:reuseIdentifier: and implement your own drawRect.

touchesMoved drawing in CAShapeLayer slow/laggy

As was suggested to me in a prior StackOverflow question, I'm trying to improve my drawing method for letting my user draw lines/dots into a UIView. I'm now trying to draw using a CAShapeLayer instead of dispatch_async. This all works correctly, however, drawing into the CAShapeLayer continuously while touches are moving becomes slow and the path lags behind, whereas my old (inefficient I was told) code worked beautifully smooth and fast. You can see my old code commented out below.
Is there any way to improve the performance for what I want to do? Maybe I'm overthinking something.
I'd appreciate any help offered.
Code:
#property (nonatomic, assign) NSInteger center;
#property (nonatomic, strong) CAShapeLayer *drawLayer;
#property (nonatomic, strong) UIBezierPath *drawPath;
#property (nonatomic, strong) UIView *drawView;
#property (nonatomic, strong) UIImageView *drawingImageView;
CGPoint points[4];
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
self.center = 0;
points[0] = [touch locationInView:self.drawView];
if (!self.drawLayer)
{
CAShapeLayer *layer = [CAShapeLayer layer];
layer.lineWidth = 3.0;
layer.lineCap = kCALineCapRound;
layer.strokeColor = self.inkColor.CGColor;
layer.fillColor = [[UIColor clearColor] CGColor];
[self.drawView.layer addSublayer:layer];
self.drawView.layer.masksToBounds = YES;
self.drawLayer = layer;
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
self.center++;
points[self.center] = [touch locationInView:self.drawView];
if (self.center == 3)
{
UIBezierPath *path = [UIBezierPath bezierPath];
points[2] = CGPointMake((points[1].x + points[3].x)/2.0, (points[1].y + points[3].y)/2.0);
[path moveToPoint:points[0]];
[path addQuadCurveToPoint:points[2] controlPoint:points[1]];
points[0] = points[2];
points[1] = points[3];
self.center = 1;
[self drawWithPath:path];
}
}
- (void)drawWithPath:(UIBezierPath *)path
{
if (!self.drawPath)
{
self.drawPath = [UIBezierPath bezierPath];
}
[self.drawPath appendPath:path];
self.drawLayer.path = self.drawPath.CGPath;
[self.drawLayer setNeedsDisplay];
// Below code worked faster and didn't lag behind at all really
/*
dispatch_async(dispatch_get_main_queue(),
^{
UIGraphicsBeginImageContextWithOptions(self.drawingImageView.bounds.size, NO, 0.0);
[self.drawingImageView.image drawAtPoint:CGPointZero];
[self.inkColor setStroke];
[path stroke];
self.drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
});
*/
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if (self.center == 0)
{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:points[0]];
[path addLineToPoint:points[0]];
[self drawWithPath:path];
}
self.drawLayer = nil;
self.drawPath = nil;
}
This problem intrigued me as I've always found UIBezierPath/shapeLayer to be reletivly fast.
It's important to note that in your code above, you continues to add points to drawPath. As this increases, the appendPath method becomes a real resource burden. Similarly, there is no point in successively rendering the same points over and over.
As a side note, there is a visible performance difference when increasing lineWidth and adding lineCap (regardless of approach). For the sake of comparing Apples with Apples, in the test below, I've left both to default values.
I took your above code and changed it a little. The technique I've used is to add touchPoints to the BezierPath up to a per-determined number, before committing the current rendering to image. This is similar to your original approach, however, given that it's not happening with every touchEvent. it's far less CPU intensive. I tested both approaches on the slowest device I have (iPhone 4S) and noted that CPU utilization on your initial implementation was consistently around 75-80% whilst drawing. Whilst with the modified/CAShapeLayer approach, CPU utilization was consistently around 10-15% Memory usage also remained minimal with the second approach.
Below is the Code I used;
#interface MDtestView () // a simple UIView Subclass
#property (nonatomic, assign) NSInteger cPos;
#property (nonatomic, strong) CAShapeLayer *drawLayer;
#property (nonatomic, strong) UIBezierPath *drawPath;
#property (nonatomic, strong) NSMutableArray *bezierPoints;
#property (nonatomic, assign) NSInteger pointCount;
#property (nonatomic, strong) UIImageView *drawingImageView;
#end
#implementation MDtestView
CGPoint points[4];
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if (self) {
//
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
self.cPos = 0;
points[0] = [touch locationInView:self];
if (!self.drawLayer)
{
// this should be elsewhere but kept it here to follow your code
self.drawLayer = [CAShapeLayer layer];
self.drawLayer.backgroundColor = [UIColor clearColor].CGColor;
self.drawLayer.anchorPoint = CGPointZero;
self.drawLayer.frame = self.frame;
//self.drawLayer.lineWidth = 3.0;
// self.drawLayer.lineCap = kCALineCapRound;
self.drawLayer.strokeColor = [UIColor redColor].CGColor;
self.drawLayer.fillColor = [[UIColor clearColor] CGColor];
[self.layer insertSublayer:self.drawLayer above:self.layer ];
self.drawingImageView = [UIImageView new];
self.drawingImageView.frame = self.frame;
[self addSubview:self.drawingImageView];
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
if (!self.drawPath)
{
self.drawPath = [UIBezierPath bezierPath];
// self.drawPath.lineWidth = 3.0;
// self.drawPath.lineCapStyle = kCGLineCapRound;
}
// grab the current time for testing Path creation and appending
CFAbsoluteTime cTime = CFAbsoluteTimeGetCurrent();
self.cPos++;
//points[self.cPos] = [touch locationInView:self.drawView];
points[self.cPos] = [touch locationInView:self];
if (self.cPos == 3)
{
/* uncomment this block to test old method
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:points[0]];
points[2] = CGPointMake((points[1].x + points[3].x)/2.0, (points[1].y + points[3].y)/2.0);
[path addQuadCurveToPoint:points[2] controlPoint:points[1]];
points[0] = points[2];
points[1] = points[3];
self.cPos = 1;
dispatch_async(dispatch_get_main_queue(),
^{
UIGraphicsBeginImageContextWithOptions(self.drawingImageView.bounds.size, NO, 0.0);
[self.drawingImageView.image drawAtPoint:CGPointZero];
// path.lineWidth = 3.0;
// path.lineCapStyle = kCGLineCapRound;
[[UIColor redColor] setStroke];
[path stroke];
self.drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
NSLog(#"it took %.2fms to draw via dispatchAsync", 1000.0*(CFAbsoluteTimeGetCurrent() - cTime));
});
*/
// I've kept the original structure in place, whilst comparing apples for apples. we really don't need to create
// a new bezier path and append it. We can simply add the points to the global drawPath, and zero it at an
// appropriate point. This would also eliviate the need for appendPath
// /*
[self.drawPath moveToPoint:points[0]];
points[2] = CGPointMake((points[1].x + points[3].x)/2.0, (points[1].y + points[3].y)/2.0);
[self.drawPath addQuadCurveToPoint:points[2] controlPoint:points[1]];
points[0] = points[2];
points[1] = points[3];
self.cPos = 1;
self.drawLayer.path = self.drawPath.CGPath;
NSLog(#"it took %.2fms to render %i bezier points", 1000.0*(CFAbsoluteTimeGetCurrent() - cTime), self.pointCount);
// 1 point for MoveToPoint, and 2 points for addQuadCurve
self.pointCount += 3;
if (self.pointCount > 100) {
self.pointCount = 0;
[self commitCurrentRendering];
}
// */
}
}
- (void)commitCurrentRendering{
CFAbsoluteTime cTime = CFAbsoluteTimeGetCurrent();
#synchronized(self){
CGRect paintLayerBounds = self.drawLayer.frame;
UIGraphicsBeginImageContextWithOptions(paintLayerBounds.size, NO, [[UIScreen mainScreen]scale]);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(context, kCGBlendModeCopy);
[self.layer renderInContext:context];
CGContextSetBlendMode(context, kCGBlendModeNormal);
[self.drawLayer renderInContext:context];
UIImage *previousPaint = UIGraphicsGetImageFromCurrentImageContext();
self.layer.contents = (__bridge id)(previousPaint.CGImage);
UIGraphicsEndImageContext();
[self.drawPath removeAllPoints];
}
NSLog(#"it took %.2fms to save the context", 1000.0*(CFAbsoluteTimeGetCurrent() - cTime));
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if (self.cPos == 0)
{
/* //not needed
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:points[0]];
[path addLineToPoint:points[0]];
[self drawWithPath:path];
*/
}
if (self.cPos == 2) {
[self commitCurrentRendering];
}
// self.drawLayer = nil;
[self.drawPath removeAllPoints];
}
#end

Resources