Partial Bezier curve with UIBezierPath [ios] - ios

Usually bezier paths have the parameter with 0 <= t <= 1 which describes a certain point on the curve.
What I would like to do is:
I have the 4 points (start, end, 2 control points ) and now I would like to draw a line from t=0 to t=0.5. Is there a standard way to do this in iOS (native framework or open source)?
If this would not be possible I would have to calculate the endpoint and also two new control points on my own.

If you are only interested in drawing that path and not calculating the points to do something else with them, then you could stroke the path only up until t=0.5.
You can do this with a CAShapeLayer by setting the strokeStart and strokeEnd properties. The appearance of the stroke can be properties like strokeColor and lineWidth. I recommend that you look at the documentation for the full list of properties.
The code would look something like this (I didn't run this so there may be typos etc.):
CAShapeLayer *halfBezier = [CAShapeLayer layer];
// use the full path
halfBezier.path = [yourFullPath CGPath];
// configure the appearance
halfBezier.fillColor = [[UIColor clearColor] CGColor];
halfBezier.strokeColor = [[UIColor redColor] CGColor];
halfBezier.lineWidth = 2.0;
// 0.0 ≤ t ≤ 0.5
halfBezier.strokeStart = 0.0; // the default value (only here for clarity)
halfBezier.strokeEnd = 0.5; // only up until t=0.5
// add this layer to the view's layer where it is supposed to be drawn
[yourView.layer addSublayer:halfBezier];

Related

Get center points of a UIBezierpath

I am able to generate the UIBezierPath of characters with whatever selected font and size. Now I want to make an etched line in between the bezier path. Can I get the center points of the bezier path along? Or any other way that I can make the center dotted line and follow that path?
Here is the code how I do so
Reference Link. I want something like this. :
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 568, 568) cornerRadius:0];
UIBezierPath *circlePath = [self createArcPath];
[path appendPath:circlePath];
[path setUsesEvenOddFillRule:YES];
shapeView = [CAShapeLayer layer];
shapeView.geometryFlipped = false;
shapeView.path = path.CGPath;
shapeView.fillRule = kCAFillRuleEvenOdd;
shapeView.fillColor = [UIColor grayColor].CGColor;
shapeView.opacity = 1.0;
shapeView.lineDashPattern = #[#2, #3];
[self.view.layer addSublayer:shapeView];
CGFloat dashes[] = {2, 3};
[path setLineDash:dashes count:2 phase:0];
- (UIBezierPath *)createArcPath
{
// Create path from text
// See: http://www.codeproject.com/KB/iPhone/Glyph.aspx
// License: The Code Project Open License (CPOL) 1.02 http://www.codeproject.com/info/cpol10.aspx
letters = CGPathCreateMutable();
CTFontRef font = CTFontCreateWithName(CFSTR("Helvetica-Bold"),80, NULL);
NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys:
(__bridge id)font, kCTFontAttributeName,//د
nil];//ج
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:#"H"
attributes:attrs];
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)attrString);
CFArrayRef runArray = CTLineGetGlyphRuns(line);
// for each RUN
for (CFIndex runIndex = 0; runIndex < CFArrayGetCount(runArray); runIndex++)
{
// Get FONT for this run
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);
// for each GLYPH in run
for (CFIndex runGlyphIndex = 0; runGlyphIndex < CTRunGetGlyphCount(run); runGlyphIndex++)
{
// get Glyph & Glyph-data
CFRange thisGlyphRange = CFRangeMake(runGlyphIndex, 1);
CGGlyph glyph;
CGPoint position;
CTRunGetGlyphs(run, thisGlyphRange, &glyph);
CTRunGetPositions(run, thisGlyphRange, &position);
// Get PATH of outline
{
CGPathRef letter = CTFontCreatePathForGlyph(runFont, glyph, NULL);
CGAffineTransform t = CGAffineTransformMakeTranslation(position.x+200, position.y+80);
CGPathAddPath(letters, &t, letter);
CGPathRelease(letter);
}
}
}
CFRelease(line);
self.path = [UIBezierPath bezierPath];
[self.path appendPath:[UIBezierPath bezierPathWithCGPath:letters]];
return self.path;
}
Can I get the center points of the bezier path along? Or any other way
that I can make the center dotted line and follow that path?
To know that user's finger is going along the path or not you have to Hit-Detection on a Path.
To determine whether a touch event occurred on the filled portion of a
path, you can use the containsPoint: method of UIBezierPath. This
method tests the specified point against all closed subpaths in the
path object and returns YES if it lies on or inside any of those
subpaths.
If you want to do hit-testing on the stroked portion of the path
(instead of the fill area), you must use Core Graphics. The
CGContextPathContainsPoint function lets you test points on either the
fill or stroke portion of the path currently assigned to the graphics
context.
Below method tests to see whether the specified point intersects the specified path. The inFill parameter lets the caller specify whether the point should be tested against the filled or stroked portion of the path. The path passed in by the caller must contain one or more closed subpaths for the hit detection to succeed.
Testing points against a path object.
- (BOOL)containsPoint:(CGPoint)point onPath:(UIBezierPath *)path inFillArea:(BOOL)inFill
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGPathRef cgPath = path.CGPath;
BOOL isHit = NO;
// Determine the drawing mode to use. Default to
// detecting hits on the stroked portion of the path.
CGPathDrawingMode mode = kCGPathStroke;
if (inFill)
{
// Look for hits in the fill area of the path instead.
if (path.usesEvenOddFillRule)
mode = kCGPathEOFill;
else
mode = kCGPathFill;
}
// Save the graphics state so that the path can be removed later.
CGContextSaveGState(context);
CGContextAddPath(context, cgPath);
// Do the hit detection.
isHit = CGContextPathContainsPoint(context, point, mode);
CGContextRestoreGState(context);
return isHit;
}
Check this link to know more about Hit-Detection on a Path
Getting center of path
You can get the width of path as myPath.bounds.size.width; and to get the center of the path just divide width by 2.
And to draw dashed line check this answer
To make dashed line on any UIBezierPath as:
CGFloat dashes[] = {2, 3};
//passing an array with the values {2,3} sets a dash pattern that alternates between a 2 space-unit-long painted segment and a 3 space-unit-long unpainted segment.
UIBezierPath *path = [UIBezierPath bezierPath];
[path setLineDash:dashes count:2 phase:0];
and to dash on CAShapeLayer use the property lineDashPattern as:
shapeView.lineDashPattern = #[#2, #3];
While bezier paths are a convenient option for simple drawing, they are complex calculations, so it will be prohibitively expensive and complicated to use them for this purpose. That's because you need to be able to calculate arbitrary points along the path and to know the prior point in order to know the current direction.
You need some other description of the characters to allow you to know these details more easily. That means using a simple vector (or stroke) description of the fonts (as mentioned by rob mayoff in the comments). This description breaks the characters down into a number of straight line segments which are easy to work with. They are all governed by a y=mx+c calculation and you always know 2 points on each line segment so it's easy to interpolate between them and to know the direction of movement.
If the descriptions at the link provided by rob (here) aren't 'accurate' enough for the size at which you want to display the characters you can create new versions with more points in order to achieve a closer approximation to bezier curve options.
Now, with this description you have a lot of options...
For dragging a finger along the path you can interpolate between points to find the closest point on the path to the current touch point and determine when the touch point has strayed too far from the path, or has intersected it. This intersection processing is also how you can determine the coverage percentage. You just need to choose the interpolation gap distance (chosen for a suitable resolution without creating too many points that are really close together) and a tolerance for how far touch points can be from the 'next' path point.
This also allows for other things in the video you link, like dropping images at each point along the (interpolated) path and animating those images around.
You can calculate points along a UIBezierPath with the code in this open source repo:
https://github.com/ImJCabus/UIBezierPath-Length
If you have your 'a' character represented as a bezier path, you can calculate the red points in the image above by writing something like:
UIBezierPath *path = /* ... */;
NSUInteger subdivisions = 100;//however precise you want to be
for(NSUInteger i = 0; i < subdivisions; i++) {
CGFloat percent = (CGFloat)i/subdivisions;
CGPoint point = [path pointAtPercentOfLength:percent];
//draw a dot at this point, or trace it, or whatever you want to do
}
Try this code to draw red dot line:
CAShapeLayer* dotLayer = [CAShapeLayer layer];
dotLayer.path = path.CGPath;
dotLayer.strokeColor = [UIColor redColor].CGColor;
dotLayer.lineWidth = 2;//or the diameter of the dots
dotLayer.lineDashPattern = #[#0, #10];//change 10 to the distance between dots
dotLayer.lineJoin = kCALineJoinRound;
dotLayer.lineCap = kCALineCapRound;
What you want is rather complicated, but I believe it can be achieved with the following steps:
1) get the letter outline path using your method
2) create a new path using CGPathCreateCopyByStrokingPath with lineWidth equal 1
3) get all segments of the new path
4) find all intersection points of all segments
5) determine which intersecting lines are adjacent and use their intersection points to form a center line
6) repeat steps 2-6 increasing the lineWidth value in step 2

