Rotating rectangle around circumference of a circle (iOS)? - ios

I am trying to rotate the rectangle around the circle. So far after putting together some code I found in various places (mainly here: https://stackoverflow.com/a/4657476/861181) , I am able to rotate rectangle around it's center axis.
How can I make it rotate around the circle?
Here is what I have:
OverlaySelectionView.h
#import <QuartzCore/QuartzCore.h>
#interface OverlaySelectionView : UIView {
#private
UIView* dragArea;
CGRect dragAreaBounds;
UIView* vectorArea;
UITouch *currentTouch;
CGPoint touchLocationpoint;
CGPoint PrevioustouchLocationpoint;
}
#property CGRect vectorBounds;
#end
OverlaySelectionView.m
#import "OverlaySelectionView.h"
#interface OverlaySelectionView()
#property (nonatomic, retain) UIView* vectorArea;
#end
#implementation OverlaySelectionView
#synthesize vectorArea, vectorBounds;
#synthesize delegate;
- (void) initialize {
self.userInteractionEnabled = YES;
self.multipleTouchEnabled = NO;
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;
self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(rotateVector:)];
panRecognizer.maximumNumberOfTouches = 1;
[self addGestureRecognizer:panRecognizer];
}
- (id) initWithCoder: (NSCoder*) coder {
self = [super initWithCoder: coder];
if (self != nil) {
[self initialize];
}
return self;
}
- (id) initWithFrame: (CGRect) frame {
self = [super initWithFrame: frame];
if (self != nil) {
[self initialize];
}
return self;
}
- (void)drawRect:(CGRect)rect {
if (vectorBounds.origin.x){
UIView* area = [[UIView alloc] initWithFrame: vectorBounds];
area.backgroundColor = [UIColor grayColor];
area.opaque = YES;
area.userInteractionEnabled = NO;
vectorArea = area;
[self addSubview: vectorArea];
}
}
- (void)rotateVector: (UIPanGestureRecognizer *)panRecognizer{
if (touchLocationpoint.x){
PrevioustouchLocationpoint = touchLocationpoint;
}
if ([panRecognizer numberOfTouches] >= 1){
touchLocationpoint = [panRecognizer locationOfTouch:0 inView:self];
}
CGPoint origin;
origin.x=240;
origin.y=160;
CGPoint previousDifference = [self vectorFromPoint:origin toPoint:PrevioustouchLocationpoint];
CGAffineTransform newTransform =CGAffineTransformScale(vectorArea.transform, 1, 1);
CGFloat previousRotation = atan2(previousDifference.y, previousDifference.x);
CGPoint currentDifference = [self vectorFromPoint:origin toPoint:touchLocationpoint];
CGFloat currentRotation = atan2(currentDifference.y, currentDifference.x);
CGFloat newAngle = currentRotation- previousRotation;
newTransform = CGAffineTransformRotate(newTransform, newAngle);
[self animateView:vectorArea toPosition:newTransform];
}
-(CGPoint)vectorFromPoint:(CGPoint)firstPoint toPoint:(CGPoint)secondPoint
{
CGPoint result;
CGFloat x = secondPoint.x-firstPoint.x;
CGFloat y = secondPoint.y-firstPoint.y;
result = CGPointMake(x, y);
return result;
}
-(void)animateView:(UIView *)theView toPosition:(CGAffineTransform) newTransform
{
[UIView setAnimationsEnabled:YES];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:0.0750];
vectorArea.transform = newTransform;
[UIView commitAnimations];
}
#end
here is attempt to clarify. I am creating the rectangle from a coordinates on a map. Here is the function that creates that rectangle in the main view. Essentially it is the middle of the screen:
overlay is the view created with the above code.
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
{
if (!circle){
circle = [MKCircle circleWithCenterCoordinate: userLocation.coordinate radius:100];
[mainMapView addOverlay:circle];
CGPoint centerPoint = [mapView convertCoordinate:userLocation.coordinate toPointToView:self.view];
CGPoint upPoint = CGPointMake(centerPoint.x, centerPoint.y - 100);
overlay = [[OverlaySelectionView alloc] initWithFrame: self.view.frame];
overlay.vectorBounds = CGRectMake(upPoint.x, upPoint.y, 30, 100);
[self.view addSubview: overlay];
}
}
Here is the sketch of what I am trying to achieve:

