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");
}
}
Related
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.
I am trying to implement two subclasses at one time.
I am using the UIViewController <CLLocationManagerDelegate> subclass in my viewController to be able to work with the gps. While using UIView subclass to draw.
This is a summary of the code I have:
MapViewController.h:
#interface MapViewController: UIViewController <CLLocationManagerDelegate>{
DrawCircle *circleView;
}
#property (nonatomic, retain) DrawCircle *circleView;
#end
DrawCircle.h:
#interface DrawCircle : UIView
-(void)addPoint:(CGPoint)point;
#end
What I was trying to do here was to make DrawCircle a property of MapViewController. However I am still having no luck drawing anything to the screen.
Here is my DrawCircle.m code if it helps at all:
-(id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if(self) {
_points = [[NSMutableArray alloc] init];
}
return self;
}
-(void)addPoint:(CGPoint)point {
//Wrap the point in an NSValue to add it to the array
[_points addObject:[NSValue valueWithCGPoint:point]];
//This will tell our view to redraw itself, calling drawRect
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
for(NSValue *pointValue in _points) {
CGPoint point = [pointValue CGPointValue];
CGContextAddEllipseInRect(ctx, CGRectMake(point.x - 10, point.y - 10, 20, 20));
}
CGContextSetFillColor(ctx, CGColorGetComponents([[UIColor redColor] CGColor]));
CGContextFillPath(ctx);
}
Also as a last bit of information I can give to you all. Here is a snapshot of my map view controller scene.
I have a feeling that the shape is being drawn, but being covered up somehow.
EDIT
After further investigation, it appears that the shapes being drawn are being covered by my UIImage. I was just messing around and removed the UIImage, and my shapes were there. Now the question is, how do i "move to back" my UIImage, so that my shapes are up front?
I'm trying to draw a rectangle which has four circular handles. Here's what it would look like:
o----o
| |
| |
o----o
The circular handles are "hot". In other words, when the user touches it, the handle can be moved around while the rest of the points are anchored. I wanted to know if anyone had an approach for coding this functionality. I'm looking at UIBezierPath to draw the rectangle with circles, but I'm having a hard time thinking about how to allow the user to tap only the circles. I was thinking it may need to be five different UIBezierPath objects, but eventually the UI will consist of multiples of these objects.
Any suggestions would be greatly appreciated. Thanks.
I wouldn't draw it as a single shape with complicated UIBezierPaths at all. I'd think about it as 6 different pieces. A Container, a rectangle, and 4 circles.
I would have a simple container UIView that has a rectangle view and four circular UIViews at its corners. Then put a UIPanGestureRecognizer on each circle. In the gesture handler, move the center of the circle and adjust the underlying rectangle rect by the same amount. This will avoid any complicated paths or math and make it simple add and subtract amounts on the rectangle itself.
Update: Code!
I created a self contained UIView subclass that handles everything. You can create one like so:
HandlesView *view = [[HandlesView alloc] initWithFrame:self.view.bounds];
[view setAutoresizingMask:UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth];
[view setBackgroundColor:[UIColor redColor]];
[self.view addSubview:view];
// A custom property that contains the selected area of the rectangle. Its updated while resizing.
[view setSelectedFrame:CGRectMake(128.0, 128.0, 200.0, 200.0)];
The frame of the view itself is the total draggable area. The selected frame is the inner visible rectangle.
//
// HandlesView.h
// handles
//
// Created by Ryan Poolos on 2/12/13.
// Copyright (c) 2013 Ryan Poolos. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#interface HandlesView : UIView
#property (nonatomic, readwrite) CGRect selectedFrame;
#end
And here is the implementation.
//
// HandlesView.m
// handles
//
// Created by Ryan Poolos on 2/12/13.
// Copyright (c) 2013 Ryan Poolos. All rights reserved.
//
#import "HandlesView.h"
#interface HandlesView ()
{
UIView *rectangle;
NSArray *handles;
NSMutableArray *touchedHandles;
UIView *circleTL;
UIView *circleTR;
UIView *circleBL;
UIView *circleBR;
}
#end
#implementation HandlesView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
rectangle = [[UIView alloc] initWithFrame:CGRectInset(self.bounds, 22.0, 22.0)];
[self addSubview:rectangle];
// Create the handles and position.
circleTL = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 44.0, 44.0)];
[circleTL setCenter:CGPointMake(CGRectGetMinX(rectangle.frame), CGRectGetMinY(rectangle.frame))];
circleTR = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 44.0, 44.0)];
[circleTR setCenter:CGPointMake(CGRectGetMaxX(rectangle.frame), CGRectGetMinY(rectangle.frame))];
circleBL = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 44.0, 44.0)];
[circleBL setCenter:CGPointMake(CGRectGetMinX(rectangle.frame), CGRectGetMaxY(rectangle.frame))];
circleBR = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 44.0, 44.0)];
[circleBR setCenter:CGPointMake(CGRectGetMaxX(rectangle.frame), CGRectGetMaxY(rectangle.frame))];
handles = #[ circleTL, circleTR, circleBL, circleBR ];
for (UIView *handle in handles) {
// Round the corners into a circle.
[handle.layer setCornerRadius:(handle.frame.size.width / 2.0)];
[self setClipsToBounds:YES];
// Add a drag gesture to the handle.
[handle addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)]];
// Add the handle to the screen.
[self addSubview:handle];
}
}
return self;
}
- (void)setSelectedFrame:(CGRect)selectedFrame
{
[rectangle setFrame:selectedFrame];
[circleTL setCenter:CGPointMake(CGRectGetMinX(rectangle.frame), CGRectGetMinY(rectangle.frame))];
[circleTR setCenter:CGPointMake(CGRectGetMaxX(rectangle.frame), CGRectGetMinY(rectangle.frame))];
[circleBL setCenter:CGPointMake(CGRectGetMinX(rectangle.frame), CGRectGetMaxY(rectangle.frame))];
[circleBR setCenter:CGPointMake(CGRectGetMaxX(rectangle.frame), CGRectGetMaxY(rectangle.frame))];
}
- (CGRect)selectedFrame
{
return rectangle.frame;
}
// Forward the background color.
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
// Set the container to clear.
[super setBackgroundColor:[UIColor clearColor]];
// Set our rectangle's color.
[rectangle setBackgroundColor:[backgroundColor colorWithAlphaComponent:0.5]];
for (UIView *handle in handles) {
[handle setBackgroundColor:backgroundColor];
}
}
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
// The handle we're moving.
UIView *touchedHandle = gesture.view;
// Keep track of touched Handles.
if (!touchedHandles) {
touchedHandles = [NSMutableArray array];
}
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
[touchedHandles addObject:touchedHandle];
break;
case UIGestureRecognizerStateChanged:
{
CGPoint tranlation = [gesture translationInView:self];
// Calculate this handle's new center
CGPoint newCenter = CGPointMake(touchedHandle.center.x + tranlation.x, touchedHandle.center.y + tranlation.y);
// Move corresponding circles
for (UIView *handle in handles) {
if (handle != touchedHandle && ![touchedHandles containsObject:handle]) {
// Match the handles horizontal movement
if (handle.center.x == touchedHandle.center.x) {
handle.center = CGPointMake(newCenter.x, handle.center.y);
}
// Match the handles vertical movement
if (handle.center.y == touchedHandle.center.y) {
handle.center = CGPointMake(handle.center.x, newCenter.y);
}
}
}
// Move this circle
[touchedHandle setCenter:newCenter];
// Adjust the Rectangle
// The origin and just be based on the Top Left handle.
float x = circleTL.center.x;
float y = circleTL.center.y;
// Get the width and height based on the difference between handles.
float width = abs(circleTR.center.x - circleTL.center.x);
float height = abs(circleBL.center.y - circleTL.center.y);
[rectangle setFrame:CGRectMake(x, y, width, height)];
[gesture setTranslation:CGPointZero inView:self];
}
break;
case UIGestureRecognizerStateEnded:
[touchedHandles removeObject:touchedHandle];
break;
default:
break;
}
}
#end
This is only a proof of concept. There are a lot of missing caveats like being able to drag outside the box, multitouch complications, negative sizes. All these problems can be handled very differently and are the secret sauce that makes something like this go from a nice idea to a beautiful custom interface. I'll leave that part up to you. :)
You will want to store the circle bezier paths in your class for when you implement gesture recognizers.
There is an Apple document describing how to implement a UIView or UIControl that accepts touch events with pictures and sample code.
http://developer.apple.com/library/ios/#documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/multitouch_background/multitouch_background.html#//apple_ref/doc/uid/TP40009541-CH5-SW9
I have a UIViewController Class which holds 2 custom UIView classes which are:
ItemView
DrawingView
in UIViewController ViewDidLoad method I have the following code:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
item = [[ItemView alloc] initWithFrame:CGRectMake(self.view.frame.size.width/2, self.view.frame.size.height/2, 230, 230)];
item.opaque = NO;
[self.view addSubview:item];
drawing = [[DrawingView alloc] initWithFrame:self.view.frame];
[self.view insertSubview:drawing aboveSubview:item];
[item release];
[drawing release];
}
The DrawingView class gets UITouches and accordingly draw on the screen.
My question is:
I can draw all over the screen except for on top of ItemView class object. DrawingView class cannot draw on to ItemView.
Let me explain it in another word:
The DrawView class works on the screen except for the area of itemView subview. On top of itemView, DrawView cannot make any draws but other than itemView area, it makes drawing.
What can I do?
What can be the problem?
How can I make drawing on all screen including the itemView area added by addSubview on ViewController class?
EDIT: I am adding a screenshot in order to explain the problem better
EDIT 2: I found that the problem is related to opacity. In the ItemView class, I changed the added UIImageView object alpha value to 0.5f.
The result is semi-transparent view and now my finger drawing is visible. However, this is not what I want. I want to draw on top of the view. I do not want to play with the alpha value.
Finally, I found the answer myself. It is not exactly what I wanted but my solution is based on CALayer.
Instead of using a custom UIView class like ItemView I added the following class method in ItemView Class:
+(CALayer *)imgLayer
{
UIImage *img = [UIImage imageNamed:#"face.png"];
CALayer *layer = [CALayer layer];
CGFloat nativeWidth = CGImageGetWidth(img.CGImage);
CGFloat nativeHeight = CGImageGetHeight(img.CGImage);
CGRect startFrame = CGRectMake(0, 0, nativeWidth, nativeHeight);
layer.contents = (id)img.CGImage;
layer.frame = startFrame;
return layer;
}
In My UIVIewController Class I called my static function as following:
CAGradientLayer *bgLayer = [GradientView greyGradient];
bgLayer.frame = self.view.bounds;
[self.view.layer insertSublayer:bgLayer atIndex:0];
CALayer *imgLayer =[ItemView imgLayer];
imgLayer.frame = self.view.bounds;
[self.view.layer insertSublayer:imgLayer above:bgLayer];
And it works! now I can make drawings on my image.
I am writing a program where the user will drag a line from one UILabel to another. I created a UIView subclass called DragView, and overrode the drawRect method. I made the UIView a subview of the RootController view. The DragView is definitely visible, the drawRect method is definitely being called, but no line is visible.
These are (what I think are) the relevant pieces of code.
//DragView.m
- (void)drawRect:(CGRect)rect
{
if (context == nil)
{
context = UIGraphicsGetCurrentContext();
}
if (_drawLineFlag)
{
CGContextSetLineWidth(context,2.0);
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
CGFloat components[] = {0.0,0.0,1.0,1.0};
CGColorRef color = CGColorCreate(colorspace, components);
CGContextSetStrokeColorWithColor(context,color);
CGContextMoveToPoint(context,_startX, _startY);
CGContextAddLineToPoint(context,_currentX,_currentY);
CGContextStrokePath(context);
}
}
DrawProgramAppDelegate
- (void) initializeUI
{
.....
dragView = [[DragView alloc] initWithFrame:CGRectMake(0.0f,0.0f,1024.0f,768.0f)] ;
[view addSubview: dragView];
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[dragView addGestureRecognizer:panGestureRecognizer];
.....
}
The event handler is:
- (void) handlePan:(UIPanGestureRecognizer *) recognizer
{
CGPoint point = [recognizer translationInView:dragView];
[dragView setCurrentX:point.x];
[dragView setCurrentY:point.y];
[dragView setDrawLineFlag:YES];
[view bringSubviewToFront:drawView];
[dragView drawRect:CGRectMake (0.0f,0.0f,768.0f, 1024.0f)];
}
Many thanks for your help.
Jon
It seems to me you are trying to draw a path (you should take a look at this post). Here it seems you just draw a line from the last point to the current point
CGContextMoveToPoint(context,_startX, _startY);
CGContextAddLineToPoint(context,_currentX,_currentY);
which given the frequency of the call would only draw a tiny line.
You should not call drawRect:
[dragView drawRect:CGRectMake (0.0f,0.0f,768.0f, 1024.0f)];
You should tell the system the view needs displaying
[dragView setNeedsDisplay];
Also you should check the gesture recognizer state, defined as
typedef enum {
UIGestureRecognizerStatePossible,
UIGestureRecognizerStateBegan,
UIGestureRecognizerStateChanged,
UIGestureRecognizerStateEnded,
UIGestureRecognizerStateCancelled,
UIGestureRecognizerStateFailed,
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
} UIGestureRecognizerState;
And possibly only draw if the state is UIGestureRecognizerStateChanged or UIGestureRecognizerStateEnded