Nested CAShapeLayer §Get origin? - uiview

for (CALayer *layer in self.view.layer.sublayers) {
if ([layer isKindOfClass:[CAShapeLayer class]]) {
CAShapeLayer *copy = (CAShapeLayer *)layer;
if ([copy.name isEqualToString:#"datumSideLayer"]) {
//get start position, get height, get width
CGPoint startPos = copy.frame.origin;
float width = copy.frame.size.width;
float height = copy.frame.size.height;
CGPoint startPoint = CGPointMake(startPos.x + (width/2), startPos.y);
CGPoint endPoint = CGPointMake(startPos.x + (width/2), startPos.y + height);
NSLog(#"%.1f:%.1f, %.1f:%.1f", startPoint.x, startPoint.y, endPoint.x, endPoint.y);
[self drawDashedLineFrom:startPoint toEnd:endPoint inView:_thicknessUI];
break;
}
}
}
The NSLog output is 0.0:0.0, 0.0:0.0.
The shape layer it's nested for is instantiated strongly as a property. However, I cannot grab the origin, width or height from the frame.
EDIT: after further digging I have no frame set for my CAShapeLayer - as it's drawn by a path.
However, how can I now suit this so it's origin is able to be grabbed from the start of the path?
[path moveToPoint:CGPointMake(self.view.frame.size.width/2 - _plateLengthPX, tenPercent)]; // datum side start
[path addLineToPoint:CGPointMake(self.view.frame.size.width/2 - capStartWidthPX - rootGapPX, tenPercent)]; // weld(cap) start
[path addLineToPoint:CGPointMake(self.view.frame.size.width/2 - rootGapPX, tenPercent + _plateHeightPX - rootPrepHeightPX)]; // weld(root prep) start
[path addLineToPoint:CGPointMake(self.view.frame.size.width/2 - rootGapPX, tenPercent + _plateHeightPX)]; // weld(root prep) end
[path addLineToPoint:CGPointMake(self.view.frame.size.width/2 - _plateLengthPX, tenPercent + _plateHeightPX)]; // datum side end
[path addLineToPoint:CGPointMake(self.view.frame.size.width/2 - _plateLengthPX, tenPercent + _plateHeightPX/2 + 10)]; // datum side end
[path addLineToPoint:CGPointMake(self.view.frame.size.width/2 - _plateLengthPX + 15, tenPercent + _plateHeightPX/2 + 5)]; // datum side end
[path addLineToPoint:CGPointMake(self.view.frame.size.width/2 - _plateLengthPX - 15, tenPercent + _plateHeightPX/2 - 5)]; // datum side end
[path addLineToPoint:CGPointMake(self.view.frame.size.width/2 - _plateLengthPX, tenPercent + _plateHeightPX/2 - 10)]; // datum side end
_datumLayer = [CAShapeLayer layer];
_datumLayer.name = #"datumSideLayer";
_datumLayer.path = [path CGPath];
_datumLayer.strokeColor = [_lineColor CGColor];
_datumLayer.lineWidth = 2.0;
_datumLayer.fillColor = [_shaderColor CGColor];
[self.view.layer addSublayer:_datumLayer];

Okay I've managed to dig deep enough to find a reason for my issue.
The reason is because the frame and origin of the CAShapeLayer are not set therefore you cannot cast a §GET to retrieve their values. However, whilst instantiating the property you can set the position & bounds relative to the path you're using.
CGRect pathRect = CGPathGetPathBoundingBox([path CGPath]);
_datumLayer.bounds = pathRect;
_datumLayer.position = CGPointMake(CGRectGetMidX(pathRect), CGRectGetMidY(pathRect));

Related

Drop `points` along a UIBezierPath such that `center` of points lies ON the path

The problem statement:
Create a semi-circular dial that has dots along it's circumference. User can pan and rotate this dial.
Approach:
Created a UIView
Added a circle with radius that's double the height of the UIView (using [UIBezierPath bezierPathWithOvalInRect] and CAShapeLayer)
Add dots (circular CALayer objects) along the circumference of this circle, such that the center of these dots lies ON the circumference.
The obstacle:
The dots are plotted along a path, but the center of these dots does not lie on the circumference.
The code is as follows:
#property (nonatomic, retain) CAShapeLayer* dialLineLayer;
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
self.dialLineLayer = [CAShapeLayer layer];
CGRect path = CGRectMake(SCREEN_WIDTH /2 - self.radius, -self.radius, 2 * self.radius, 2 * self.radius);
[self.dialLineLayer setPath:[[UIBezierPath bezierPathWithOvalInRect:path] CGPath]];
[self.dialLineLayer setStrokeColor:[[UIColor la_white10Color] CGColor]];
[self.dialLineLayer setFillColor:[[UIColor clearColor] CGColor]];
[self.dialLineLayer setLineWidth:3.0f];
[self.layer addSublayer:self.dialLineLayer];
[self addDotsOnLineLayer];
}
return self;
}
- (CGFloat) radius {
// View has an aspect ratio constraint of 75:34 (W:H)
return SCREEN_WIDTH * 34 / 75 - 10; // Circle radius is 10px less that View's height
}
- (CGFloat) circumference {
return M_2_PI * self.radius; // 2Pi * radius
}
- (void) addDotsOnLineLayer {
CGPoint dotPoint = CGPointMake(SCREEN_WIDTH /2 - self.radius - self.radius/2, 0);
for (double t = 0; t < 2*M_PI; t += 0.2) {
CGFloat dotRadius = arc4random_uniform(10) - arc4random_uniform(3); // Random radius between 3px and 10px
CALayer* dotLayer = [CALayer layer];
dotLayer.frame = CGRectMake(dotPoint.x, dotPoint.y, dotRadius, dotRadius);
dotLayer.cornerRadius = dotRadius / 2;
dotLayer.masksToBounds = YES;
dotLayer.backgroundColor = [UIColor greenColor].CGColor;
dotPoint.x = self.radius * cos(t) + (SCREEN_WIDTH / 2);
dotPoint.y = self.radius * sin(t) + 0;
[self.dialLineLayer addSublayer:dotLayer];
}
}
Try this: (Brace for major 'doh' + facepalm ;) )
dotLayer.frame = CGRectMake(0, 0, dotRadius, dotRadius);
dotLayer.position = dotPoint;
Explanation
You are currently positioning the origin of your layer on the path. (Remove the corner radius and you'll see what I mean.) However, since you want the center of the layer on the path, you have to assign to .position.
Note that this is only true, as long as you leave the .anchorPoint property of the layer at {0.5, 0.5}.
See Core Animation Basics for more info.

Draw dynamic shapes iOS at very high rate

I'm trying to create a sort of "radar" that would look like this :
The idea is that the blue part will act like a compass to indicate proximity of stuff to the user. The shape would be narrow, long and blue when the user is far from the objective, and become shorter, thicker and red when he gets closer, as shown below:
To achieve this, I drew a few circles (actually I tried IBDesignable and IBInspectable but I keep getting "Build failed" but that is another subject)
My question is : Is this possible to draw the "cone" part that is supposed to change its dimensions at a high rate without risking lag ?
Can I use UIBezierPath / AddArc or Addline methods to achieve this or I am heading to a dead end ?
There are two approaches:
Define complex bezier shapes and animate use CABasicAnimation:
- (UIBezierPath *)pathWithCenter:(CGPoint)center angle:(CGFloat)angle radius:(CGFloat)radius {
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint point1 = CGPointMake(center.x - radius * sinf(angle), center.y - radius * cosf(angle));
[path moveToPoint:center];
[path addLineToPoint:point1];
NSInteger curves = 8;
CGFloat currentAngle = M_PI_2 * 3.0 - angle;
for (NSInteger i = 0; i < curves; i++) {
CGFloat nextAngle = currentAngle + angle * 2.0 / curves;
CGPoint point2 = CGPointMake(center.x + radius * cosf(nextAngle), center.y + radius * sinf(nextAngle));
CGFloat controlPointRadius = radius / cosf(angle / curves);
CGPoint controlPoint = CGPointMake(center.x + controlPointRadius * cosf(currentAngle + angle / curves), center.y + controlPointRadius * sinf(currentAngle + angle / curves));
[path addQuadCurveToPoint:point2 controlPoint:controlPoint];
currentAngle = nextAngle;
point1 = point2;
}
[path closePath];
return path;
}
I just split the arc into a series of quad curves. I find cubic curves can get closer, but with just a few quad curves, we get a very good approximation.
And then animate it with CABasicAnimation:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.fillColor = self.startColor.CGColor;
layer.path = self.startPath.CGPath;
[self.view.layer addSublayer:layer];
self.radarLayer = layer;
}
- (IBAction)didTapButton:(id)sender {
self.radarLayer.path = self.finalPath.CGPath;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pathAnimation.fromValue = (id)self.startPath.CGPath;
pathAnimation.toValue = (id)self.finalPath.CGPath;
pathAnimation.removedOnCompletion = false;
self.radarLayer.fillColor = self.finalColor.CGColor;
CABasicAnimation *fillAnimation = [CABasicAnimation animationWithKeyPath:#"fillColor"];
fillAnimation.fromValue = (id)self.startColor.CGColor;
fillAnimation.toValue = (id)self.finalColor.CGColor;
fillAnimation.removedOnCompletion = false;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = #[pathAnimation, fillAnimation];
group.duration = 2.0;
[self.radarLayer addAnimation:group forKey:#"bothOfMyAnimations"];
}
That yields:
The other approach is to use simple bezier, but animate with display link:
If you wanted to use a CAShapeLayer and a CADisplayLink (which is like a timer that's optimized for screen updates), you could do something like:
#interface ViewController ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFAbsoluteTime startTime;
#property (nonatomic) CFAbsoluteTime duration;
#property (nonatomic, weak) CAShapeLayer *radarLayer;
#end
#implementation ViewController
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
CAShapeLayer *layer = [CAShapeLayer layer];
[self.view.layer addSublayer:layer];
self.radarLayer = layer;
[self updateRadarLayer:layer percent:0.0];
}
- (IBAction)didTapButton:(id)sender {
[self startDisplayLink];
}
- (void)updateRadarLayer:(CAShapeLayer *)radarLayer percent:(CGFloat)percent {
CGFloat angle = M_PI_4 * (1.0 - percent / 2.0);
CGFloat distance = 200 * (0.5 + percent / 2.0);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:self.view.center];
[path addArcWithCenter:self.view.center radius:distance startAngle:M_PI_2 * 3.0 - angle endAngle:M_PI_2 * 3.0 + angle clockwise:true];
[path closePath];
radarLayer.path = path.CGPath;
radarLayer.fillColor = [self colorForPercent:percent].CGColor;
}
- (UIColor *)colorForPercent:(CGFloat)percent {
return [UIColor colorWithRed:percent green:0 blue:1.0 - percent alpha:1];
}
- (void)startDisplayLink {
self.startTime = CFAbsoluteTimeGetCurrent();
self.duration = 2.0;
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)handleDisplayLink:(CADisplayLink *)link {
CGFloat percent = (CFAbsoluteTimeGetCurrent() - self.startTime) / self.duration;
if (percent < 1.0) {
[self updateRadarLayer:self.radarLayer percent:percent];
} else {
[self stopDisplayLink];
[self updateRadarLayer:self.radarLayer percent:1.0];
}
}
- (void)stopDisplayLink {
[self.displayLink invalidate];
self.displayLink = nil;
}
That yields:
You should probably use one or more CAShapeLayer objects and Core Animation.
You can install the CGPath from a UIBezierPath into a shape layer, and you can animate the changes to the shape layers, although to get the animation to work right you want to have the same number and type of control points in the shape at the beginning and ending of your animation.

iOS Wave Effect Animated Bezier Curve

I am trying to achieve an animation effect which will simulate a very simple representation of a wave effect by using bezierpath and keyframeanimation.
However, with key frame animation I cant get the effect properly, the animation is working from start point to end instead of making wave curves up and down.
This could be a better representation of what is happening.
Bezier Curve Wave Animation
and this is the code for the animation:
CGPoint rightCurve;
rightCurve.x = center.x + radius * cos(startAngle);
rightCurve.y = center.y + radius * sin(startAngle);
CGPoint leftCurve;
leftCurve.x = center.x + radius * cos(endAngle);
leftCurve.y = center.y + radius * sin(endAngle);
CGPoint centerCurve;
centerCurve.x = center.x;
centerCurve.y = leftCurve.y;
CGPoint controlLeftUp = CGPointMake((leftCurve.x + centerCurve.x) /2, ((centerCurve.y + ((centerCurve.x - rightCurve.x)/2)*1.3)));
CGPoint controlLeftDown = CGPointMake((leftCurve.x + centerCurve.x) /2, ((centerCurve.y - ((centerCurve.x - rightCurve.x)/2)*1.3)));
CGPoint controlRightDown = CGPointMake((rightCurve.x + centerCurve.x) /2, ((centerCurve.y - ((centerCurve.x - rightCurve.x)/2)*1.3)));
CGPoint controlRightUp = CGPointMake((rightCurve.x + centerCurve.x) /2, ((centerCurve.y + ((centerCurve.x - rightCurve.x)/2)*1.3)));
UIBezierPath* path0 = [UIBezierPath bezierPath];
[path0 moveToPoint:CGPointMake(leftCurve.x, leftCurve.y)];
[path0 addQuadCurveToPoint:CGPointMake(centerCurve.x, centerCurve.y) controlPoint:controlLeftUp];
[path0 moveToPoint:CGPointMake(centerCurve.x , centerCurve.y)];
[path0 addQuadCurveToPoint:CGPointMake(rightCurve.x, rightCurve.y) controlPoint:controlRightDown];
UIBezierPath* path1 = [UIBezierPath bezierPath];
[path1 moveToPoint:CGPointMake(rightCurve.x,rightCurve.y)];
[path1 addQuadCurveToPoint:CGPointMake(centerCurve.x, centerCurve.y) controlPoint:controlRightUp];
[path1 moveToPoint:CGPointMake(centerCurve.x , centerCurve.y)];
[path1 addQuadCurveToPoint:CGPointMake(leftCurve.x, leftCurve.y) controlPoint:controlLeftDown];
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:#"path"];
animation.duration = _animationInterval;
animation.values = [NSArray arrayWithObjects:(id)path0.CGPath, (id)path1.CGPath, nil];
animation.repeatCount = HUGE_VALF;
[shapeLayer addAnimation:animation forKey:nil];
It will be amazing if someone tell me how to edit the animation or the curve.
Thanks in advance.

How to get custom shape using UIBezierPath and make it as subview

I am drawing a rect using UIBezierPath. The following code creates the rect in my view controller:
#interface ViewController ()
{
TargetAreaView *targetView; //TargetAreaView is a subclass of UIView
UIView *detectionView;
}
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CGRect frame = self.view.frame;
frame.size.height = frame.size.height - 200;
detectionView = [[UIView alloc]initWithFrame:frame];
targetView = [[TargetAreaView alloc]initWithFrame:detectionView.frame];
targetView.backgroundColor = [UIColor clearColor];
[self.view addSubview:targetView];
// Do any additional setup after loading the view, typically from a nib.
}
I am using the bezier path to draw the rect in TargetAreaView class.
I have used the following code in the UIView subclass:(TargetAreaView)
#interface TargetAreaView : UIView
{
NSMutableArray *points;
CGRect superViewFrame;
CGPoint selectedPoint;
}
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
superViewFrame = frame;
points = [[NSMutableArray alloc]init];
CGPoint startPoint = CGPointMake(superViewFrame.size.width / 2 - 200/2, superViewFrame.size.height / 2 - 200/2);
CGPoint heightPoint1 = CGPointMake(startPoint.x, startPoint.y + 200);
CGPoint widthPoint1 = CGPointMake(startPoint.x + 200, startPoint.y + 200);
CGPoint heightPoint2 = CGPointMake(startPoint.x + 200, startPoint.y);
CGPoint widthPoint2 = CGPointMake(startPoint.x, startPoint.y);
[points addObject:[NSValue valueWithCGPoint:startPoint]];
[points addObject:[NSValue valueWithCGPoint:heightPoint1]];
[points addObject:[NSValue valueWithCGPoint:widthPoint1]];
[points addObject:[NSValue valueWithCGPoint:heightPoint2]];
[points addObject:[NSValue valueWithCGPoint:widthPoint2]];
}
return self;
}
- (void)drawRect:(CGRect)rect
{
if (points && points.count > 0)
{
[self drawTargetArea:points];
[self setNeedsDisplay];
}
}
-(void)drawTargetArea:(NSMutableArray *)savedPoints
{
UIBezierPath *aPath = [UIBezierPath bezierPath];
// Set the starting point of the shape.
for (int idx = 0; idx < savedPoints.count; idx++)
{
if (idx == 0)
{
NSValue *startPoint = [savedPoints objectAtIndex:idx];
[aPath moveToPoint:startPoint.CGPointValue]; // Starting Point
}
else
{
NSValue *drawPoints = [savedPoints objectAtIndex:idx];
[aPath addLineToPoint:drawPoints.CGPointValue]; // Add lines from previous to current point
}
}
[aPath closePath]; // close the final path
aPath.lineWidth = 10.0f;
[[UIColor redColor] setStroke];
[aPath stroke];
}
The above UIView subclass code generates the rect as shown below:
I have written the logic to move the corners of rect to change its shape as shown below:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint startTouchPoint = [touch locationInView:self]; //get the user touch position and check it is nearer in 10px range to any of the position in rect
for (NSValue *aPoint in points)
{
CGPoint currentPoint = aPoint.CGPointValue;
CGFloat distance = [self touchPositionDifference:startTouchPoint WithCornerPosition:currentPoint]; // Calculate the distance between user touch position and rect points
if (distance < 10)
{
selectedPoint = currentPoint; // get the nearest point in a global variable
break;
}
}
}
-(CGFloat)touchPositionDifference:(CGPoint)touchPosition WithCornerPosition:(CGPoint)cornnerPosition
{
CGFloat dx= touchPosition.x - cornnerPosition.x;
CGFloat dy= touchPosition.y - cornnerPosition.y;
CGFloat distance= sqrt(dx*dx + dy*dy);
return distance; // calculate and return distance between two points
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint touchEndPoint = [touch locationInView:self];
for (int idx = 0; idx < points.count; idx++)
{
NSValue *aPoint = [points objectAtIndex:idx];
CGPoint currentPoint = aPoint.CGPointValue;
if (CGPointEqualToPoint(currentPoint, selectedPoint))
{
[points replaceObjectAtIndex:idx withObject:[NSValue valueWithCGPoint:touchEndPoint]]; // get the global point value and touch ended value and replace the new value with old value
}
}
[self setNeedsDisplay];// and draw the shape with new points
}
I can able to get the output like following
I have return code to get the bezier path as follows:
-(UIBezierPath *)getPath
{
if (points.count <= 0) return nil;
NSArray *oldPoints = points;
UIBezierPath *aPath = [UIBezierPath bezierPath];
// Set the starting point of the shape.
CGPoint p1 = [[oldPoints objectAtIndex:0] CGPointValue];
[aPath moveToPoint:CGPointMake(p1.x, p1.y)];
for (uint i=1; i<oldPoints.count; i++)
{
CGPoint p = [[oldPoints objectAtIndex:i] CGPointValue];
[aPath addLineToPoint:CGPointMake(p.x, p.y)];
}
[aPath closePath];
return aPath; //This returns me a bezier path of the already drawn shape
}
I am getting the entire view(within that view i am having the irregular shape View is represented in green color and the shape is represented in red border color) as shown below
I want to get that particular shape alone(the irregular shape) As shown below
How can I achieve this? Am I following the right path?
You can use the following code to draw the above mentioned shape.
- (void)drawCanvas1WithFrame: (CGRect)frame
{
//// Color Declarations
UIColor* color = [UIColor colorWithRed: 1 green: 0 blue: 0 alpha: 1];
//// Bezier Drawing
UIBezierPath* bezierPath = UIBezierPath.bezierPath;
[bezierPath moveToPoint: CGPointMake(CGRectGetMinX(frame) + 77.5, CGRectGetMinY(frame) + 39.5)];
[bezierPath addCurveToPoint: CGPointMake(CGRectGetMinX(frame) + 130.5, CGRectGetMinY(frame) + 14.5) controlPoint1: CGPointMake(CGRectGetMinX(frame) + 130.5, CGRectGetMinY(frame) + 14.5) controlPoint2: CGPointMake(CGRectGetMinX(frame) + 130.5, CGRectGetMinY(frame) + 14.5)];
[bezierPath addLineToPoint: CGPointMake(CGRectGetMinX(frame) + 122.5, CGRectGetMinY(frame) + 75.5)];
[bezierPath addLineToPoint: CGPointMake(CGRectGetMinX(frame) + 61.5, CGRectGetMinY(frame) + 110.5)];
[bezierPath addLineToPoint: CGPointMake(CGRectGetMinX(frame) + 77.5, CGRectGetMinY(frame) + 39.5)];
[UIColor.greenColor setFill];
[bezierPath fill];
[color setStroke];
bezierPath.lineWidth = 1;
[bezierPath stroke];
}