Introduction
A rotation is always done around (0,0).
What you already know:
To rotate around the center of the rectangle you translate the rect to origin, rotate and translate back.
Now for your question:
to rotate around a center point of a circle, simply move the center of the rectangle such that the circle is at (0,0) then rotate, and move back.
start positioning the rectangle at 12 o clock, with the center line at 12.
1) as explained you always rotate around 0,0, so move the center of the circle to 0,0
CGAffineTransform trans1 = CGAffineTransformTranslation(-circ.x, -circ.y);
2) rotate by angle
CGAffineTransform transRot = CGAffineTransformRotation(angle); // or -angle try out.
3) Move back
CGAffineTransform transBack = CGAffineTransformTranslation(circ.x, circ.y);
Concat these 3 rotation matrices to one combibed matrix, and apply it to the rectangle.
CGAffineTransformation tCombo = CGAffineTransformConcat(trans1, transRot);
tCombo = CGTransformationConcat(tCombo, transback);
Apply
rectangle.transform = tCombo;
You probably should also read the chapter about Transformation matrices in Quartz docu.
This code is written with a text editor only, so expect slighly different function names.

Related

iOS Layer animation with auto layout

I have a simple container view(green) and two sub views(red and blue) as below.
The container view is not applying auto layout and I config its size & location by frame.
While the sub views are applying auto layout(see code below)
#implementation XXView {
UIView *_leftView;
UIView *_rightView;
}
- (instancetype)init {
self = [super initWithFrame:CGRectZero];
if (self) {
[self setupViewHierarchy];
[self setupConstraints];
}
return self;
}
- (void)setupViewHierarchy {
_leftView = [[UIView alloc] init];
_leftView.backgroundColor = UIColor.redColor;
_leftView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_leftView];
_rightView = [[UIView alloc] init];
_rightView.backgroundColor = UIColor.blueColor;
_rightView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_rightView];
self.backgroundColor = UIColor.greenColor;
}
- (void)setupConstraints {
[NSLayoutConstraint activateConstraints:#[
[_leftView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:10],
[_leftView.topAnchor constraintEqualToAnchor:self.topAnchor],
[_leftView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[_leftView.widthAnchor constraintEqualToConstant:50],
[_rightView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor constant:-10],
[_rightView.topAnchor constraintEqualToAnchor:_leftView.topAnchor],
[_rightView.bottomAnchor constraintEqualToAnchor:_leftView.bottomAnchor],
[_rightView.widthAnchor constraintEqualToAnchor:_leftView.widthAnchor],
]];
}
...
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
XXView *tv = [[XXView alloc] init];
CGFloat width = self.view.bounds.size.width;
tv.frame = CGRectMake(50, 400, width-100, 100);
[self.view addSubview:tv];
self.tv = tv;
}
Then I would like to animate the container's width change by using the CABasicAnimation as below:
- (void)startAnimation {
[CATransaction begin];
CGFloat width = self.bounds.size.width;
CABasicAnimation *widthAnimation = [CABasicAnimation animationWithKeyPath:#"bounds.size.width"];
widthAnimation.fromValue = #(width/2);
widthAnimation.toValue = #(width);
widthAnimation.duration = 1;
[self.layer addAnimation:widthAnimation forKey:#"123"];
[CATransaction commit];
}
However, the animation is not as I would expect. I would expect the left view moves as the container's leading side and the right view does as the trailing side.
What I see is, the green view expands as expected and the left view moves as green view's leading side. However, the right view is always keeping the same distance to left view. Below is the screenshot taken at the beginning of the animation.
Why the animation is not working as expected?
The problem is that your CABasicAnimation is modifying the bounds of the layer ... but as far as auto-layout is concerned that does not change the width of the view.
It's not really clear what your goal is here, but if you change your animation method to this it might get you on your way:
- (void)startAnimation {
CGRect f = self.frame;
CGFloat fw = f.size.width;
f.size.width = fw / 2;
self.frame = f;
[self layoutIfNeeded];
f.size.width = fw;
[UIView animateWithDuration:1.0 animations:^{
self.frame = f;
[self layoutIfNeeded];
}];
}

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."

should this CustomView be a category or a subclass?

I want to extend an existing custom UIViewclass so it can be used in several places within an app. The view has a method that arranges multiple UIButtonsin a circle. The number of UIButtons will vary depending on where the method is called. So too will the size of the UIButton and the radius of the circle. It would also be useful (but not essential) to be able to call the method several times within the same UIView.
Which is the better way ? to make this a category or a sub-class? It would appear I could use either based on this discussion of pros and cons. But my question is more specific.
I’ve made categories for UIColor and UIFontbut so far I have not been able to make this method work as a category just by following Apple's documentation (I tried it both as a class method and as an instance method). Before I try to make it work as a subclass, can someone who has done it before, either way, please recommend the better approach based on my example below.
Here is the method as it was in the CustomView
circleOfButtons
- (void)circleOfButtons {
screenCentre.x = CGRectGetWidth (self.bounds) / 2;
screenCentre.y = CGRectGetHeight (self.bounds) / 2;
for (int i = 1; i <= buttonCount; i++) {
radians = 2 * M_PI * i / buttonCount;
CGFloat arcStartPoint = - M_PI / 2; // first point clockwise after 12 o'clock
buttonCentre.x = screenCentre.x + radius * cos(radians + arcStartPoint);
buttonCentre.y = screenCentre.y + radius * sin(radians + arcStartPoint);
CGPoint target = CGPointMake(buttonCentre.x, buttonCentre.y);
CGFloat x = screenCentre.x - buttonSize / 2;
CGFloat y = screenCentre.y - buttonSize / 2;
CGFloat wide = buttonSize;
CGFloat high = buttonSize;
UIButton *circleButton = [[UIButton alloc] initWithFrame:CGRectMake(x, y, wide, high)];
[circleButton setTag:i];
circleButton.clipsToBounds = YES;
circleButton.layer.masksToBounds = NO;
circleButton.layer.borderWidth = 0.25f;
circleButton.layer.cornerRadius = buttonSize/2;
circleButton.layer.borderColor = [UIColor blackColor].CGColor;
circleButton.backgroundColor = UIColor.whiteColor;
[circleButton setTitle:[NSString stringWithFormat:#"%i", i] forState:UIControlStateNormal];
[self addSubview:circleButton];
// animation 1
[UIView animateWithDuration:0.5 animations:^{
circleButton.transform = CGAffineTransformMakeScale(1.0, 1.0);
circleButton.center = screenCentre;
}
completion:^(BOOL finished){}];
// animation 2
[UIView animateWithDuration:1.0f animations:^{
circleButton.transform = CGAffineTransformIdentity;
circleButton.center = target;
}
completion:^(BOOL finished){}];
}
and here is the CustomView
CustomView.m
#import <UIKit/UIKit.h>
#import "CustomView.h"
#implementation CustomView : UIView
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:[UIScreen mainScreen].bounds];
if (self) {
self.translatesAutoresizingMaskIntoConstraints = true;
self.backgroundColor = [UIColor lightGrayColor];
buttonCount = 5; //16;
buttonSize = 80; //41;
radius = 68; //105;
[self circleOfButtons];
}
return self;
}
CustomView.h
#import <UIKit/UIKit.h>
#interface CustomView : UIView {
CGPoint screenCentre, buttonCentre;
float radius, radians, buttonSize;
int buttonCount;
}
ViewController.m
#import "ViewController.h"
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CGRect rect = [UIScreen mainScreen].bounds;
float statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height;
CGRect screenFrame = CGRectMake(0, statusBarHeight, rect.size.width, rect.size.height - statusBarHeight);
self.view = [[UIView alloc] initWithFrame: screenFrame];
CustomView *cv = [[CustomView alloc]initWithFrame:screenFrame];
[self.view addSubview:cv];
}

Change View Alpha on Pan Gesture or Dragging

I want to a UIView to drag to bottom of the screen on Pan Gesture but also the view alpha should scale down to "zero", when it reaches to the bottom of the screen.
And vise versa, when I will drag the view upwards then the UIView alpha should scale down to "1"
But the problem is that the view's alpha is scaling down to "Zero" on panning half of the screen or sometimes when I drag the view slower.
Initially I have made the UIView background color to Black.
I need to scale down the alpha of the view gradually , any idea or suggestion will be helpful.
UIPanGestureRecognizer * panner = nil;
panner = [[UIPanGestureRecognizer alloc] initWithTarget: self action:#selector(handlePanGesture:)];
[self.view addGestureRecognizer:panner ];
[panner setDelegate:self];
[panner release];
CGRect frame = CGRectMake(0, 0, 320, 460);
self.dimmer = [[UIView alloc] initWithFrame:frame];
[self.dimmer setBackgroundColor:[UIColor blackColor]];
[self.view addSubview:dimmer];
-(IBAction) handlePanGesture:(UIPanGestureRecognizer *) sender {
static CGPoint lastPosition = {0};
CGPoint nowPosition; float alpha = 0.0;
float new_alpha = 0.0;
nowPosition = [sender translationInView: [self view]];
alpha = [dimmer alpha] -0.0037;
dimmer.alpha -=alpha;
}
I would look at the point on the screen you are currently at inside your handlePanGesture: find the percentage you are at on the view CGFloat percentage = nowPosition.y/self.view.frame.size.height; then set the alpha to that dimmer.alpha = 1.0 - percentage;. This way no matter where you are moving, you are setting the alpha to how close to the bottom you are.
You aren't scaling relative to your gesture; you're setting dimmer.alpha = 0.0037 every time handlePanGesture: executes, regardless of pan direction or distance.
-(IBAction) handlePanGesture:(UIPanGestureRecognizer *) sender {
static CGPoint lastPosition = {0};
CGPoint nowPosition;
float alpha = 0.0;
float new_alpha = 0.0; // Unused!!
nowPosition = [sender translationInView: [self view]]; // Unused!!
alpha = [dimmer alpha] - 0.0037;
dimmer.alpha -= alpha; // === dimmer.alpha = dimmer.alpha - (dimmer.alpha - 0.0037)
// === dimmer.alpha = 0.0037 !!!
}
A better implementation might look something like this:
-(IBAction) handlePanGesture:(UIPanGestureRecognizer *) sender {
CGPoint nowPosition = [sender translationInView: [self view]];
CGFloat alpha = dimmer.alpha - ([sender translationInView: [self view]].y)/320.0;
dimmer.alpha = MAX(0, MIN(1, alpha));
}

Pan gesture interferes with scroll

I have three view controllers that are part of a UIScrollView. I want to be able to swipe between the three, although one of the view controllers has a UIPanGestureRecognizer. I use this pan gesture recognizer to allow the user to drag their finger up and down to increase and decrease the height of a rectangular UIView. Therefore, this UIPanGestureRecognizer only really needs to know about the upwards/downwards panning, and the scroll view can use the horizontal panning.
An example of this, is like the home screen; you can swipe left or right, but also swipe down to get spotlight. I want this kind of mechanism.
This is my code for the pan:
- (void)pan:(UIPanGestureRecognizer *)aPan; // When pan guesture is recognised
{
CGPoint location = [aPan locationInView:self.view]; // Location of finger on screen
CGRect secondRect = CGRectMake(210.0, 45.0, 70.0, 325.0); // Rectangles of maximimum bar area
CGRect minuteRect = CGRectMake(125.0, 45.0, 70.0, 325.0);
CGRect hourRect = CGRectMake(41.0, 45.0, 70.0, 325.0);
if (CGRectContainsPoint(secondRect, location)) { // If finger is inside the 'second' rectangle
CGPoint currentPoint = [aPan locationInView:self.view];
currentPoint.y -= 80; // Make sure animation doesn't go outside the bars' rectangle
if (currentPoint.y < 0) {
currentPoint.y = 0;
}
else if (currentPoint.y > 239) {
currentPoint.y = 239;
}
currentPoint.y = 239.0 - currentPoint.y;
CGFloat pointy = currentPoint.y - fmod(currentPoint.y, 4.0);
[UIView animateWithDuration:0.01f // Animate the bars to rise as the finger moves up and down
animations:^{
CGRect oldFrame = secondBar.frame;
secondBar.frame = CGRectMake(oldFrame.origin.x, (oldFrame.origin.y - (pointy - secondBar.frame.size.height)), oldFrame.size.width, (pointy));
}];
CGFloat result = secondBar.frame.size.height - fmod(secondBar.frame.size.height, 4.0);
secondInt = (result / 4.0); // Update labels with new time
self->secondLabel.text = [NSString stringWithFormat:#"%02d", secondInt];
}
The code is basically repeated for three separate rectangular UIViews.
If anyone can tell me how to get the homescreen-style panning/swiping into my app, that would be great!!
Alright, here is the short answer:
You have to use UIGestureRecognizer's method -requireGestureRecognizerToFail:.
And here is the long answer:
You have to make the pan gesture recognizer of your scroll view to succeed only if the pan gesture recognizer of TimerViewController fails. However that gesture (TimerViewController's gesture) should only succeed if the initial movement is vertical. If it is horizontal it should fail.
To accomplish this we have to subclass UIPanGestureRecognizer and modify it to fit those needs.
Here is what you have to do:
Disregard ALL the changes you made from my previous answer
Add VerticalPanGestureRecognizer to your project.
Modify TimerViewController as shown.
Modify ScrollViewController as shown.
VerticalPanGestureRecognizer.h
#import <UIKit/UIKit.h>
#import <UIKit/UIGestureRecognizerSubclass.h>
#interface VerticalPanGestureRecognizer : UIPanGestureRecognizer
#end
VerticalPanGestureRecognizer.m
#import "VerticalPanGestureRecognizer.h"
#interface VerticalPanGestureRecognizer ()
#property (nonatomic, assign) CGPoint origLoc;
#end
#implementation VerticalPanGestureRecognizer
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.origLoc = [[touches anyObject] locationInView:self.view.superview];
[super touchesBegan:touches withEvent:event];
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (self.state == UIGestureRecognizerStatePossible) {
CGPoint loc = [[touches anyObject] locationInView:self.view.superview];
CGFloat deltaX = fabs(loc.x - self.origLoc.x);
CGFloat deltaY = fabs(loc.y - self.origLoc.y);
if (deltaY < deltaX)
self.state = UIGestureRecognizerStateFailed;
}
[super touchesMoved:touches withEvent:event];
}
#end
TimerViewController.h
// Your imports here
#interface TimerViewController : UIViewController
{
// Your ivars here
}
// Add the following property
#property (nonatomic, strong) UIPanGestureRecognizer *pan;
// Your methods here
#end
TimerViewController.m
#import "TimerViewController.h"
#import "VerticalPanGestureRecognizer.h"
#implementation TimerViewController
#synthesize pan = _pan;
// prefersStatusBarHidden method here
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil // Initialise view controller
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Instantiate the pan gesture as "VerticalPanGestureRecognizer"
self.pan = [[VerticalPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)]; // Create recogniser for a pan guesture
self.pan.maximumNumberOfTouches = self.pan.minimumNumberOfTouches = 1;
[self.view addGestureRecognizer:self.pan];
}
return self;
}
// The rest of your code here
#end
ScrollViewController.m
- (void)viewDidLoad
{
// Your code here
TimerViewController *tvc = [[TimerViewController alloc]init];
CGRect frame = tvc.view.frame;
frame.origin.x = 320;
tvc.view.frame = frame;
// Add the following line
[self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:tvc.pan];
[self addChildViewController:tvc];
[self.scrollView addSubview:tvc.view];
[tvc didMoveToParentViewController:self];
// More code here
}
This new approach works perfectly. I tested it.
Let me know if you have more questions.
Cheers!
UPDATE
To answer the question you posted on the comments, here is what you have to do:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
BarsViewController *bvc = [[BarsViewController alloc]init];
[self addChildViewController:bvc];
[self.scrollView addSubview:bvc.view];
[bvc didMoveToParentViewController:self];
TimerViewController *tvc = [[TimerViewController alloc]init];
CGRect frame = tvc.view.frame;
frame.origin.x = 320;
tvc.view.frame = frame;
[self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:tvc.pan];
[self addChildViewController:tvc];
[self.scrollView addSubview:tvc.view];
[tvc didMoveToParentViewController:self];
StopwatchViewController *svc = [[StopwatchViewController alloc] init];
frame = svc.view.frame;
frame.origin.x = 320*2;
svc.view.frame = frame;
[self addChildViewController:svc];
[self.scrollView addSubview:svc.view];
[svc didMoveToParentViewController:self];
self.scrollView.contentSize = CGSizeMake(320*3, self.view.frame.size.height);
self.scrollView.pagingEnabled = YES;
[self.scrollView setShowsHorizontalScrollIndicator:NO];
}
Again, I tested it and it's working. You just have to add the gesture recognizer for the bars

Resources