Draggable image that only moves if you first touch it - ios

I am making a game in which you must drag an image across the view. I am currently using this code to do so:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *mytouch = [[event allTouches] anyObject];
circle.center = [mytouch locationInView:self.view];
[self collision];
}
The problem with the code above is that if the image is on the right side of the screen and I press down on the left, the image will move to the location I just touched on the left. I want it to be draggable, but only move if you first press down on the image, then drag it around. So you would first have to press down on the image on the right side of the screen and (keeping my finger on the screen) drag to the left (or wherever I drag my finger).

The most concise way to do this I've found is to subclass UIImageView, then all you have to do is this:
#import "MyImageView.h"
#implementation MyImageView
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.superview];
self.center = location;
}
#end
An example ViewController for this is as follows:
#import "ViewController.h"
#import "MyImageView.h"
#import <QuartzCore/QuartzCore.h>
#define NUM_IMAGES 50
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
const float radius = 15.0f;
const float diameter = radius * 2;
for (int i = 0; i < NUM_IMAGES; ++i)
{
float x = radius + drand48() * (self.view.frame.size.width - diameter - radius);
float y = radius + drand48() * (self.view.frame.size.height - diameter - radius);
MyImageView *imageView = [[MyImageView alloc] initWithFrame:CGRectMake(x, y, diameter, diameter)];
imageView.backgroundColor = [UIColor colorWithRed:drand48()
green:drand48()
blue:drand48()
alpha:1.0f];
imageView.userInteractionEnabled = YES;
imageView.layer.cornerRadius = radius;
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0.f, 0.f, diameter, diameter)];
label.text = [NSString stringWithFormat:#"%i", i + 1];
label.textAlignment = NSTextAlignmentCenter;
[imageView addSubview:label];
[self.view addSubview:imageView];
}
}
#end

Related

Merge background color and placeholder image in one image