I created a linegraph using UIBezierPath .for every minute the data is going change in linegraph so i need to animate last line of line graph

CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 2.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
[pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
Instead of the path, how can I set animation to last line ?
You cannot just animate "the last line in a path". But you can achieve the visual effect by using two entirely separate shape layers, one with the path thus far that is not to be animated (the blue path in my example below) and a completely separate layer whose path is just the last the segment of the line, which will be animated (a red path in my example). You can then:
Make the path of the blue shape layer to be the just the existing, unanimated portion of the final path.
Make the path for the red shape layer to be just the last segment of the new path;
Animate the red layer's strokeEnd from #(0.0) to #(1.0);
When that's done (e.g. in animationDidStop:finished:), remove that red shape layer's path altogether and extend the path of the blue shape layer's path; and
Repeat as needed.
When you do this, it will look like you animated another segment onto the blue shape layer, but what you really did is that you create a separate layer with just the portion to be animated, animate that, and when done, remove that layer and update the original layer's path accordingly
Note, in the above animated example, the red path and the blue paths are on different shape layers, but it almost feels like it's one path because one layer's path starts right where the other one left off. Clearly, in your final product, you would make the two shape layers appear the same, which would be less jarring than the red/blue combination above. I just used different colors here so you could understand what the two different shape layers were doing.
FYI, this is the code that generated the above drawing:
- (void)addLineToPoint:(CGPoint)point {
// create a path that consists just of the portion to be animated
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:self.lastPoint];
[path addLineToPoint:point];
// set the redAnimatedPathLayer's path to be this short segment
self.redAnimatedPathLayer.path = [path CGPath];
// and animate it
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
self.redAnimatedPathLayer.path = [path CGPath];
pathAnimation.duration = 0.5;
pathAnimation.fromValue = #(0.0);
pathAnimation.toValue = #(1.0);
pathAnimation.delegate = self;
[self.redAnimatedPathLayer addAnimation:pathAnimation forKey:#"strokeEndKey"];
// save the point for future reference
self.lastPoint = point;
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
// update the blue layer's path to include this new point
[self.mainPath addLineToPoint:self.lastPoint];
self.blueNonAnimatedLayer.path = self.mainPath.CGPath;
// reset the red layer's path
self.redAnimatedPathLayer.path = nil;
[self repeat];
}

UIBezierPath & CAShapeLayer initial animation jump

I built this for my company: https://github.com/busycm/BZYStrokeTimer and during the course of building, I noticed an interesting "bug" that I can't seem to mitigate when using UIBezierPath. Right when the animation starts, the path jumps a certain number of pixels forward (or backwards depending if it's counterclockwise) instead of starting up with a smooth, incremental animation. And what I found that's really interesting is how much the path jumps forward is actually the value of the line width for the CAShaperLayer.
So for example, if my bezier path starts off at CGRectGetMidX(self.bounds) and the line with is 35, the animation actually starts from CGRectGetMidX(self.bounds)+35 and the larger the line width, the more noticeable the jump is. Is there any way to get rid of that so that path will smoothly animate out from the start point?
Here's a picture of the first frame. This is what it looks like immediately after the animation starts.
Then when I resume the animation and pause again, the distance moved is about 1/100th of the distance you see in the picture.
Here's my bezier path code:
- (UIBezierPath *)generatePathWithXInset:(CGFloat)dx withYInset:(CGFloat)dy clockWise:(BOOL)clockwise{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(CGRectGetMidX(self.bounds)+dx/2, dy/2)];
[path addLineToPoint:CGPointMake(CGRectGetMaxX(self.bounds)-dx/2, dy/2)];
[path addLineToPoint:CGPointMake(CGRectGetMaxX(self.bounds)-dx/2, CGRectGetMaxY(self.bounds)-dy/2)];
[path addLineToPoint:CGPointMake(dx/2, CGRectGetMaxY(self.bounds)-dy/2)];
[path addLineToPoint:CGPointMake(dx/2, dy/2)];
[path closePath];
return clockwise ? path : [path bezierPathByReversingPath];
}
Here's the animation code:
CABasicAnimation *wind = [self generateAnimationWithDuration:self.duration == 0 ? kDefaultDuration : self.duration fromValue:#(self.shapeLayer.strokeStart) toValue:#(self.shapeLayer.strokeEnd) withKeypath:keypath withFillMode:kCAFillModeForwards];
wind.timingFunction = [CAMediaTimingFunction functionWithName:self.timingFunction];
wind.removedOnCompletion = NO;
self.shapeLayer.path = [self generatePathWithXInset:self.lineWidth withYInset:self.lineWidth clockWise:self.clockwise].CGPath;
[self.shapeLayer addAnimation:wind forKey:#"strokeEndAnimation"];
And here's how I construct the CAShapeLayer.
- (CAShapeLayer *)shapeLayer {
return !_shapeLayer ? _shapeLayer = ({
CAShapeLayer *layer = [CAShapeLayer layer];
layer.lineWidth = kDefaultLineWidth;
layer.fillColor = UIColor.clearColor.CGColor;
layer.strokeColor = [UIColor blackColor].CGColor;
layer.lineCap = kCALineCapSquare;
layer.frame = self.bounds;
layer.strokeStart = 0;
layer.strokeEnd = 1;
layer;
}) : _shapeLayer;
}
I think what's happening here is that, in this frame of the animation, you are drawing a line that consists of a single point. Since the line has a thickness associated with it, and the line cap type is kCALineCapSquare, that'll get rendered as a square with height and width equal to the line width.
You can think of it as if you are drawing a line with a square marker, and you are going to drag the midpoint of the marker so that it goes through every point in the curve you specified. For the first point in the line, it's as if the marker touches down at that point, leaving a square behind.
Here's a visual representation the different line cap types that will hopefully make it more intuitive. You should probably change the line cap style to kCALineCapButt.
Sidenote:
After you make that change, in this line of code
[path moveToPoint:CGPointMake(CGRectGetMidX(self.bounds)+dx/2, dy/2)];
you probably don't have to offset the x coordinate by dx/2 anymore.

Animating a circular UIBezierPath

I've got a project where I'm animating a UIBezierPath based on a set progress. The BezierPath is in the shape of a circle and lies in a UIView and animation is done in drawRect using CADisplayLink right now. Simply put, based on a set progress x the path should radially extend (if xis larger than before) or shrink (if x is smaller).
self.drawProgress = (self.displayLink.timestamp - self.startTime)/DURATION;
CGFloat startAngle = -(float)M_PI_2;
CGFloat stopAngle = ((self.x * 2*(float)M_PI) + startAngle);
CGFloat currentEndAngle = ((self.oldX * 2*(float)M_PI) + startAngle);
CGFloat endAngle = currentEndAngle-((currentEndAngle-stopAngle)*drawProgress);
UIBezierPath *guideCirclePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
This is in the case of x shrinking since our last update. The issues I'm experiencing are actually a few:
The shape always starts drawing at 45º (unless I rotate the view). I have not found any way to change this, and setting the startAngleto -45º makes no difference really because it always "pops" to 45. Is there anything I can do about this, or do I have to resort to other methods of drawing?
Is there any other way that one should animate these things? I've read much about using CAShapeLayer but I haven't quite understood the actual difference (in terms of drawbacks and benefits) in using these two methods. If anyone could clarify I would be very much obliged!
UPDATE: I migrated the code over to CAShapeLayer instead, but now I'm facing a different issue. It's best described with this image:
What's happening is that when the layer is supposed to shrink, the thin outer line is still there (regardless of direction of movement). And when the bar shrinks, the delta of 1-xisn't removed unless I explicitly make a new white shape over it. The code for this follows. Any ideas?
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:stopAngle clockwise:YES];
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [circlePath CGPath];
circle.strokeStart = 0;
circle.strokeEnd = 1.0*self.progress;
// Colour and other customizations here.
if (self.progress > self.oldProgress) {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(circle.strokeEnd);
} else {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(1.0*self.progress);
circle.strokeEnd = 1.0*self.progress;
}
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; //kCAMediaTimingFunctionEaseIn
[circle addAnimation:drawAnimation forKey:#"strokeEnd"];
UPDATE 2: I've ironed out most of the other bugs. Turned out it was just me being rather silly the whole time and overcomplicating the whole animation (not to mention multiplying by 1 everywhere, what?). I've made a gif of the bug I can't solve:
Any ideas?
UPDATE 3: (and closure). I managed to get rid of the bug by calling
[self.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
And now everything works as it should. Thanks for all the help!
Using CAShapeLayer is much easier and cleaner. The reason is that CAShapeLayer includes properties strokeStart and strokeEnd. These values range from 0 (the beginning of the path) to 1 (the end of the path) and are animatable.
By changing them you can easily draw any arc of your circle (or any part of an arbitrary path, for that matter.) The properties are animatable, so you can create an animation of a growing/shrinking pie slice or section of a ring shape. It's much easier and more performant than implementing code in drawRect.

Create the indented look found in UINavigationBarButton - programmatically

I'm trying to programmatically recreate the indented button look that can be seen on a UINavigationBarButton. Not the shiny two tone look or the gradient, just the perimeter shading:
It looks like an internal dark shadowing around the entire view perimeter, slightly darker at the top? And then an external highlighting shadow around the lower view perimeter.
I've played a bit with Core Graphics, and experimented with QuartzCore and shadowing with view.layer.shadowRadius and .shadowOffset, but can't even get the lower highlighting to look right. I'm also not sure where to start to achieve both a dark shadowing with internal offset and a light shadowing with external offset.
It seems as though you want a border that looks looks like a shadow. Since the shadow appears to some sort of gradient, setting a border as a gradient won't be possible at first glance. However, it is possible to create a path that represents the border and then fill that with a gradient. Apple provides what seems to be a little known function called CGPathCreateCopyByStrokingPath. This takes a path (say, a rounded rect, for example) and creates a new path that would be the stroke of the old path given the settings you pass into the function (like line width, join/cap setting, miter limit, etc). So lets say you define a path (this isn't exactly what Apple provides, but's it's similar):
+ (UIBezierPath *) bezierPathForBackButtonInRect:(CGRect)rect withRoundingRadius:(CGFloat)radius{
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint mPoint = CGPointMake(CGRectGetMaxX(rect) - radius, rect.origin.y);
CGPoint ctrlPoint = mPoint;
[path moveToPoint:mPoint];
ctrlPoint.y += radius;
mPoint.x += radius;
mPoint.y += radius;
if (radius > 0) [path addArcWithCenter:ctrlPoint radius:radius startAngle:M_PI + M_PI_2 endAngle:0 clockwise:YES];
mPoint.y = CGRectGetMaxY(rect) - radius;
[path addLineToPoint:mPoint];
ctrlPoint = mPoint;
mPoint.y += radius;
mPoint.x -= radius;
ctrlPoint.x -= radius;
if (radius > 0) [path addArcWithCenter:ctrlPoint radius:radius startAngle:0 endAngle:M_PI_2 clockwise:YES];
mPoint.x = rect.origin.x + (10.0f);
[path addLineToPoint:mPoint];
[path addLineToPoint:CGPointMake(rect.origin.x, CGRectGetMidY(rect))];
mPoint.y = rect.origin.y;
[path addLineToPoint:mPoint];
[path closePath];
return path;
}
This returns a path similar to Apple's back button (I use this in my app). I have added this method (along with dozens more) as a category to UIBezierPath.
Now lets add that inner shadow in a drawing routine:
- (void) drawRect:(CGRect)rect{
UIBezierPath *path = [UIBezierPath bezierPathForBackButtonInRect:rect withRoundingRadius:5.0f];
//Just fill with blue color, do what you want here for the button
[[UIColor blueColor] setFill];
[path fill];
[path addClip]; //Not completely necessary, but borders are actually drawn 'around' the path edge, so that half is inside your path, half is outside adding this will ensure the shadow only fills inside the path
//This strokes the standard path, however you might want to might want to inset the rect, create a new 'back button path' off the inset rect and create the inner shadow path off that.
//The line width of 2.0f will actually show up as 1.0f with the above clip: [path addClip];, due to the fact that borders are drawn around the edge
UIBezierPath *innerShadow = [UIBezierPath bezierPathWithCGPath: CGPathCreateCopyByStrokingPath(path.CGPath, NULL, 2.0f, path.lineCapStyle, path.lineJoinStyle, path.miterLimit)];
//You need this, otherwise the center (inside your path) will also be filled with the gradient, which you don't want
innerShadow.usesEvenOddFillRule = YES;
[innerShadow addClip];
//Now lets fill it with a vertical gradient
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint start = CGPointMake(0, 0);
CGPoint end = CGPointMake(0, CGRectGetMaxY(rect));
CGFloat locations[2] = { 0.0f, 1.0f};
NSArray *colors = [NSArray arrayWithObjects:(id)[UIColor colorWithWhite:.7f alpha:.5f].CGColor, (id)[UIColor colorWithWhite:.3f alpha:.5f].CGColor, nil];
CGGradientRef gradRef = CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), (__bridge CFArrayRef)colors, locations);
CGContextDrawLinearGradient(context, gradRef, start, end, 0);
CGGradientRelease(gradRef);
}
Now this is just a simple example. I don't save/restore contexts or anything, which you'll probably want to do. There are things you might still want to do to make it better, like maybe inset the 'shadow' path if you want to use a normal border. You might want to use more/different colors and locations. But this should get you started.
UPDATE
There is another method you can use to create this effect. I wrote an algorithm to bevel arbitrary bezier paths in core graphics. This can be used to create the effect you're looking for. This is an example of how I use it in my app:
You pass to the routine the CGContextRef, CGPathRef, size of the bevel and what colors you want it to use for the highlight/shadow.
The code I used for this can be found here:Github - Beveling Algorithm.
I also explain the code and my methodology here: Beveling-Shapes in Core Graphics
Using the layer's shadow won't do it. You need both a light outer shadow and a dark inner shadow to get that effect. A layer can only have one (outer) shadow. (Also, layer shadows are redrawn dynamically, and force CPU-based rendering which kills performance.)
You'll need to do your own drawing with CoreGraphics, either in a view's drawRect: method or a layer's drawInContext: method. (Or you draw into an image context and then reuse the image.) Said drawing will mostly use CGContext functions. (I'll name some below, but this link has documentation for them all.)
For a round rect button, you might find it tedious to create the appropriate CGPath -- instead, you can use +[UIBezierPath bezierPathWithRoundedRect:cornerRadius:] and then the path's CGPath property to set the context's current path with CGContextAddPath.
You can create an inner shadow by setting a clipping path (see CGContextClip and related functions) to the shape of the button, setting up a shadow (see CGContextSetShadowWithColor and related functions), and then drawing around the outside of the shape you want shadowed. For the inner shadow, stroke (CGContextStrokePath) a round-rect that's a bit larger than your button, using a thick stroke width (CGContextSetLineWidth) so there's plenty of "ink" to generate a shadow (remember, this stroke won't be visible due to the clipping path).
You can create an outer shadow in much the same way -- don't use a clipping path this time, because you want the shadow to be outside the shape, and fill (CGContextFillPath) the shape of your button instead of stroking it. Note that drawing a shadow is sort of a "mode": you save the graphics state (CGContextSaveGState), setup a shadow, then draw the shape you want to see a shadow of (the shape itself isn't drawn when you're in this mode), and finally restore state (CGContextRestoreGState) to get out of "shadow mode". Since that mode doesn't draw the shape, only the shadow, you'll need to draw the shape itself separately.
There's an order to do this all in, too. It should be obvious if you think about the order in which you'd paint these things with physical media: First draw the outer shadow, then the button's fill, then the inner shadow. You might add a stroke after that if the inner shadow doesn't give you a pronounced enough outline.
There are a few drawing tools which can output source code for CoreGraphics: Opacity is one that I use. Be careful with these, though, as they code they generate may not be efficient.

Resources