iOS: DrawRect does not move my view - ios

I’m a new developer on iOS and I’m struggling with the DrawRect method: the first time it gets called, it actually draws what I want where I want, but all the next calls to DrawRect fail to move my view (although they do resize it).
I therefore made a minimalist test app to reproduce my problem but still could not isolate the cause.
This app just draws a blue rectangle in the top left corner, and each time you tap into it, it’s supposed to:
Switch width and height (this does work)
Move the rectangle by 10 points to the right (this does not work)
Of course I checked that DrawRect actually gets called and that its bounds did change to what I wanted, but still, my view does not want to move.
==============================================
Here’s my ViewController (RVTViewController.m)
==============================================
#implementation RVTViewController
(void)viewDidLoad`
{
[super viewDidLoad];
if (!self.myView)
{
CGRect viewBounds = CGRectMake(0,0,100,150);
self.myView = [[RVTView alloc] initWithFrame:viewBounds];
[self.view addSubview:self.myView];
UITapGestureRecognizer* gestRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tap:)];
[self.myView addGestureRecognizer:gestRec];
}
}
-(void) tap:(UITapGestureRecognizer*) gesture
{
CGRect newViewBounds = CGRectMake(self.myView.bounds.origin.x + 10,
self.myView.bounds.origin.y,
self.myView.bounds.size.height,
self.myView.bounds.size.width);
self.myView.bounds = newViewBounds;
[self.myView setNeedsLayout];
[self.myView setNeedsDisplay];
}
#end
=====================================
And here’s my custom view (RVTView.m)
=====================================
#implementation RVTView
- (void)drawRect:(CGRect)rect
{
CGRect newBounds = self.bounds;
UIBezierPath* newRect = [UIBezierPath bezierPathWithRect:newBounds];
[[UIColor blueColor] setFill];
UIRectFill(self.bounds);
[newRect stroke];
}
#end
Can someone please tell me what I am getting wrong ?

Using [self.myView setFrame:newViewBounds]; instead of self.myView.bounds = newViewBounds; fixed the problem.

Related

CAShapeLayer is sometimes visible but sometime is not

Inside UICollectionViewCell I add CAShapeLayer circle inside initWithCoder;
So here I didn't do anything unusual, its very simple. But when I launch the app, the circle is sometimes visible, but sometimes is not.
I logged the circle where I saw that the color, opacity, path and frame is set properly. But on the screen there's no circle. If I begin scrolling collection view, the circle appears right away. Also if I touch any other button or scroll on the screen. I even try to make a screenshot for you to see it, but at the moment I take a screenshot, the circle appears right away. So I can't show the problem to you.
I thing it's possible that this is a mistake made inside the iOS. Is there a way to force to redraw the circle.
here is my initializor
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
CAShapeLayer *circle = [CAShapeLayer layer];
circle.contentsScale = SCREEN_SCALE;
circle.rasterizationScale = 2.0 * SCREEN_SCALE;
circle.shouldRasterize = YES;
circle.opacity = 1.0;
circle.backgroundColor = [UIColor clearColor].CGColor;
circle.fillColor = [UIColor grayColor].CGColor;
CGRect frame = CGRectMake(0, 0, 50, 50);
circle.frame = frame;
circle.path = [UIBezierPath bezierPathWithOvalInRect:frame].CGPath;
[self.layer addSublayer:circle];
}
return self;
}
I think I didnt do anything wrong, there didnt do anything else with the circle elsewhere in the code.

How to add a gesture recognizer to a shape drawn by uibezierpath

I am drawing a circle in the drawRect function in a subclass of UIView
- (void)drawRect:(CGRect)rect
{
CGContextRef contextRef = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(contextRef, 2.0);
CGContextSetRGBFillColor(contextRef, 0, 0, 1.0, 1.0);
CGContextSetRGBStrokeColor(contextRef, 0, 0, 1.0, 1.0);
CGRect circlePoint = (CGRectMake(self.bounds.size.width/3, self.bounds.size.height/2, 200.0, 200.0));
CGContextFillEllipseInRect(contextRef, circlePoint);
}
I want to add a gesture recognizer to the circle to make it tappable
UITapGestureRecognizer *singleFingerTap =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(handleSingleTap:)];
[self.view addGestureRecognizer:singleFingerTap];
I thought of dragging a UIGestureRecognizer onto the view (in storyboard) in the location where the big circle will be, but the circle is much bigger than the UIGestureRecognizer widget.
How can I either combine the code or assign a UIGestureRecognizer to an area of the view that's exactly the same as the size and location of the circle?
The short answer is that you can't. Gesture recognizers are attached to views, not shapes or layers. You would have to create a custom view object for each shape. You could certainly do that.
What I suggest you do is to create a custom subclass of UIView that manages all your shapes. (I'll call it ShapesView) Have that custom ShapesView manage an array of custom shape objects. Attach a gesture recognizer to your ShapesView. In the code that responds to gestures, have it do custom hit testing to determine which shape was tapped, and move the shapes around.
UIBezierPath includes a containsPoint method that would allow you to do hit testing if you maintained a bezier path for each shape.
I'm not sure how to do it using drawRect the way you are, but I've done something similar using UIBezierPath. I subclassed UIView, and made this view the main view of my controller. This is the code in that view,
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
self.shape = [UIBezierPath bezierPathWithOvalInRect:(CGRectMake(self.bounds.size.width/3, self.bounds.size.height/3, 200.0, 200.0))];
}
return self;
}
-(void)drawRect:(CGRect)rect {
[[UIColor blueColor] setFill];
[self.shape fill];
}
shape is a property declared in the .h file. In the view controller .m file, I add the gesture recognizer, and check if the touch is inside the shape,
#interface ViewController ()
#property (strong,nonatomic) RDView *mainView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.mainView = (RDView *)self.view;
UITapGestureRecognizer *singleFingerTap =
[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
[self.view addGestureRecognizer:singleFingerTap];
}
-(void)handleSingleTap:(UITapGestureRecognizer *) tapper {
if ([self.mainView.shape containsPoint:[tapper locationInView:self.mainView]]) {
NSLog(#"tapped");
}
}

Lines drawn using drawRect method not getting scrolled

I am creating an app where I want to draw lines on scrollView. I am able to draw lines.
Here is my code
#interface GraphOnScrollView : UIScrollView
#property(strong,nonatomic)NSMutableArray *intensityArray;
#end
import "GraphOnScrollView.h"
#implementation GraphOnScrollView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.userInteractionEnabled=YES;
self.scrollEnabled=YES;
UIButton *DirectMsgBtn1 =[UIButton buttonWithType:UIButtonTypeCustom];
DirectMsgBtn1.titleLabel.font =[UIFont systemFontOfSize:12.0];
[DirectMsgBtn1 setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[DirectMsgBtn1 setTitle:#"direct message" forState:UIControlStateNormal];
[DirectMsgBtn1 setBackgroundColor:[UIColor clearColor]];
[DirectMsgBtn1 addTarget:self
action:#selector(DirectMessageViewPopUp:)
forControlEvents:UIControlEventTouchDown];
DirectMsgBtn1.frame = CGRectMake(0.0, 0, 100, 30);
[self addSubview:DirectMsgBtn1];
DirectMsgBtn1 = nil;
}
return self; }
// Only override drawRect: if you perform custom drawing. // An
empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
CGFloat y_Axix =20.0;
CGFloat lineWidth=1.0;
for (int i=0; i<[self.intensityArray count]; i++)
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(c, 2.0);
lineWidth+=1;
CGFloat red[4] = {1.0f, 0.5f, 0.0f, 1.0f};
CGContextSetStrokeColor(c, red);
CGContextBeginPath(c);
CGFloat width=[[self.intensityArray objectAtIndex:i] floatValue];
CGContextMoveToPoint(c, 5.0f, y_Axix);
CGContextAddLineToPoint(c, width, y_Axix);
CGContextStrokePath(c);
CGContextAddArc(c,width,y_Axix,1.0f,0,2*3.1415926535898,1);
CGContextDrawPath(c,kCGPathStroke);
y_Axix=y_Axix+50;
}
NSLog(#"intensity array %#", self.intensityArray);
self.contentSize = CGSizeMake(self.frame.size.width, 1000);
}
This is code I am using for adding scrollview on my view
GraphOnScrollView *GraphView =[[GraphOnScrollView
alloc]initWithFrame:CGRectMake(0.0, 150.0, 320.0, 280.0)];
GraphView.backgroundColor =[UIColor whiteColor];
GraphView.intensityArray =[NSMutableArray arrayWithObjects:#"60",#"100",#"40",#"10",#"20",#"40",#"40",#"100",
nil];
[self.view addSubview:GraphView];
Using this code the lines which i have drawn is not getting scrolled but the scrollview is scrolled. I don't know what is the problem.
Thanks
While drawing on any view it draws everything on the canvas of that view.
In the case of your, you are drawing on UIScrollView so all drawing performs on the canvas of the UIScrollView, so it is not scrollable.
To solve that problem you can create on separate UIView with the size you want, perform any drawing on it and add that view in scrollview. I think this will be the best solution.
You can also refer to this link for some more help.
Alternate Approach (i actually used uiview for drawing and made it look like scroll)
save the touch location in
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
}
record the touch point in touches moved.
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
}
now you can translate coordinate of your points as
newX=x+(touchstart.x-touchMoved.x);//do this for x of every point
newy=y+(touchstart.y-touchMoved.y);// do this for y of every point
put the above lines in a function and call them from touches moved you can do it in touches ended method as well if lag is ok with you.
Point is translate the point and calculate the degree of translation from user touch began and moved or swipe

UIScrollView zooming out of a view with a -ve origin

I have a UIScrollView. In this I have a UIView which has a frame with a negative origin - I need to limit the scroll view so that you can't scroll around the entire view..
I have implemented Zoom in this scrollview.
When Zooming the Scroll view will adjust the size of the Zoomable view according to the scale. BUT IT DOES NOT ADJUST THE ORIGIN.
So if I have a view with a frame of {0, -500}, {1000, 1000}
The I zoom out to a scale of 0.5, this will give me a new frame of {0, -500}, {500, 500}
Clearly this is not good, the entire view is zoomed out of the scrollview. I want the frame to be {0, -250}, {500, 500}
I can fix things a bit in the scrollViewDidZoom method by adjusting the origin correctly.. This does work, but the zoom is not smooth.. Changing the origin here causes it to jump.
I notice in the documentation for UIView it says (regarding the frame property):
Warning: If the transform property is not the identity transform, the
value of this property is undefined and therefore should be ignored.
Not quite sure why that is.
Am I approaching this problem wrong? What is the best way to fix it?
Thanks
Below is some source code from the test app I am using:
In the ViewController..
- (void)viewDidLoad
{
[super viewDidLoad];
self.bigView = [[BigView alloc] initWithFrame: CGRectMake(0, -400, 1000, 1000)];
[self.bigScroll addSubview: bigView];
self.bigScroll.delegate = self;
self.bigScroll.minimumZoomScale = 0.2;
self.bigScroll.maximumZoomScale = 5;
self.bigScroll.contentSize = bigView.bounds.size;
}
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return bigView;
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
// bigView.frame = CGRectMake(0, -400 * scrollView.zoomScale,
// bigView.frame.size.width, bigView.frame.size.height);
bigView.center = CGPointMake(500 * scrollView.zoomScale, 100 * scrollView.zoomScale);
}
And then in the View...
- (void)drawRect:(CGRect)rect
{
// Drawing code
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);
CGContextFillRect(ctx, CGRectMake(100, 500, 10, 10));
for (int i = 0; i < 1000; i += 100) {
CGContextStrokeRect(ctx, CGRectMake(0, i, 1000, 3));
}
}
Note that here the jumpiness is more apparent at larger zoom scales. In my real app where there is much more drawing and processing going on the jump is more apparent at all times.
You don't have to use the frame property - and should not, given Apple's very firm warning. In such cases you can usually use bounds and center to achieve your result.
In your case you can ignore all of the subview's properties. Assuming that your subview is the viewForZoomingInScrollView you can use the scrollView's contentOffset and zoomScale properties
- (void) setMinOffsets:(UIScrollView*)scrollView
{
CGFloat minOffsetX = MIN_OFFSET_X*scrollView.zoomScale;
CGFloat minOffsetY = MIN_OFFSET_Y*scrollView.zoomScale;
if ( scrollView.contentOffset.x < minOffsetX
|| scrollView.contentOffset.y < minOffsetY ) {
CGFloat offsetX = (scrollView.contentOffset.x > minOffsetX)?
scrollView.contentOffset.x : minOffsetX;
CGFloat offsetY = (scrollView.contentOffset.y > minOffsetY)?
scrollView.contentOffset.y : minOffsetY;
scrollView.contentOffset = CGPointMake(offsetX, offsetY);
}
}
Call it from both scrollViewDidScroll and scrollViewDidZoom in your scrollView delegate. This should work smoothly, but if you have doubts you can also implement it by subclassing the scrollView and invoking it with layoutSubviews. In their PhotoScroller example, Apple centers a scrollView's content by overriding layoutSubviews - although maddeningly they ignore their own warnings and adjust the subview's frame property to achieve this.
update
The above method eliminates the 'bounce' as the scrollView hits it's limits. If you want to retain the bounce, you can directly alter the view's center property instead:
- (void) setViewCenter:(UIScrollView*)scrollView
{
UIView* view = [scrollView subviews][0];
CGFloat centerX = view.bounds.size.width/2-MIN_OFFSET_X;
CGFloat centerY = view.bounds.size.height/2-MIN_OFFSET_Y;
centerX *=scrollView.zoomScale;
centerY *=scrollView.zoomScale;
view.center = CGPointMake(centerX, centerY);
}
update 2
From your updated question (with code), I can see that neither of these solutions fix you problem. What seems to be happening is that the greater you make your offset, the jerkier the zoom movement becomes. With an offset of 100points the action is still quite smooth, but with an offset of 500points, it is unacceptably rough. This is partly related to your drawRect routine, and partly related to (too much) recalculation going on in the scrollView to display the right content. So I have another solution…
In your viewController, set your customView's bounds/frame origin to the normal (0,0). We will offset the content using layers instead. You will need to add the QuartzCore framework to your project, and #import it into your custom view.
In the custom view initialise two CAShapeLayers - one for the box, the other for the lines. If they share the same fill and stroke you would only need one CAShapeLayer (for this example I changed your fill and stroke colors). Each CAShapeLayer comes with it's own CGContext, which you can initialise once per layer with colors, linewidths etc. Then to make a CAShapelayer do it's drawing all you have to do is set it's path property with a CGPath.
#import "CustomView.h"
#import <QuartzCore/QuartzCore.h>
#interface CustomView()
#property (nonatomic, strong) CAShapeLayer* shapeLayer1;
#property (nonatomic, strong) CAShapeLayer* shapeLayer2;
#end
#implementation CustomView
#define MIN_OFFSET_X 100
#define MIN_OFFSET_Y 500
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self initialiseLayers];
}
return self;
}
- (void) initialiseLayers
{
CGRect layerBounds = CGRectMake( MIN_OFFSET_X,MIN_OFFSET_Y
, self.bounds.size.width + MIN_OFFSET_X
, self.bounds.size.height+ MIN_OFFSET_Y);
self.shapeLayer1 = [[CAShapeLayer alloc] init];
[self.shapeLayer1 setFillColor:[UIColor clearColor].CGColor];
[self.shapeLayer1 setStrokeColor:[UIColor yellowColor].CGColor];
[self.shapeLayer1 setLineWidth:1.0f];
[self.shapeLayer1 setOpacity:1.0f];
self.shapeLayer1.anchorPoint = CGPointMake(0, 0);
self.shapeLayer1.bounds = layerBounds;
[self.layer addSublayer:self.shapeLayer1];
Setting the bounds is the critical bit. Unlike views, which clip their subviews, CALayers will draw beyond the bounds of their superlayer. You are going to start drawing MIN_OFFSET_Y points above the top of your View and MIN_OFFSET_X to the left. This allows you to draw content beyond your scrollView's content view without the scrollView having to do any extra work.
Unlike views, a superlayer does not automatically clip the contents of sublayers that lie outside its bounds rectangle. Instead, the superlayer allows its sublayers to be displayed in their entirety by default.
(Apple Docs, Building a Layer Hierarchy)
self.shapeLayer2 = [[CAShapeLayer alloc] init];
[self.shapeLayer2 setFillColor:[UIColor blueColor].CGColor];
[self.shapeLayer2 setStrokeColor:[UIColor clearColor].CGColor];
[self.shapeLayer2 setLineWidth:0.0f];
[self.shapeLayer2 setOpacity:1.0f];
self.shapeLayer2.anchorPoint = CGPointMake(0, 0);
self.shapeLayer2.bounds = layerBounds;
[self.layer addSublayer:self.shapeLayer2];
[self drawIntoLayer1];
[self drawIntoLayer2];
}
Set a bezier path for each shape layer, then pass it in:
- (void) drawIntoLayer1 {
UIBezierPath* path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointMake(0,0)];
for (int i = 0; i < self.bounds.size.height+MIN_OFFSET_Y; i += 100) {
[path moveToPoint:
CGPointMake(0,i)];
[path addLineToPoint:
CGPointMake(self.bounds.size.width+MIN_OFFSET_X, i)];
[path addLineToPoint:
CGPointMake(self.bounds.size.width+MIN_OFFSET_X, i+3)];
[path addLineToPoint:
CGPointMake(0, i+3)];
[path closePath];
}
[self.shapeLayer1 setPath:path.CGPath];
}
- (void) drawIntoLayer2 {
UIBezierPath* path = [UIBezierPath bezierPathWithRect:
CGRectMake(100+MIN_OFFSET_X, MIN_OFFSET_Y, 10, 10)];
[self.shapeLayer2 setPath:path.CGPath];
}
This obviates the need for drawRect - you only need to redraw your layers if you change the path property. Even if you do change the path property as often as you would call drawRect, the drawing should now be significantly more efficient. And as path is an animatable property, you also get animation thrown in for free if you need it.
In your case we only need to set the path once, so all of the work is done once, on initialisation.
Now you can remove any centering code from your scrollView delegate methods, it isn't needed any more.

Flipping a circular UIView

I'm working on a project that has circular views that flip over.
So far, I've tried a couple of the solutions provided here on SOflow:
simply rounding the corners of the view's layer.
creating a custom CircleView that sets backgroundColor to [UIColor clearColor] and manually drawing the circle on in drawRect.
In each case, when the view flips to the new view, it's a rectangular portion of the screen that flips, not just the circle. I'd like the flip to just be the circle, but I'm not sure where to go next.
Here's the method I'm using to flip the view currently:
-(void)changeSpotToView:(SpotView *)toView {
[UIView transitionFromView:self.activeView
toView:toView
duration:1.0
options:UIViewAnimationOptionTransitionFlipFromTop
completion:^(BOOL finished) {
self.activeView = toView;
}];
}
The container the SpotViews are in is also a SpotView, so it should be just a circle as well.
Any guidance would be appreciated!
The problem is transitionFromView:toView:… animates the superview of the fromView.
I reproduced the problem:
I was able to fix it in my test case simply by embedding my fromView and toView in a container view with a clear background. Here's my working version:
My nib looks like this:
I have bounds rectangles enabled (Editor > Canvas > Show Bounds Rectangles), so you can see the outline of the container view, which is bigger than its subviews.
I have a red view and a blue view as subviews of the container view. The blue view is behind the red view and has the same frame. I apply circular masks to them in code.
Here's my code:
- (void)viewDidLoad {
[super viewDidLoad];
self.blueView.hidden = YES;
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self addCircleMaskToView:self.blueView];
[self addCircleMaskToView:self.redView];
}
- (void)addCircleMaskToView:(UIView *)view {
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = view.bounds;
maskLayer.path = [UIBezierPath bezierPathWithOvalInRect:view.bounds].CGPath;
maskLayer.fillColor = [UIColor whiteColor].CGColor;
view.layer.mask = maskLayer;
}
- (IBAction)buttonWasTapped:(id)sender {
[UIView transitionFromView:self.redView toView:self.blueView
duration:1.0 options:UIViewAnimationOptionTransitionFlipFromTop
| UIViewAnimationOptionShowHideTransitionViews
completion:nil];
}

Resources