I am building a scratch card experience where I have a background color and a transparent png on the top.
Beneath this, I have the actual image with the content of the scratch card.
I want to combine the background color and the transparent image as one uiimage so that when i scratch this, I am able to see the below content.
I have tried putting a background color to the actual image but when I scratch it, I cannot see the content. Instead the background color starts clearing the transparent image.
I have written the following code for clearing the area of the image after i touch it:
- (UIImage *)addTouches:(NSSet *)touches {
CGSize size = CGSizeMake(self.image.size.width * self.image.scale, self.image.size.height * self.image.scale);
CGContextRef ctx = _imageContext;
CGContextSetFillColorWithColor(ctx,[UIColor clearColor].CGColor);
CGContextSetStrokeColorWithColor(ctx,[UIColor colorWithRed:0 green:0 blue:0 alpha:0].CGColor);
int tempFilled = _tilesFilled;
// process touches
for (UITouch *touch in touches) {
CGContextBeginPath(ctx);
CGPoint touchPoint = [touch locationInView:self];
touchPoint = fromUItoQuartz(touchPoint, self.bounds.size);
touchPoint = scalePoint(touchPoint, self.bounds.size, size);
if(UITouchPhaseBegan == touch.phase){
[self.touchPoints removeAllObjects];
[self.touchPoints addObject:[NSValue valueWithCGPoint:touchPoint]];
[self.touchPoints addObject:[NSValue valueWithCGPoint:touchPoint]];
// on begin, we just draw ellipse
CGRect rect = CGRectMake(touchPoint.x - _radius, touchPoint.y - _radius, _radius*2, _radius*2);
CGContextAddEllipseInRect(ctx, rect);
CGContextFillPath(ctx);
static const FillTileWithPointFunc fillTileFunc = (FillTileWithPointFunc) [self methodForSelector:#selector(fillTileWithPoint:)];
(*fillTileFunc)(self,#selector(fillTileWithPoint:),rect.origin);
} else if (UITouchPhaseMoved == touch.phase) {
[self.touchPoints addObject:[NSValue valueWithCGPoint:touchPoint]];
// then touch moved, we draw superior-width line
CGContextSetStrokeColor(ctx, CGColorGetComponents([UIColor clearColor].CGColor));
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineWidth(ctx, 2 * _radius);
// CGContextMoveToPoint(ctx, prevPoint.x, prevPoint.y);
// CGContextAddLineToPoint(ctx, rect.origin.x, rect.origin.y);
while(self.touchPoints.count > 3){
CGPoint bezier[4];
bezier[0] = ((NSValue*)self.touchPoints[1]).CGPointValue;
bezier[3] = ((NSValue*)self.touchPoints[2]).CGPointValue;
CGFloat k = 0.3;
CGFloat len = sqrt(pow(bezier[3].x - bezier[0].x, 2) + pow(bezier[3].y - bezier[0].y, 2));
bezier[1] = ((NSValue*)self.touchPoints[0]).CGPointValue;
bezier[1] = [self normalizeVector:CGPointMake(bezier[0].x - bezier[1].x - (bezier[0].x - bezier[3].x), bezier[0].y - bezier[1].y - (bezier[0].y - bezier[3].y) )];
bezier[1].x *= len * k;
bezier[1].y *= len * k;
bezier[1].x += bezier[0].x;
bezier[1].y += bezier[0].y;
bezier[2] = ((NSValue*)self.touchPoints[3]).CGPointValue;
bezier[2] = [self normalizeVector:CGPointMake( (bezier[3].x - bezier[2].x) - (bezier[3].x - bezier[0].x), (bezier[3].y - bezier[2].y) - (bezier[3].y - bezier[0].y) )];
bezier[2].x *= len * k;
bezier[2].y *= len * k;
bezier[2].x += bezier[3].x;
bezier[2].y += bezier[3].y;
CGContextMoveToPoint(ctx, bezier[0].x, bezier[0].y);
CGContextAddCurveToPoint(ctx, bezier[1].x, bezier[1].y, bezier[2].x, bezier[2].y, bezier[3].x, bezier[3].y);
[self.touchPoints removeObjectAtIndex:0];
}
CGContextStrokePath(ctx);
CGPoint prevPoint = [touch previousLocationInView:self];
prevPoint = fromUItoQuartz(prevPoint, self.bounds.size);
prevPoint = scalePoint(prevPoint, self.bounds.size, size);
static const FillTileWithTwoPointsFunc fillTileFunc = (FillTileWithTwoPointsFunc) [self methodForSelector:#selector(fillTileWithTwoPoints:end:)];
(*fillTileFunc)(self,#selector(fillTileWithTwoPoints:end:),touchPoint, prevPoint);
}
}
// was _tilesFilled changed?
if(tempFilled != _tilesFilled) {
[_delegate mdScratchImageView:self didChangeMaskingProgress:self.maskingProgress];
}
CGImageRef cgImage = CGBitmapContextCreateImage(ctx);
UIImage *image = [UIImage imageWithCGImage:cgImage];
CGImageRelease(cgImage);
return image;
}
/*
* filling tile with one ellipse
*/
-(void)fillTileWithPoint:(CGPoint) point{
size_t x,y;
point.x = MAX( MIN(point.x, self.image.size.width - 1) , 0);
point.y = MAX( MIN(point.y, self.image.size.height - 1), 0);
x = point.x * self.maskedMatrix.max.x / self.image.size.width;
y = point.y * self.maskedMatrix.max.y / self.image.size.height;
char value = [self.maskedMatrix valueForCoordinates:x y:y];
if (!value){
[self.maskedMatrix setValue:1 forCoordinates:x y:y];
_tilesFilled++;
}
}
/*
* filling tile with line
*/
-(void)fillTileWithTwoPoints:(CGPoint)begin end:(CGPoint)end{
CGFloat incrementerForx,incrementerFory;
static const FillTileWithPointFunc fillTileFunc = (FillTileWithPointFunc) [self methodForSelector:#selector(fillTileWithPoint:)];
/* incrementers - about size of a tile */
incrementerForx = (begin.x < end.x ? 1 : -1) * self.image.size.width / _tilesX;
incrementerFory = (begin.y < end.y ? 1 : -1) * self.image.size.height / _tilesY;
// iterate on points between begin and end
CGPoint i = begin;
while(i.x <= MAX(begin.x, end.x) && i.y <= MAX(begin.y, end.y) && i.x >= MIN(begin.x, end.x) && i.y >= MIN(begin.y, end.y)){
(*fillTileFunc)(self,#selector(fillTileWithPoint:),i);
i.x += incrementerForx;
i.y += incrementerFory;
}
(*fillTileFunc)(self,#selector(fillTileWithPoint:),end);
}
What you probably want to do is use a Layer Mask.
When masking a layer, from Apple's docs:
The layer’s alpha channel determines how much of the layer’s content and background shows through. Fully or partially opaque pixels allow the underlying content to show through, but fully transparent pixels block that content.
So, you'd want to use a path to mask your dark circle.
However, to get the "scratch off" effect, you would need to draw the path with a Clear stroke... which you cannot accomplish with a CAShapeLayer.
So, we'll use a custom CALayer subclass.
MyShapeLayer.h
//
// MyShapeLayer.h
//
#import <QuartzCore/QuartzCore.h>
#interface MyShapeLayer : CALayer
#property(nonatomic) CGPathRef path;
#end
MyShapeLayer.m
//
// MyShapeLayer.m
//
#import <UIKit/UIKit.h>
#import "MyShapeLayer.h"
#implementation MyShapeLayer
- (void)drawInContext:(CGContextRef)inContext {
// fill entire layer with solid color
CGContextSetGrayFillColor(inContext, 0.0, 1.0);
CGContextFillRect(inContext, self.bounds);
// we want to "clear" the stroke
CGContextSetStrokeColorWithColor(inContext, [UIColor clearColor].CGColor);
// any color will work, as the mask uses the alpha value
CGContextSetFillColorWithColor(inContext, [UIColor whiteColor].CGColor);
// adjust drawing-line-width as desired
CGContextSetLineWidth(inContext, 60.0);
CGContextSetLineCap(inContext, kCGLineCapRound);
CGContextSetLineJoin(inContext, kCGLineJoinRound);
CGContextAddPath(inContext, self.path);
CGContextSetBlendMode(inContext, kCGBlendModeSourceIn);
CGContextDrawPath(inContext, kCGPathFillStroke);
}
#end
Now we can create a UIView subclass to draw a filled-circle path on a CAShapeLayer and mask it with our MyShapeLayer.
ScratchOffView.h
//
// ScratchOffView.h
//
#import <UIKit/UIKit.h>
#interface ScratchOffView : UIView
#property (assign, readwrite) CGFloat expandedBounds;
#end
ScratchOffView.m
//
// ScratchOffView.m
//
#import "ScratchOffView.h"
#import "MyShapeLayer.h"
#interface ScratchOffView()
#property (strong, nonatomic) UIBezierPath *maskPath;
#property (strong, nonatomic) MyShapeLayer *maskLayer;
#property (strong, nonatomic) CAShapeLayer *scratchOffShapeLayer;
#property (strong, nonatomic) CALayer *scratchOffLayer;
#end
#implementation ScratchOffView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit {
_maskPath = [UIBezierPath new];
_maskLayer = [MyShapeLayer new];
_scratchOffLayer = [CALayer new];
_scratchOffShapeLayer = [CAShapeLayer new];
// Important, otherwise you will get a black rectangle
_maskLayer.opaque = NO;
// add the layer holding the shape to "Scratch Off"
[self.layer addSublayer:_scratchOffShapeLayer];
UIColor *c = [UIColor colorWithRed:50.0 / 255.0 green:150.0 / 255.0 blue:140.0 / 255.0 alpha:1.0];
[_scratchOffShapeLayer setFillColor:c.CGColor];
// set the mask layer
[_scratchOffShapeLayer setMask:_maskLayer];
// default 0.0 == no expanded bounds for touch
_expandedBounds = 0.0;
}
- (void)layoutSubviews {
[super layoutSubviews];
[_maskLayer setFrame:[self bounds]];
[_scratchOffShapeLayer setFrame:[self bounds]];
UIBezierPath *b = [UIBezierPath bezierPathWithOvalInRect:[self bounds]];
[_scratchOffShapeLayer setPath:b.CGPath];
// triggers drawInContext
[_maskLayer setNeedsDisplay];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
[_maskPath moveToPoint:currentPoint];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self];
// add line to our maskPath
[_maskPath addLineToPoint:currentPoint];
// update the mask layer path
[_maskLayer setPath:_maskPath.CGPath];
// triggers drawInContext
[_maskLayer setNeedsDisplay];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// accept touch if within expanded bounds
// setting _expandedBounds to a Positive number allows the
// touches to start outside the frame
CGRect r = CGRectInset([self bounds], -_expandedBounds, -_expandedBounds);
return CGRectContainsPoint(r, point);
}
#end
Note that we've added a property: expandedBounds. Since the touches will only register if they begin on this view, we can (virtually) expand the bounds of the view so the user can touch and "drag into the circle."
Here is a complete example implementation. To try and match your question, I use this image (420 x 460 pixels) as the "background" image:
and this image (284 x 284 pixels) as the "image to reveal under the scratch-off circle" (the transparent area is the size we want the circle to be):
ScratchOffTestViewController.h
//
// ScratchOffTestViewController.h
//
#import <UIKit/UIKit.h>
#interface ScratchOffTestViewController : UIViewController
#end
ScratchOffTestViewController.m
//
// ScratchOffTestViewController.m
//
#import "ScratchOffTestViewController.h"
#import "ScratchOffView.h"
#interface ScratchOffTestViewController ()
#property (strong, nonatomic) ScratchOffView *scratchOffView;
#end
#implementation ScratchOffTestViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// create the Scratch Off View
_scratchOffView = [ScratchOffView new];
// load background and giftBox image
UIImage *bkgImage = [UIImage imageNamed:#"backgroundImage"];
UIImage *giftBoxImage = [UIImage imageNamed:#"giftBox"];
if (!bkgImage || !giftBoxImage) {
NSLog(#"Could not load images!!!");
return;
}
UIImageView *bkgImageView = [UIImageView new];
UIImageView *giftImageView = [UIImageView new];
bkgImageView.image = bkgImage;
giftImageView.image = giftBoxImage;
bkgImageView.translatesAutoresizingMaskIntoConstraints = NO;
giftImageView.translatesAutoresizingMaskIntoConstraints = NO;
_scratchOffView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:bkgImageView];
[self.view addSubview:giftImageView];
[self.view addSubview:_scratchOffView];
UILayoutGuide *g = [self.view safeAreaLayoutGuide];
[NSLayoutConstraint activateConstraints:#[
// constrain background image view to background image size
[bkgImageView.widthAnchor constraintEqualToConstant:bkgImage.size.width],
[bkgImageView.heightAnchor constraintEqualToConstant:bkgImage.size.height],
// centered
[bkgImageView.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],
[bkgImageView.centerYAnchor constraintEqualToAnchor:g.centerYAnchor],
// constrain giftBox image view to giftBox image size
[giftImageView.widthAnchor constraintEqualToConstant:giftBoxImage.size.width],
[giftImageView.heightAnchor constraintEqualToConstant:giftBoxImage.size.height],
// centered horizontally, and a little above vertically
[giftImageView.centerXAnchor constraintEqualToAnchor:bkgImageView.centerXAnchor],
[giftImageView.centerYAnchor constraintEqualToAnchor:bkgImageView.centerYAnchor],
// constrain Scratch Off View to giftImageView
[_scratchOffView.widthAnchor constraintEqualToAnchor:giftImageView.widthAnchor],
[_scratchOffView.heightAnchor constraintEqualToAnchor:giftImageView.widthAnchor],
[_scratchOffView.centerXAnchor constraintEqualToAnchor:giftImageView.centerXAnchor],
[_scratchOffView.centerYAnchor constraintEqualToAnchor:giftImageView.centerYAnchor],
]];
// expand the touch bounds of the Scratch Off View by 80-pts
_scratchOffView.expandedBounds = 80.0;
return;
}
#end
On start, we see this:
and after touch-drag a bit on the circle, we see this:
If we continue dragging our touch around, the dark-green circle will eventually be completely gone -- we will have "scratched it off."

How to detect a touch in a subview that was dragged there (or fastest way to get which subview is underneath a tap in its superview)

I've got a Super View, and its got a bunch of subviews. I'd like a user to be able to drag their finger inside the super view and when they drag over the subviews, that subview changes.
I need this to be very efficient, because changes can happen really fast.
So far what i've tried, is in the super view I can get the touch events from touchesBegan:withEvent: and touchesMoved:withEvent: These two combined give me all the touches I need. On each call of these methods, I have to iterate through all of the subviews and test like so
for (UIView *view in self.subviews) {
if (CGRectContainsPoint(view.frame, touchPoint) {
return view;
}
}
Then change that view however I want to. Problem is, from my benchmarking this process is just two slow.
My immediate thought is to have the subview's themselves detect the touch and then just post a notification or something like that when they are tapped. This way I don't have to iterate through them all inside the superview every freaking touch event.
The problem I am encountering with this, is that I haven't found a way to get the subviews themselves to detect that they are touched when they are just dragged over and the touch actually originated on a different UIView.
I'm open to any suggestions that either speed up the first process, or have a different way to accomplish this.
Since your subviews are on a Grid and if they are equally sized, you should be able to calculate the highlighted one directly. You just need to store their references on creation in a 2D array, for performance I suggest to use a c-array.
int x = touchPoint.x/viewWidth;
int y = touchPoint.y/viewHeight;
return views[x][y];
Depending on your Layout, some border margins or spacings between the views should be taken into account.
This way you do not need to iterate the subviews array and you do not need to perform all these CGRectContainsPoint calculations.
Should be easy:
MyViewController.h
#import <UIKit/UIKit.h>
#interface MyViewController : UIViewController
#end
MyViewController.m
#import "MyViewController.h"
#define SIDE_LENGTH 60
#define NUM_SQUARES 15
#implementation MyViewController
{
UIView *_selectedView;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self populateView];
}
- (void)populateView
{
for (int i = 0; i < 15; ++i)
{
CGRect frame = CGRectMake(drand48() * self.view.frame.size.width - SIDE_LENGTH,
drand48() *self.view.frame.size.height - SIDE_LENGTH,
SIDE_LENGTH,
SIDE_LENGTH);
UIView *view = [[UIView alloc] initWithFrame:frame];
view.backgroundColor = [UIColor colorWithRed:drand48()
green:drand48()
blue:drand48()
alpha:1.0f];
view.layer.cornerRadius = SIDE_LENGTH / 2.0f; // cuz circles r pretty
[self.view addSubview:view];
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.view];
for (UIView *view in self.view.subviews)
{
if (CGRectContainsPoint(view.frame, location)) {
_selectedView = view;
break;
}
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.view];
_selectedView.center = location;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.view];
_selectedView.center = location;
}
#end
Of course, for you, just get rid of the populateView method. I'm just using it to demonstrate how this works as a drop-in class.
If you're looking for speed, then subclass UIView and do this:
MyView.h
#import <UIKit/UIKit.h>
#class MyView;
#interface MyView : UIView
#end
MyView.m
#implementation MyView
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.superview];
self.center = location;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.superview];
self.center = location;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.superview];
self.center = location;
}
#end
Then MyViewController.m becomes much simpler and faster:
#import "MyViewController.h"
#import "MyView.h"
#define SIDE_LENGTH 60
#define NUM_SQUARES 15
#implementation MyViewController
{
UIView *_selectedView;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self populateView];
}
- (void)populateView
{
for (int i = 0; i < 15; ++i)
{
CGRect frame = CGRectMake(drand48() * self.view.frame.size.width - SIDE_LENGTH,
drand48() *self.view.frame.size.height - SIDE_LENGTH,
SIDE_LENGTH,
SIDE_LENGTH);
MyView *view = [[MyView alloc] initWithFrame:frame];
view.backgroundColor = [UIColor colorWithRed:drand48()
green:drand48()
blue:drand48()
alpha:1.0f];
view.layer.cornerRadius = SIDE_LENGTH / 2.0f;
[self.view addSubview:view];
}
}
#end