Layer masking based on positioning iOS

I've never worked with animations on iOS specifically and was hoping to be pointed in the right direction. I have so far animated a bunch of pill shaped objects in a fashion similar to what I have below, however I have yet to implement the correct masking based on the y positioning of the shape.
The pill shapes are just UIViews. I'm looking for a way to get their backgrounds to change based on their positioning; the background itself does not move, just the pills.
EDIT:
Based on Rob's answer below, I am now tying the animation to the device's movement like so:
- (void)setAnimationToDeviceMotion {
motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 0.1f;
NSOperationQueue *motionQueue = [[NSOperationQueue alloc] init];
[motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryZVertical toQueue:motionQueue withHandler:^(CMDeviceMotion *motion, NSError *error) {
if (!error) {
double pitch = motion.attitude.pitch;
NSLog(#"%f", pitch);
[self addMaskToView:self.imageView withAdjustment:pitch];
}
}];
}
Assuming this is eight pill shapes that will reveal a single image underneath, you
create a UIBezierPath for the eight pill shapes
create a CAShapeLayer layer that uses that UIBezierPath (or more accurately, its CGPath)
apply this CAShapeLayer as the mask for the layer for your UIImageView
If you want to animate this over time, just replace the mask (e.g. in response to a accelerometer event, a CADisplayLink or NSTimer, or a UIPanGesture), or just update the path of the CAShapeLayer you have applied as the mask to the UIImageView's layer.
Thus, you might have something like:
- (void)addMaskToView:(UIView *)view withAdjustment:(CGFloat)adjustment
{
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = [[self pillsPathWithAdjustment:adjustment forView:view] CGPath];
self.imageView.layer.mask = mask;
}
- (UIBezierPath *)pillsPathWithAdjustment:(CGFloat)adjustment forView:(UIView *)view
{
UIBezierPath *path = [UIBezierPath bezierPath];
for (NSInteger i = 0; i < kNumberOfPills; i++)
[self addPillPathNumber:i forView:view toPath:path adjustment:adjustment];
return path;
}
- (void)addPillPathNumber:(NSInteger)index forView:(UIView *)view toPath:(UIBezierPath *)path adjustment:(CGFloat)adjustment
{
CGFloat const pillSpacing = 5.0f;
CGFloat const pillWidth = view.bounds.size.width / kNumberOfPills - pillSpacing;
CGFloat const pillHeight = view.bounds.size.height * 0.4;
CGFloat const cornerRounding = 10.0f;
CGPoint point = CGPointMake(index * (pillWidth + pillSpacing) + pillSpacing / 2.0, sinf((float) index * M_PI / kNumberOfPills + adjustment * M_PI * 2.0) * 100.0);
// top
point.x += cornerRounding;
[path moveToPoint:point];
point.x += pillWidth - cornerRounding * 2.0;
[path addLineToPoint:point];
// top right corner
[path addArcWithCenter:CGPointMake(point.x, point.y + cornerRounding) radius:cornerRounding startAngle:3 * M_PI_2 endAngle:2.0 * M_PI clockwise:YES];
point.x += cornerRounding;
point.y += cornerRounding;
// right
point.y += pillHeight - cornerRounding * 2.0;
[path addLineToPoint:point];
// lower right corner
[path addArcWithCenter:CGPointMake(point.x - cornerRounding, point.y) radius:cornerRounding startAngle:0 endAngle:M_PI_2 clockwise:YES];
// bottom
point.y += cornerRounding;
point.x -= cornerRounding;
point.x -= pillWidth - cornerRounding * 2.0;
[path addLineToPoint:point];
// lower left corner
[path addArcWithCenter:CGPointMake(point.x, point.y - cornerRounding) radius:cornerRounding startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
// left
point.y -= cornerRounding;
point.x -= cornerRounding;
point.y -= pillHeight - cornerRounding * 2.0;
[path addLineToPoint:point];
// upper left corner
[path addArcWithCenter:CGPointMake(point.x + cornerRounding, point.y) radius:cornerRounding startAngle:M_PI endAngle:3.0 * M_PI_2 clockwise:YES];
[path closePath];
}
By way of illustration, this is how I'd animate this using a CADisplayLink:
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.startTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CGFloat seconds;
CGFloat fractionsOfSecond = modff(CACurrentMediaTime() - self.startTime, &seconds);
[self addMaskToView:self.imageView withAdjustment:fractionsOfSecond];
}
To do masking, you'll need to work with the view's layers. Instead of moving the views themselves up and down, create views that occupy the whole height of the parent, and then create a CALayer in the pill shape. Set the pill layer as the view's layer's mask, and move it up and down.
Something like this:
UIImageView *myImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"redAndBlueBackground"]];
CALayer *pillLayer = [[CALayer alloc] initWithFrame:CGRectMake(0, 0, myImageView.bounds.size.width, myImageView.bounds.size.height / 2];
pillLayer.cornerRadius = 8;
myImageView.layer.maskLayer = pillLayer;
// move the pill layer to height y:
pillLayer.center = CGPointMake(myImageView.bounds.size.width / 2,
(myImageView.bounds.size.height / 2) + y);

Resources