I have created a method that I would like to call on UITextField. Here is the method:
- (void)addBorderLayer:(UITextField *)tf{
CALayer *border = [CALayer layer];
CGFloat borderWidth = 2;
border.borderColor = [UIColor redColor].CGColor;
border.frame = CGRectMake(0, tf.frame.size.height - borderWidth, tf.frame.size.width, tf.frame.size.height);
border.borderWidth = borderWidth;
tf.layer.masksToBounds = YES;
}
Once I attempt to call the method it doesn't change my UITextField:
- (void)viewDidLoad {
[super viewDidLoad];
[self addBorderLayer:self.emailTextField];
}
What am I doing wrong?
In addBorderLayer: you need to add the layer to text field after you set it up:
[tf.layer addSublayer:border];
You should change the layer of your textfield. Consider something like this:
-(void) addBorderLayer:(UITextField *) tf
{
tf.layer.borderColor = [UIColor redColor].CGColor;
tf.layer.borderWidth = 2;
tf.layer.cornerRadius = 5;
}
The issue is as everyone else has said you aren't actually adding the layer to the UITextField. The other answers are all good but I thought I'd share a something that might seem a little more complicated but would probably make it more reusable and that's always good.
First thing is create a new category for UIView called UIView+layer and in here we'd have the following:
UIView+layer.h
#interface UIView(layer)
- (void)addBorder;
- (void)addBorderWithColor:(UIColor *)color;
- (void)addBorderWithColor:(UIColor *)color width:(CGFloat)width;
- (void)addBorderWithColor:(UIColor *)color radius:(CGFloat)radius;
- (void)addBorderWithWidth:(CGFloat)width;
- (void)addBorderWithWidth:(CGFloat)width radius:(CGFloat)radius;
- (void)addBorderWithRadius:(CGFloat)radius;
- (void)addBorderWithColor:(UIColor *)color width:(CGFloat)width radius:(CGFloat)radius; // Essentially the one we will use
#end
UIView+layer.m
#import "UIView+layer.h"
#pragma mark - Default values
// Some constants for the default values
CGFloat const kDefaultBorderWidth = 2.0;
CGFloat const kDefaultBorderRadius = 5.0;
UIColor * const kDefaultBorderColor = [UIColor blackColor];
#implementation UIView(layer)
// Essentially this is the method all the others will call.
- (void)addBorderWithColor:(UIColor *)color width:(CGFloat)width radius:(CGFloat)radius
{
CALayer *border = [CALayer layer];
border.borderColor = color.CGColor;
border.frame = CGRectMake(0, self.frame.size.height - width, self.frame.size.width, self.frame.size.height);
border.borderWidth = width;
self.layer.masksToBounds = YES;
[self addSublayer:layer];
// OR you can affect the layer it already has like with the code below instead of using the addSublayer: method
// self.layer.borderColor = color.CGColor;
// self.layer.borderWidth = width;
// self.layer.frame = CGRectMake(0, self.frame.size.height - width, self.frame.size.width, self.frame.size.height);
}
#pragma mark - additional methods that you can call for setting a border
- (void)addBorder
{
[self addBorderWithColor:kDefaultBorderColor
width:kDefaultBorderWidth
radius:kDefaultBorderRadius];
}
- (void)addBorderWithColor:(UIColor *)color
{
[self addBorderWithColor:color
width:kDefaultBorderWidth
radius:kDefaultBorderRadius];
}
- (void)addBorderWithColor:(UIColor *)color width:(CGFloat)width
{
[self addBorderWithColor:color
width:width
radius:kDefaultBorderRadius];
}
- (void)addBorderWithColor:(UIColor *)color radius:(CGFloat)radius
{
[self addBorderWithColor:color
width:kDefaultBorderWidth
radius:radius];
}
- (void)addBorderWithWidth:(CGFloat)width
{
[self addBorderWithColor:kDefaultBorderColor
width:width
radius:kDefaultBorderRadius];
}
- (void)addBorderWithWidth:(CGFloat)width radius:(CGFloat)radius
{
[self addBorderWithColor:kDefaultBorderColor
width:width
radius:radius];
}
- (void)addBorderWithRadius:(CGFloat)radius
{
[self addBorderWithColor:kDefaultBorderColor
width:kDefaultBorderWidth
radius:radius];
}
#end
Now if you don't know anything about categories you're probably wondering why we did UIView instead of UITextField. The reason being is that we are making it reusable with all classes that are a subclass of UIView and that includes UITextField so all you need to do is do #import "UIView+layer.h" and then you can use these methods for the instance of your UITextField so in your case you can now just do [self.emailTextField addBorder]; and it will add a border to your UITextField using the default values.
I know it looks like a lot more work compared to the others but this just makes it so it is more reusable across more classes that subclass UIView and re-usability is always a code thing when it comes to coding.
Try moving the code to viewWillAppear, at which point it should have had its views positioned and the bounds etc will be correct.
Also add the layer to the text field as pointed out by Greg.
You are creating the new layer, but you never add it to the ui.
You will want to add it to the target view's layer.
[tf.layer addSublayer:border];
Related
The unusual bottom corners of an iPhoneX are Apple's new (2017) "continuous corners for iPhoneX".
It is trivial for any experienced iOS programmer to approximate the curve, but:
Does anyone know exactly how to achieve these, exactly as Apple does?
Even if it's a private call, it would be good to know.
It does seem bizarre that Apple have not explained this.
Please note that it's trivial to "approximate" the curve:
To repeat,
it is trivial for any experienced iOS programmer to approximate the curve.
The question being asked here is specifically how to do Apple actually do it?
Please do not post any more answers showing beginners how to draw a curve and approximate the iPhone curve.
As of iOS 13, there's an API available for this:
https://developer.apple.com/documentation/quartzcore/calayercornercurve
See CALayerCornerCurve.continuous
I wrote an experimental class which constructs a bezier path which overlaps the border of a CALayer due to #Aflah Bhari's comments. The layer has set its private property continuousCornersto YES. This is the result:
The border of the layer is blue while the color of the path is red.
Here is the code. You can set radius and insets in attribute inspector of Interface Builder. I have created the image above by setting the class of the view controllers view to ArcView, its radius to 30.0 and the insets to (20.0, 20.0).
Here is the code:
ArcView.h
IB_DESIGNABLE
#interface ArcView : UIView
#property(nonatomic) IBInspectable CGFloat radius;
#property(nonatomic) IBInspectable CGSize insets;
#end
ArcView.m
#import "ArcView.h"
#interface CALayer(Private)
#property BOOL continuousCorners;
#end
#interface ArcView()
#property (strong) CALayer *borderLayer;
#end
#implementation ArcView
- (void)setRadius:(CGFloat)inRadius {
if(_radius != inRadius) {
_radius = inRadius;
self.borderLayer.cornerRadius = inRadius;
[self setNeedsDisplay];
}
}
- (void)setInsets:(CGSize)inInsets {
if(!CGSizeEqualToSize(_insets, inInsets)) {
_insets = inInsets;
[self setNeedsLayout];
[self setNeedsDisplay];
}
}
- (void)awakeFromNib {
[super awakeFromNib];
self.borderLayer = [CALayer new];
self.borderLayer.borderColor = [[UIColor blueColor] CGColor];
self.borderLayer.borderWidth = 0.5;
self.borderLayer.continuousCorners = YES;
self.borderLayer.cornerRadius = self.radius;
[self.layer addSublayer:self.borderLayer];
}
- (void)layoutSubviews {
[super layoutSubviews];
self.borderLayer.frame = CGRectInset(self.bounds, self.insets.width, self.insets.height);
}
- (void)drawRect:(CGRect)rect {
CGFloat theRadius = self.radius;
CGFloat theOffset = 1.2 * theRadius;
CGRect theRect = CGRectInset(self.bounds, self.insets.width, self.insets.height);
UIBezierPath *thePath = [UIBezierPath new];
CGPoint thePoint;
[thePath moveToPoint:CGPointMake(CGRectGetMinX(theRect) + theOffset, CGRectGetMinY(theRect))];
[thePath addLineToPoint:CGPointMake(CGRectGetMaxX(theRect) - theOffset, CGRectGetMinY(theRect))];
thePoint = CGPointMake(CGRectGetMaxX(theRect), CGRectGetMinY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMaxX(theRect), CGRectGetMinY(theRect) + theOffset) controlPoint:thePoint];
[thePath addLineToPoint:CGPointMake(CGRectGetMaxX(theRect), CGRectGetMaxY(theRect) - theOffset)];
thePoint = CGPointMake(CGRectGetMaxX(theRect), CGRectGetMaxY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMaxX(theRect) - theOffset, CGRectGetMaxY(theRect)) controlPoint:thePoint];
[thePath addLineToPoint:CGPointMake(CGRectGetMinX(theRect) + theOffset, CGRectGetMaxY(theRect))];
thePoint = CGPointMake(CGRectGetMinX(theRect), CGRectGetMaxY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMinX(theRect), CGRectGetMaxY(theRect) - theOffset) controlPoint:thePoint];
[thePath addLineToPoint:CGPointMake(CGRectGetMinX(theRect), CGRectGetMinY(theRect) + theOffset)];
thePoint = CGPointMake(CGRectGetMinX(theRect), CGRectGetMinY(theRect));
[thePath addQuadCurveToPoint:CGPointMake(CGRectGetMinX(theRect) + theOffset, CGRectGetMinY(theRect)) controlPoint:thePoint];
thePath.lineWidth = 0.5;
[[UIColor redColor] set];
[thePath stroke];
}
#end
I hope this helps you with your problem. I've found the factor of 1.2 for theOffset through experiments. You might modify this value if necessary. The value I have chosen for the radius is not optimal and can certainly be improved. But since it depends on the exact distance from the rim, I didn't invest much time for it.
I am trying to create a circular progress bar around an image as shown in the screenshot below. So far I have managed to create a round image with a green border using the code below:
self.image.layer.cornerRadius = self.image.frame.size.width / 2
self.image.clipsToBounds = true
self.image.layer.borderWidth = 6.0
self.image.layer.borderColor = UIColor.greenColor.CGColor
My question is, how do I create a circular progress bar from the border than I have set? Or do I need to remove this border and take a different approach?
I understood your problem and I suggest you to use some library to create such a loader you can find many of the swift libraries. On of them is FPActivityIndicator, it is the same circular loader which you are searching for, you only have to adjust the radius and position of the loader to move it around the image
if you need further help you can send me message and if it helps please accept the answer. Thanks
I have customized the answer of WDUK in stack overflow post according to your need,
It is something like,
TestView.h
#import <UIKit/UIKit.h>
#interface TestView : UIView
#property (nonatomic) double percent;
#end
TestView.m
#import "TestView.h"
#interface TestView () {
CGFloat startAngle;
CGFloat endAngle;
}
#end
#implementation TestView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor whiteColor];
// Determine our start and stop angles for the arc (in radians)
startAngle = M_PI * 1.5;
endAngle = startAngle + (M_PI * 2);
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGFloat constant = rect.size.width/ 5;
UIImage *img = [UIImage imageNamed:#"lion.jpg"]; // lion.jpg is image name
UIImageView *imgView = [[UIImageView alloc]initWithFrame:CGRectMake(rect.origin.x + constant/2, rect.origin.y + constant/2, rect.size.width-constant, rect.size.height - constant)];
imgView.image = img;
imgView.layer.masksToBounds = YES;
imgView.layer.cornerRadius = imgView.frame.size.width / 2;
UIBezierPath* bezierPath = [UIBezierPath bezierPath];
// Create our arc, with the correct angles
[bezierPath addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
radius:constant*2
startAngle:startAngle
endAngle:(endAngle - startAngle) * (_percent / 100.0) + startAngle
clockwise:YES];
// Set the display for the path, and stroke it
bezierPath.lineWidth = 20;
[[UIColor redColor] setStroke];
[bezierPath stroke];
[self addSubview:imgView];
}
#end
ViewController.m
#import "ViewController.h"
#import "TestView.h"
#interface ViewController (){
TestView* m_testView;
NSTimer* m_timer;
}
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Init our view
CGRect frame = CGRectMake(50, 50, 200, 200);
m_testView = [[TestView alloc] initWithFrame:frame];
m_testView.percent = 0;
[self.view addSubview:m_testView];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)viewDidAppear:(BOOL)animated
{
// Kick off a timer to count it down
m_timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(increamentSpin) userInfo:nil repeats:YES];
}
- (void)increamentSpin
{
// increament our percentage, do so, and redraw the view
if (m_testView.percent < 100) {
m_testView.percent = m_testView.percent + 1;
[m_testView setNeedsDisplay];
}
else {
[m_timer invalidate];
m_timer = nil;
}
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
If you are decreasing frame size from viewcontroller then you should reduce bezierPath.lineWidth accordingly to show respectively thin progress around imageview.
And this is working perfact, i have tested it!!!
I have this 10 keypad on iPhone that has basically the same screen as the iPhone unlock screen, only mine rotates which resizes the buttons to fit the screen. The problem is that the animation of rotating the device distorts the round shape because they have changed size, but the cornerRadius is the same until it completes the animation. Once the rotation animation has completed, the buttons get the radius set again, thus making them round. I don't know how to have the UIButtons always round, specifically during the rotation animation. Here's basically what I have:
- (void)viewDidLayoutSubviews {
[self setupButtons];
}
- (void)setupButtons {
for (UIButton *button in self.touchPadButtons) {
[self roundifyButton:button withRadius:radius];
}
}
- (void)roundifyButton:(UIButton *)button {
NSInteger height = button.frame.size.height;
radius = height/2;
button.layer.cornerRadius = radius;
button.layer.borderWidth = .6f;
button.layer.borderColor = [UIColor whiteColor].CGColor;
button.clipsToBounds = YES;
}
I've tried using:
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
but it seems like in order for setting the radius to work from that method, I'd have to set the size of my buttons programmatically instead of using autolayout. Does anyone have any magical suggestions on handling this? I would sure love to not rotate, like Apple, but unfortunately that decision was not mine.
Wow, this was tougher than I thought it would be. Fortunately, WWDC is going on right now and I was able to get a solution from the Interface Builder lab. His solution was to subclass UIButton and overwrite the drawRect: method. So this is the only method you should have in the CircleButton class. One issue I found is that the lineWidth property doesn't get set before it's initialized by the nib. I overwrote the init method and set a default value, but it doesn't get hit the first time when the nib initializes the buttons. So I had to add the default value in the drawRect: method. I hope this helps people who need circular UIButtons that can resize.
- (void)drawRect:(CGRect)rect {
[[UIColor whiteColor] set];
if (!self.lineWidth) {
self.lineWidth = 0.75;
}
CGRect bounds = [self bounds];
CGRect circleRect = CGRectMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds), 0, 0);
CGFloat radius = MIN(bounds.size.width, bounds.size.height) / 2.0;
circleRect = CGRectInset(circleRect, -radius, -radius);
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(circleRect, self.lineWidth / 2.0, self.lineWidth / 2.0)];
[path setLineWidth:self.lineWidth];
[path stroke];
}
In case you want to animate the button click the way iPhone lock-screen does, you'll need to add this to the CircleButton class. Otherwise only the titleLabel will be highlighted.
- (void)drawRect:(CGRect)rect {
[[UIColor whiteColor] set];
if (!self.lineWidth) {
self.lineWidth = 0.75;
}
CGRect bounds = [self bounds];
CGRect circleRect = CGRectMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds), 0, 0);
CGFloat radius = MIN(bounds.size.width, bounds.size.height) / 2.0;
circleRect = CGRectInset(circleRect, -radius, -radius);
self.layer.cornerRadius = radius;
self.clipsToBounds = YES;
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(circleRect, self.lineWidth / 2.0, self.lineWidth / 2.0)];
[path setLineWidth:self.lineWidth];
[path stroke];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// highlight button on click
self.backgroundColor = [UIColor lightGrayColor];
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
// remove highlight
self.backgroundColor = [UIColor clearColor];
}
I'm trying to make a button that will show/hide a color-selection slider. That's the easy part.
What I am not sure how to do is to make the button be a circle with an outline (say black) and the fill be the color shown in the slider. As the user moves the slider, the color of the circle should change accordingly.
I am new to iOS development, so probably I am approaching this all wrong. I would really appreciate any ideas even they are completely different from what I have here.
I started to approach this with a subclass using QuartzCore layers, and the result is decent, but there are issues/things that bother me:
For some reason I get no highlight state when pressing the button
It's obviously odd to use rounded corners to achieve a circle
Unfortunately, it seems at the point were I draw the layers, the button is not laid out yet so I have had to hardcode the radius instead of making it based on the button size.
So, yeah I hardly think this approach is ideal. I will greatly appreciate any and all help. Thank you!
#import "ColorPickerButton.h"
#implementation ColorPickerButton
#pragma mark - UIButton Overrides
+ (ColorPickerButton *)buttonWithType:(UIButtonType)buttonType
{
return [super buttonWithType:UIButtonTypeCustom];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
r = 0.3;
g = 0.6;
b = 0.9;
[self drawOutline];
[self drawColor];
}
return self;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (void)layoutSubviews
{
_outlineLayer.frame = CGRectInset(self.bounds, 5, 5);
_colorLayer.frame = CGRectInset(self.bounds, 5+_outlineLayer.borderWidth, 5+_outlineLayer.borderWidth);
[super layoutSubviews];
}
#pragma mark - Layer setters
- (void)drawOutline
{
if (!_outlineLayer)
{
_outlineLayer = [CALayer layer];
_outlineLayer.cornerRadius = 20.0;
_outlineLayer.borderWidth = 3;
_outlineLayer.borderColor = [[UIColor colorWithRed:170.0/255.0 green:170.0/255.0 blue:170.0/255.0 alpha:1.0] CGColor];
_outlineLayer.opacity = 0.6;
[self.layer insertSublayer:_outlineLayer atIndex:1];
}
}
- (void)drawColor
{
if (!_colorLayer)
{
_colorLayer = [CALayer layer];
}
_colorLayer.cornerRadius = 16.0;
_colorLayer.backgroundColor = [[UIColor colorWithRed:r green:g blue:b alpha:1.0] CGColor];
_colorLayer.opacity = 1.0;
[self.layer insertSublayer:_colorLayer atIndex:2];
}
- (void)changeColorWithRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue
{
r = red;
g = green;
b = blue;
[self drawColor];
}
#end
We do this in our app. We use the CALayer on the button and add rounded corners 1/2 the size of the component width. We add a border to the button and then set the button background color to achieve the 'fill' color. Works well.
Note to casual readers: Despite the title, this question has nothing to do with the UIScrollView properties bounces (scrolling related) or bouncesZoom.
I am using UIScrollView to add zooming to a custom view. The custom view uses sublayers to draw its content. Each sublayer is a CALayer instance that is added to the view's main layer with [CALayer addSublayer:]. Sublayers use CoreGraphics to render their content.
After each zoom completes, the custom view needs to redraw its content at the new zoom scale so that the content appears crisp and sharp again. I am currently trying to get the
approach to work that is shown in this SO question, i.e. I reset the scroll view's zoomScale property to 1.0 after each zoom operation, then I adjust the minimumZoomScale and maximumZoomScale properties so that the user cannot zoom in/out more than originally intended.
The content redrawing already works correctly (!), but what I am missing is a smooth GUI update so that the zoomed content is redrawn in place without appearing to move. With my current solution (code example follows at bottom of this question), I observe a kind of "bounce" effect: As soon as the zoom operation ends, the zoomed content briefly moves to a different location, then immediately moves back to its original location.
I am not entirely sure what the reason for the "bounce" effect is: Either there are two GUI update cycles (one for resetting zoomScale to 1.0, and another for setNeedsDisplay), or some sort of animation is taking place that makes both changes visible, one after the other.
My question is: How can I prevent the "bounce" effect described above?
UPDATE: The following is a minimal but complete code example that you can simply copy&paste to observe the effect that I am talking about.
Create a new Xcode project using the "Empty application" template.
Add the code below to AppDelegate.h and AppDelegate.m, respectively.
In the project's Link build phase, add a reference to QuartzCore.framework.
Stuff that goes into AppDelegate.h:
#import <UIKit/UIKit.h>
#class LayerView;
#interface AppDelegate : UIResponder <UIApplicationDelegate, UIScrollViewDelegate>
#property (nonatomic, retain) UIWindow* window;
#property (nonatomic, retain) LayerView* layerView;
#end
Stuff that goes into AppDelegate.m:
#import "AppDelegate.h"
#import <QuartzCore/QuartzCore.h>
#class LayerDelegate;
#interface LayerView : UIView
#property (nonatomic, retain) LayerDelegate* layerDelegate;
#end
#interface LayerDelegate : NSObject
#property(nonatomic, retain) CALayer* layer;
#property (nonatomic, assign) CGFloat zoomScale;
#end
static CGFloat kMinimumZoomScale = 1.0;
static CGFloat kMaximumZoomScale = 5.0;
#implementation AppDelegate
- (void) dealloc
{
self.window = nil;
self.layerView = nil;
[super dealloc];
}
- (BOOL) application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
[UIApplication sharedApplication].statusBarHidden = YES;
self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
self.window.backgroundColor = [UIColor whiteColor];
UIScrollView* scrollView = [[[UIScrollView alloc] initWithFrame:self.window.bounds] autorelease];
[self.window addSubview:scrollView];
scrollView.contentSize = scrollView.bounds.size;
scrollView.delegate = self;
scrollView.minimumZoomScale = kMinimumZoomScale;
scrollView.maximumZoomScale = kMaximumZoomScale;
scrollView.zoomScale = 1.0f;
scrollView.bouncesZoom = NO;
self.layerView = [[[LayerView alloc] initWithFrame:scrollView.bounds] autorelease];
[scrollView addSubview:self.layerView];
[self.window makeKeyAndVisible];
return YES;
}
- (UIView*) viewForZoomingInScrollView:(UIScrollView*)scrollView
{
return self.layerView;
}
- (void) scrollViewDidEndZooming:(UIScrollView*)scrollView withView:(UIView*)view atScale:(float)scale
{
CGPoint contentOffset = scrollView.contentOffset;
CGSize contentSize = scrollView.contentSize;
scrollView.maximumZoomScale = scrollView.maximumZoomScale / scale;
scrollView.minimumZoomScale = scrollView.minimumZoomScale / scale;
// Big change here: This resets the scroll view's contentSize and
// contentOffset, and also the LayerView's frame, bounds and transform
// properties
scrollView.zoomScale = 1.0f;
CGFloat newZoomScale = self.layerView.layerDelegate.zoomScale * scale;
self.layerView.layerDelegate.zoomScale = newZoomScale;
self.layerView.frame = CGRectMake(0, 0, contentSize.width, contentSize.height);
scrollView.contentSize = contentSize;
[scrollView setContentOffset:contentOffset animated:NO];
[self.layerView setNeedsDisplay];
}
#end
#implementation LayerView
- (id) initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.layerDelegate = [[[LayerDelegate alloc] init] autorelease];
[self.layer addSublayer:self.layerDelegate.layer];
// super's initWithFrame already invoked setNeedsDisplay, but we need to
// repeat because at that time our layerDelegate property was still empty
[self setNeedsDisplay];
}
return self;
}
- (void) dealloc
{
self.layerDelegate = nil;
[super dealloc];
}
- (void) setNeedsDisplay
{
[super setNeedsDisplay];
// Zooming changes the view's frame, but not the frame of the layer
self.layerDelegate.layer.frame = self.bounds;
[self.layerDelegate.layer setNeedsDisplay];
}
#end
#implementation LayerDelegate
- (id) init
{
self = [super init];
if (self)
{
self.layer = [CALayer layer];
self.layer.delegate = self;
self.zoomScale = 1.0f;
}
return self;
}
- (void) dealloc
{
self.layer = nil;
[super dealloc];
}
- (void) drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
CGRect layerRect = self.layer.bounds;
CGFloat radius = 25 * self.zoomScale;
CGFloat centerDistanceFromEdge = 5 * self.zoomScale + radius;
CGPoint topLeftCenter = CGPointMake(CGRectGetMinX(layerRect) + centerDistanceFromEdge,
CGRectGetMinY(layerRect) + centerDistanceFromEdge);
[self drawCircleWithCenter:topLeftCenter radius:radius fillColor:[UIColor redColor] inContext:context];
CGPoint layerCenter = CGPointMake(CGRectGetMidX(layerRect), CGRectGetMidY(layerRect));
[self drawCircleWithCenter:layerCenter radius:radius fillColor:[UIColor greenColor] inContext:context];
CGPoint bottomRightCenter = CGPointMake(CGRectGetMaxX(layerRect) - centerDistanceFromEdge,
CGRectGetMaxY(layerRect) - centerDistanceFromEdge);
[self drawCircleWithCenter:bottomRightCenter radius:radius fillColor:[UIColor blueColor] inContext:context];
}
- (void) drawCircleWithCenter:(CGPoint)center
radius:(CGFloat)radius
fillColor:(UIColor*)color
inContext:(CGContextRef)context
{
const int startRadius = [self radians:0];
const int endRadius = [self radians:360];
const int clockwise = 0;
CGContextAddArc(context, center.x, center.y, radius,
startRadius, endRadius, clockwise);
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillPath(context);
}
- (double) radians:(double)degrees
{
return degrees * M_PI / 180;
}
#end
Based on your sample project, the key is that you're manipulating a CALayer directly. By default, setting CALayer properties, such as frame, cause animations. The suggestion to use [UIView setAnimationsEnabled:NO] was on the right track, but only affects UIView-based animations. If you do the CALayer equivalent, say in your setNeedsDisplay: method:
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.layerDelegate.layer.frame = self.bounds;
[CATransaction commit];
It prevents the implicit frame-changing animation and looks right to me. You can also disable these implicit animations via a CALayerDelegate method in your LayerDelegate class:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
return (id)[NSNull null]; // NSNull means "don't do any implicit animations"
}
Original Suggestions:
Perhaps you are in an animation block without knowing it? Or, perhaps one of the methods you're calling is setting up an animation block? What if you [UIView setAnimationsEnabled:NO] before your code and re-enable them after?
If it's not an animation, then it's probably as you suspect; two view updates of some kind. (Perhaps one from the scroll view, and one from your code somehow?) Some runnable sample code would be great in that case.
(Out of curiosity, have you tried using CALayer's shouldRasterize and rasterizationScale rather than faking out the zoom level?)
In the X Code user interface builder there's a Bounce setting (it's under Scroll View).