Slide out viewcontroller from right of screen

I want to create a slide out menu from the right of my main view controller. However I want the slide out menu to appear on top of the main view and not push it to one side.
I've looked at ECSlidingViewController 2 and it allows sliding out from either side, however the slide out menus appear cut off on the exposed edges. It creates the effect of the top view controller moving left or right to reveal the hidden menu under it. I want it to appear that the hidden menu sliding out is going onto of the main view controller and that we can see its left edge so no content is cut off.
Is this at all possible and would ECSlidingViewController 2 work for this?
Edit
This is what I have tried:
To detect the touch started:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
BBAppDelegate *myAppDelegate = (BBAppDelegate*)[UIApplication sharedApplication].delegate;
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchLocation = [touch locationInView:self.view];
NSArray *myViewArray = [[NSBundle mainBundle] loadNibNamed:#"FilterView" owner:self options:nil];
self.filterView = myViewArray[0];
[myAppDelegate.window addSubview:self.filterView];
if (CGRectContainsPoint(self.filterView.frame, touchLocation)) {
dragging = YES;
oldX = touchLocation.x;
oldY = touchLocation.y;
}
}
And then dragging:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [[event allTouches] anyObject];
CGPoint touchLocation = [touch locationInView:self.view];
if (dragging) {
CGRect frame = self.filterView.frame;
frame.origin.x = self.filterView.frame.origin.x + touchLocation.x;
frame.origin.y = self.filterView.frame.origin.y + touchLocation.y;
self.filterView.frame = frame;
}
}
However, when I start a touch, the filterView replaces all views and doesn't slide or animate in anyway. I though it was due to not providing a starting point - so I instated self.filterView like so:
self.filterView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 120, 564)];
However changing these values have no effect on the position / size of the view when its displayed.
Edit 2
Okay, I started a new project for testing and managed to get a view on screen to move around by following my touches.
I am using UIPanGestureRecognizer
Like so:
viewDidLoad
- (void)viewDidLoad
{
[super viewDidLoad];
NSArray *myViewArray = [[NSBundle mainBundle] loadNibNamed:#"View" owner:self options:nil];
UIView *myView = myViewArray[0];
self.filterView = myView;
self.filterView.frame = CGRectMake(100, 100, 200, 200);
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self
action:#selector(dragging:)];
pan.delegate = self;
[self.filterView addGestureRecognizer:pan];
[self.view addSubview:self.filterView];
}
Then in the dragging method:
-(void)dragging:(UIPanGestureRecognizer *)gesture
{
if(gesture.state == UIGestureRecognizerStateBegan)
{
NSLog(#"Received a pan gesture");
self.panCoord = [gesture locationInView:gesture.view];
}
CGPoint newCoord = [gesture locationInView:gesture.view];
float dX = newCoord.x-self.panCoord.x;
gesture.view.frame = CGRectMake(gesture.view.frame.origin.x+dX, gesture.view.frame.origin.y, gesture.view.frame.size.width, gesture.view.frame.size.height);
}
This allows the view to move horizontally only.
Almost there - what I am trying to figure out now is getting it to snap to a location when the user lifts their finger. And how to move this view when its start position is off screen.
Edit 3
Okay, so I wanted to control the speed and velocity of the view coming out onto screen. If the user swipes quickly, the view goes into place at the speed they swipe. If they swipe slowly, it goes into place slowly. However I also set the lowest time - so if they take a long time - it defaults to the min amount.
Here is the code:
{
float start;
float end;
NSDate *methodStart;
NSDate *methodEnd;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
methodStart = [NSDate date];
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:touch.view];
start = location.x;}
-(void)dragging:(UIPanGestureRecognizer *)gesture{
if(gesture.state == UIGestureRecognizerStateBegan)
{
self.panCoord = [gesture locationInView:gesture.view];
}
CGPoint newCoord = [gesture locationInView:gesture.view];
float dX = newCoord.x-self.panCoord.x;
gesture.view.frame = CGRectMake(gesture.view.frame.origin.x+dX, gesture.view.frame.origin.y, gesture.view.frame.size.width, gesture.view.frame.size.height);
if (gesture.state == UIGestureRecognizerStateEnded){
methodEnd = [NSDate date];
NSTimeInterval executionTime = [methodEnd timeIntervalSinceDate:methodStart];
double time = executionTime;
float distance = start - end;
int distanceOverTime = distance / time;
float veloticty = distanceOverTime / 100;
double miniTimeToUse = 0.5;
double newMinTime;
if (time < miniTimeToUse){
newMinTime = miniTimeToUse;
}else {
newMinTime = time;
}
if (gesture.view.frame.origin.x+dX >= 180){
[UIView animateWithDuration:newMinTime
delay:0
usingSpringWithDamping:0.9
initialSpringVelocity:veloticty
options:0
animations:^{
self.filterView.frame = CGRectMake(300, 100, 200, 200);
} completion:nil];
}
if (gesture.view.frame.origin.x+dX <= 181){
[UIView animateWithDuration:newMinTime
delay:0
usingSpringWithDamping:0.9
initialSpringVelocity:veloticty
options:0
animations:^{
self.filterView.frame = CGRectMake(50, 100, 200, 200);
} completion:nil];
}
}
}
Now just to get it to work on pulling the view off from the side.

CGAffineTransformMakeRotation goes the other way after 180 degrees (-3.14)

So,
i am trying to do a very simple disc rotation (2d), according to the user touch on it, just like a DJ or something.
It is working, but there is a problem, after certain amount of rotation, it starts going backwards, this amount is after 180 degrees or as i saw in while logging the angle, -3.14 (pi).
I was wondering, how can i achieve a infinite loop, i mean, the user can keep rotating and rotating to any side, just sliding his finger?
Also a second question is, is there any way to speed up the rotation?
Here is my code right now:
#import <UIKit/UIKit.h>
#interface Draggable : UIImageView {
CGPoint firstLoc;
UILabel * fred;
double angle;
}
#property (assign) CGPoint firstLoc;
#property (retain) UILabel * fred;
#end
#implementation Draggable
#synthesize fred, firstLoc;
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
angle = 0;
if (self) {
// Initialization code
}
return self;
}
-(void)handleObject:(NSSet *)touches
withEvent:(UIEvent *)event
isLast:(BOOL)lst
{
UITouch *touch =[[[event allTouches] allObjects] lastObject];
CGPoint curLoc = [touch locationInView:self];
float fromAngle = atan2( firstLoc.y-self.center.y,
firstLoc.x-self.center.x );
float toAngle = atan2( curLoc.y-(self.center.y+10),
curLoc.x-(self.center.x+10));
float newAngle = angle + (toAngle - fromAngle);
NSLog(#"%f",newAngle);
CGAffineTransform cgaRotate = CGAffineTransformMakeRotation(newAngle);
self.transform = cgaRotate;
if (lst)
angle = newAngle;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch =[[[event allTouches] allObjects] lastObject];
firstLoc = [touch locationInView:self];
};
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self handleObject:touches withEvent:event isLast:NO];
};
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self handleObject:touches withEvent:event isLast:YES];
}
#end
And in the ViewController:
UIImage *tmpImage = [UIImage imageNamed:#"theDisc.png"];
CGRect cellRectangle;
cellRectangle = CGRectMake(-1,self.view.frame.size.height,tmpImage.size.width ,tmpImage.size.height );
dragger = [[Draggable alloc] initWithFrame:cellRectangle];
[dragger setImage:tmpImage];
[dragger setUserInteractionEnabled:YES];
dragger.layer.anchorPoint = CGPointMake(.5,.5);
[self.view addSubview:dragger];
I am open to new/cleaner/more correct ways of doing this too.
Thanks in advance.
Flip the angle if it's below -180 or above 180 degrees. Consider the following touchesMoved implementation:
#implementation RotateView
#define DEGREES_TO_RADIANS(angle) ((angle) / 180.0 * M_PI)
CGFloat angleBetweenLinesInDegrees(CGPoint beginLineA, CGPoint endLineA, CGPoint beginLineB, CGPoint endLineB)
{
CGFloat a = endLineA.x - beginLineA.x;
CGFloat b = endLineA.y - beginLineA.y;
CGFloat c = endLineB.x - beginLineB.x;
CGFloat d = endLineB.y - beginLineB.y;
CGFloat atanA = atan2(a, b);
CGFloat atanB = atan2(c, d);
// convert radians to degrees
return (atanA - atanB) * 180 / M_PI;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint curPoint = [[touches anyObject] locationInView:self];
CGPoint prevPoint = [[touches anyObject] previousLocationInView:self];
// calculate rotation angle between two points
CGFloat angle = angleBetweenLinesInDegrees(self.center, prevPoint, self.center, curPoint);
// Flip
if (angle > 180) {
angle -= 360;
} else if (angle < -180) {
angle += 360;
}
self.layer.transform = CATransform3DRotate(self.layer.transform, DEGREES_TO_RADIANS(angle), .0, .0, 1.0);
}
#end
When dragging around the outer bounds of the view, it will rotate it continuously like a spinning wheel. Hope it helps.
You have some problems here:
1-)
CGPoint curLoc = [touch locationInView:self];
and
firstLoc = [touch locationInView:self];
You are transforming your view, and then asking for the location of a touch in it. You cannot get the correct location of a touch in a rotated view.
Make them something not transformed. (for example self.superview after putting it in a container)
2-)
cellRectangle = CGRectMake(-1,self.view.frame.size.height,tmpImage.size.width ,tmpImage.size.height );
You are placing your Draggable instance out of the screen by passing self.view.frame.size.height as the CGRect's y parameter.

UITextView text space go smaller during rezise

I'd like to enable the user to resize an UITextView. I display a button in the bottom rigth corner of the UITextView such as when the user moves it, the UITextView follow it and is resized.
This is works fine but the text space has a strange behaviour. The text space seems to get smaller and smaller during the resize. So if I resize a lot, go to a big frame and then to a small frame, the text space has become smaller than the frame.
- (id) init{//My subclass of UITextView
self = [super init];
if(self){
//Resize button
//Init ResizeButton
self.resizeButton = [[UIButton alloc]init];
//Image
[self.resizeButton setImageForAllState:[UIImage imageNamed:#"Isosceles-right-triangle.png"]];
//Target
[self.resizeButton addTarget:self action:#selector(wasDragged:withEvent:) forControlEvents:UIControlEventTouchDragInside];
[self.resizeButton addTarget:self action:#selector(wasDragged:withEvent:) forControlEvents:UIControlEventTouchDragOutside];
//Add to view
[self addSubview:resizeButton];
//Self
//text font
[self setFont:[UIFont fontWithName:#"papyrus" size:17]];
}
return self;
}
- (void)wasDragged:(UIButton *)button withEvent:(UIEvent *)event
{
// NSLog(#"wasDragged");// get the touch
UITouch *touch = [[event touchesForView:button] anyObject];
// get delta
CGPoint previousLocation = [touch previousLocationInView:button];
CGPoint location = [touch locationInView:button];
CGFloat delta_x = location.x - previousLocation.x;
CGFloat delta_y = location.y - previousLocation.y;
// move button
button.center = CGPointMake(button.center.x + delta_x,
button.center.y + delta_y);
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width+ delta_x, self.frame.size.height+ delta_y);
}
The view is also draggable
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// Calculate offset
CGPoint pt = [[touches anyObject] locationInView:self];
float dx = pt.x - startLocation.x;
float dy = pt.y - startLocation.y;
CGPoint newcenter = CGPointMake(self.center.x + dx, self.center.y + dy);
// Set new location
self.center = newcenter;
}
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Calculate and store offset, and pop view into front if needed
CGPoint pt = [[touches anyObject] locationInView:self];
startLocation = pt;
}
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
}
EDIT
Before resizing
Resizing
Back to original size
Well, I have this problem too. The soliution is to subclass UITextView and override setFrame: method. Before setting your wanted frame, set CGRectZero frame for textView. It will fix your problem. Code bellow.
- (void)setFrame:(CGRect)frame
{
[super setFrame:CGRectZero];
[super setFrame:frame];
}
Seems like you could do one of two things:
First: turn off the 'Adjust to fit' of the control
textfield.adjustsFontSizeToFitWidth = NO;
Second: Hardcode a font size into the code
textfield.font = [UIFont fontWithName:#"desired font name" size:10];

Resources