Upgrade to Pro — share decks privately, control downloads, hide ads and more …

To drawRect or not to drawRect?

sammyd
March 29, 2014

To drawRect or not to drawRect?

All too often online advice for constructing beautiful UI on iOS starts out by suggesting that you create a custom UIView subclass and override drawRect, but is this the best approach? What alternatives exist? How should I choose between them.

This talk will involve a high-level look at the different graphical processing options on iOS, including CoreGraphics, CoreAnimation, UIView and OpenGL. Then we'll take a look at the following questions, with plenty of examples: - what does drawRect actually do? - when is drawRect slow? - what are all these CALayer subclasses? - when should I be choosing each of the options?

By the end of the talk we'll have covered some specific examples of how to choose each of the drawing technologies, and will hopefully have persuaded you that drawRect isn't always the best choice.

sammyd

March 29, 2014
Tweet

More Decks by sammyd

Other Decks in Technology

Transcript

  1. How do the bits fit together? UIView Hierarchy Graphics Hardware

    via ! CALayer rotate 30° opacity 60% backed by
  2. How do the bits fit together? UIView Hierarchy Graphics Hardware

    via ! CALayer rotate 30° opacity 60% backed by
  3. How do the bits fit together? UIView Hierarchy Graphics Hardware

    via ! CALayer rotate 30° opacity 60% backed by drawRect:
  4. So what actually is drawRect? ˒ UIView backed by CALayer

    ˒ Implementation of CALayerDelegate method drawLayer:inContext: ˒ Informal protocol ˒ Use to provide layer content and arrange sublayers
  5. SCArrowView protocol @protocol SCArrowView <NSObject>
 
 - (instancetype)initWithFrame:(CGRect)frame
 from:(CGPoint)from
 to:(CGPoint)to;

    @property (nonatomic, assign) CGPoint from;
 @property (nonatomic, assign) CGPoint to;
 @property (nonatomic, assign) CGFloat headSize;
 @property (nonatomic, assign) CGFloat lineThickness;
 @property (nonatomic, assign) CGFloat bendiness;
 @property (nonatomic, strong) UIColor *color; - (void)redrawArrow; @end
  6. ˒UIView = blank canvas ˒Various subclasses for creating content ˒UIImageView

    displays images ˒Highly optimised for this purpose UIImageView
  7. UIImageView Arrow if(! self.imageView && self.image) {
 self.imageView = [[UIImageView

    alloc] initWithImage:self.image];
 [self addSubView:self.imageView];
 } from to
  8. UIImageView Arrow CGPoint ivCentre = CGPointZero;
 ivCentre.x = (self.from.x +

    self.to.x) / 2.0;
 ivCentre.y = (self.from.y + self.to.y) / 2.0;
 self.imageView.center = ivCentre; from to
  9. UIImageView Arrow CGFloat arrowLength = sqrt(pow((self.to.y - self.from.y),2) +
 pow((self.to.x

    - self.from.x),2) );
 CGRect arrowBounds = self.imageView.bounds;
 arrowBounds.size.width = arrowLength;
 self.imageView.bounds = arrowBounds; from to
  10. UIImageView Arrow from to CGFloat arrowAngle = atan2((self.to.y - self.from.y),


    (self.to.x - self.from.x));
 self.arrowImageView.transform =
 CGAffineTransformMakeRotation(arrowAngle);
  11. UIImageView Arrow from to UIImage *preColoredArrow = [self preColoredArrowImage];
 self.tintColor

    = self.color;
 self.image = [preColoredArrow
 imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
 self.arrowImageView.image = self.image;
  12. - addLineToPoint: ! + bezierPathWithOvalInRect:
 - addArcWithCenter:radius:startAngle:
 endAngle:
 clockwise: !

    - addCurveToPoint:controlPoint1:
 controlPoint2:
 - addQuadCurveToPoint:controlPoint: UIBezierPath
  13. CoreGraphics Arrow CGContextMoveToPoint(cxt, start.x, start.y); CGContextAddQuadCurveToPoint(cxt, control.x, control.y, end.x, end.y);

    CGContextMoveToPoint(cxt, arrowSide1.x, arrowSide1.y); CGContextRef cxt = UIGraphicsGetCurrentContext();
  14. CoreGraphics Arrow CGContextMoveToPoint(cxt, start.x, start.y); CGContextAddQuadCurveToPoint(cxt, control.x, control.y, end.x, end.y);

    CGContextMoveToPoint(cxt, arrowSide1.x, arrowSide1.y); CGContextAddLineToPoint(cxt, end.x, end.y); CGContextRef cxt = UIGraphicsGetCurrentContext();
  15. CoreGraphics Arrow CGContextMoveToPoint(cxt, start.x, start.y); CGContextAddQuadCurveToPoint(cxt, control.x, control.y, end.x, end.y);

    CGContextMoveToPoint(cxt, arrowSide1.x, arrowSide1.y); CGContextAddLineToPoint(cxt, arrowSide2.x, arrowSide2.y); CGContextAddLineToPoint(cxt, end.x, end.y); CGContextRef cxt = UIGraphicsGetCurrentContext();
  16. CoreGraphics Arrow CGContextMoveToPoint(cxt, start.x, start.y); CGContextAddQuadCurveToPoint(cxt, control.x, control.y, end.x, end.y);

    CGContextMoveToPoint(cxt, arrowSide1.x, arrowSide1.y); CGContextAddLineToPoint(cxt, arrowSide2.x, arrowSide2.y); CGContextAddLineToPoint(cxt, end.x, end.y); CGContextStrokePath(cxt); CGContextRef cxt = UIGraphicsGetCurrentContext();
  17. Adding animation - (void)animationUpdate:(CADisplayLink *)sender
 {
 self.animating = YES
 if(!self.timer)

    {
 self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(animationUpdate:)];
 [self.timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
 self.animationStartTime = CACurrentMediaTime();
 } 
 CGFloat propComplete = (CACurrentMediaTime() - self.animationStartTime) / self.animationTime;
 if(propComplete >= 1) {
 self.currentEnd = self.to;
 [self.timer invalidate];
 self.timer = nil;
 } else {
 self.currentEnd = CGPointMake((self.to.x - self.from.x) * propComplete + self.from.x,
 (self.to.y - self.from.y) * propComplete + self.from.y);
 } 
 [self setNeedsDisplay];
 }
  18. Adding animation - (void)animationUpdate:(CADisplayLink *)sender
 {
 self.animating = YES
 if(!self.timer)

    {
 self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(animationUpdate:)];
 [self.timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
 self.animationStartTime = CACurrentMediaTime();
 } 
 CGFloat propComplete = (CACurrentMediaTime() - self.animationStartTime) / self.animationTime;
 if(propComplete >= 1) {
 self.currentEnd = self.to;
 [self.timer invalidate];
 self.timer = nil;
 } else {
 self.currentEnd = CGPointMake((self.to.x - self.from.x) * propComplete + self.from.x,
 (self.to.y - self.from.y) * propComplete + self.from.y);
 } 
 [self setNeedsDisplay];
 }
  19. Adding animation - (void)animationUpdate:(CADisplayLink *)sender
 {
 self.animating = YES
 if(!self.timer)

    {
 self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(animationUpdate:)];
 [self.timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
 self.animationStartTime = CACurrentMediaTime();
 } 
 CGFloat propComplete = (CACurrentMediaTime() - self.animationStartTime) / self.animationTime;
 if(propComplete >= 1) {
 self.currentEnd = self.to;
 [self.timer invalidate];
 self.timer = nil;
 } else {
 self.currentEnd = CGPointMake((self.to.x - self.from.x) * propComplete + self.from.x,
 (self.to.y - self.from.y) * propComplete + self.from.y);
 } 
 [self setNeedsDisplay];
 }
  20. Adding animation - (void)animationUpdate:(CADisplayLink *)sender
 {
 self.animating = YES
 if(!self.timer)

    {
 self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(animationUpdate:)];
 [self.timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
 self.animationStartTime = CACurrentMediaTime();
 } 
 CGFloat propComplete = (CACurrentMediaTime() - self.animationStartTime) / self.animationTime;
 if(propComplete >= 1) {
 self.currentEnd = self.to;
 [self.timer invalidate];
 self.timer = nil;
 } else {
 self.currentEnd = CGPointMake((self.to.x - self.from.x) * propComplete + self.from.x,
 (self.to.y - self.from.y) * propComplete + self.from.y);
 } 
 [self setNeedsDisplay];
 }
  21. Adding animation - (void)animationUpdate:(CADisplayLink *)sender
 {
 self.animating = YES
 if(!self.timer)

    {
 self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(animationUpdate:)];
 [self.timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
 self.animationStartTime = CACurrentMediaTime();
 } 
 CGFloat propComplete = (CACurrentMediaTime() - self.animationStartTime) / self.animationTime;
 if(propComplete >= 1) {
 self.currentEnd = self.to;
 [self.timer invalidate];
 self.timer = nil;
 } else {
 self.currentEnd = CGPointMake((self.to.x - self.from.x) * propComplete + self.from.x,
 (self.to.y - self.from.y) * propComplete + self.from.y);
 } 
 [self setNeedsDisplay];
 }
  22. Adding animation - (void)animationUpdate:(CADisplayLink *)sender
 {
 self.animating = YES
 if(!self.timer)

    {
 self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(animationUpdate:)];
 [self.timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
 self.animationStartTime = CACurrentMediaTime();
 } 
 CGFloat propComplete = (CACurrentMediaTime() - self.animationStartTime) / self.animationTime;
 if(propComplete >= 1) {
 self.currentEnd = self.to;
 [self.timer invalidate];
 self.timer = nil;
 } else {
 self.currentEnd = CGPointMake((self.to.x - self.from.x) * propComplete + self.from.x,
 (self.to.y - self.from.y) * propComplete + self.from.y);
 } 
 [self setNeedsDisplay];
 }
  23. Presentation Tree Render Tree Application Framework ˒ CALayer is the

    fundamental object ˒ Exists in a hierarchy ˒ 3 distinct hierarchies Model Tree CoreAnimation
  24. Providing Content to CALayer ˒ contents property ˒ drawLayer:inContext: method

    on CALayerDelegate ˒ Subclass and override drawing methods - drawInContext: or display ˒ Specialist subclasses of CALayer
  25. Creating Arrow Path self.arrowPathLayer = [CAShapeLayer layer];
 [self.layer addSublayer:self.arrowPathLayer]; self.arrowPathLayer.bounds

    = self.bounds;
 self.arrowPathLayer.position = CGPointMake(CGRectGetMidX(self.bounds),
 CGRectGetMidY(self.bounds)); self.arrowPathLayer.strokeColor = self.color.CGColor;
 self.arrowPathLayer.lineWidth = self.lineThickness; UIBezierPath *path = [self.arrowPath arrowBezierPath];
 self.arrowPathLayer.path = path.CGPath;
  26. Creating Arrow Head self.arrowHeadLayer = [CAShapeLayer layer];
 [self.layer addSublayer:self.arrowHeadLayer]; self.arrowHeadLayer.bounds

    = self.bounds;
 self.arrowHeadLayer.position = CGPointMake(CGRectGetMidX(self.bounds),
 CGRectGetMidY(self.bounds)); self.arrowHeadLayer.strokeColor = self.color.CGColor;
 self.arrowHeadLayer.lineWidth = self.lineThickness; UIBezierPath *headPath = [self arrowHeadBezierPath]; self.arrowHeadLayer.path = headPath.CGPath;
  27. CAAnimation CALayer Implicit ! backgroundColor borderColor borderWidth bounds cornerRadius mask

    opacity position transform … CAGradientLayer ! colors locations startPoint endPoint CAShapeLayer ! lineDashPhase fillColor path* strokeStart strokeEnd … CATextLayer ! fontSize* foregroundColor* CAReplicatorLayer ! instanceAlphaOffset instanceColor instanceDelay …
  28. Stroke Animation 0 1 CABasicAnimation *animation = [CABasicAnimation
 animationWithKeyPath:@"strokeEnd"];
 animation.duration

    = 2;
 animation.fromValue = @0;
 animation.toValue = @1;
 animation.timingFunction = [CAMediaTimingFunction
 functionWithName:kCAMediaTimingFunctionEaseIn];
  29. Stroke Animation 0 1 CABasicAnimation *animation = [CABasicAnimation
 animationWithKeyPath:@"strokeEnd"];
 animation.duration

    = 2;
 animation.fromValue = @0;
 animation.toValue = @1;
 animation.timingFunction = [CAMediaTimingFunction
 functionWithName:kCAMediaTimingFunctionEaseIn];
  30. Arrow Head Animation CABasicAnimation *headAnimationLeft = [CABasicAnimation
 animationWithKeyPath:@"strokeEnd"];
 headAnimationLeft.duration =

    1.0;
 headAnimationLeft.fromValue = @0.5;
 headAnimationLeft.toValue = @1;
 headAnimationLeft.beginTime = 2;
 headAnimationLeft.timingFunction = [CAMediaTimingFunction
 functionWithName:kCAMediaTimingFunctionEaseOut]; 1 0.5 0 1 0.5 0
  31. Arrow Head Animation CABasicAnimation *headAnimationLeft = [CABasicAnimation
 animationWithKeyPath:@"strokeEnd"];
 headAnimationLeft.duration =

    1.0;
 headAnimationLeft.fromValue = @0.5;
 headAnimationLeft.toValue = @1;
 headAnimationLeft.beginTime = 2;
 headAnimationLeft.timingFunction = [CAMediaTimingFunction
 functionWithName:kCAMediaTimingFunctionEaseOut]; 1 0.5 0 1 0.5 0
  32. Arrow Head Animation CABasicAnimation *headAnimationLeft = [CABasicAnimation
 animationWithKeyPath:@"strokeEnd"];
 headAnimationLeft.duration =

    1.0;
 headAnimationLeft.fromValue = @0.5;
 headAnimationLeft.toValue = @1;
 headAnimationLeft.beginTime = 2;
 headAnimationLeft.timingFunction = [CAMediaTimingFunction
 functionWithName:kCAMediaTimingFunctionEaseOut]; CABasicAnimation *headAnimationRight = [CABasicAnimation
 animationWithKeyPath:@"strokeStart"];
 headAnimationRight.duration = 1.0;
 headAnimationRight.fromValue = @0.5;
 headAnimationRight.toValue = @0;
 headAnimationRight.beginTime = 2;
 headAnimationRight.timingFunction = [CAMediaTimingFunction
 functionWithName:kCAMediaTimingFunctionEaseOut]; 1 0.5 0 1 0.5 0
  33. Arrow Head Animation CABasicAnimation *headAnimationLeft = [CABasicAnimation
 animationWithKeyPath:@"strokeEnd"];
 headAnimationLeft.duration =

    1.0;
 headAnimationLeft.fromValue = @0.5;
 headAnimationLeft.toValue = @1;
 headAnimationLeft.beginTime = 2;
 headAnimationLeft.timingFunction = [CAMediaTimingFunction
 functionWithName:kCAMediaTimingFunctionEaseOut]; CABasicAnimation *headAnimationRight = [CABasicAnimation
 animationWithKeyPath:@"strokeStart"];
 headAnimationRight.duration = 1.0;
 headAnimationRight.fromValue = @0.5;
 headAnimationRight.toValue = @0;
 headAnimationRight.beginTime = 2;
 headAnimationRight.timingFunction = [CAMediaTimingFunction
 functionWithName:kCAMediaTimingFunctionEaseOut]; 1 0.5 0 1 0.5 0
  34. Arrow Head Animation CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
 animationGroup.duration =

    3;
 animationGroup.animations = @[headHidden, headAnimationRight,
 headAnimationLeft]; [self.arrowPathLayer addAnimation:animation
 forKey:@"SCArrowDrawAnimation"];
 [self.arrowHeadLayer addAnimation:animationGroup
 forKey:@"SCArrowDrawHeadAnimation"];
  35. Arrow Head Animation CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
 animationGroup.duration =

    3;
 animationGroup.animations = @[headHidden, headAnimationRight,
 headAnimationLeft]; [self.arrowPathLayer addAnimation:animation
 forKey:@"SCArrowDrawAnimation"];
 [self.arrowHeadLayer addAnimation:animationGroup
 forKey:@"SCArrowDrawHeadAnimation"];
  36. Arrow Head Animation CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
 animationGroup.duration =

    3;
 animationGroup.animations = @[headHidden, headAnimationRight,
 headAnimationLeft]; [self.arrowPathLayer addAnimation:animation
 forKey:@"SCArrowDrawAnimation"];
 [self.arrowHeadLayer addAnimation:animationGroup
 forKey:@"SCArrowDrawHeadAnimation"];
  37. Arrow Head Animation CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
 animationGroup.duration =

    3;
 animationGroup.animations = @[headHidden, headAnimationRight,
 headAnimationLeft]; [self.arrowPathLayer addAnimation:animation
 forKey:@"SCArrowDrawAnimation"];
 [self.arrowHeadLayer addAnimation:animationGroup
 forKey:@"SCArrowDrawHeadAnimation"];
  38. Arrow Head Animation CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
 animationGroup.duration =

    3;
 animationGroup.animations = @[headHidden, headAnimationRight,
 headAnimationLeft]; [self.arrowPathLayer addAnimation:animation
 forKey:@"SCArrowDrawAnimation"];
 [self.arrowHeadLayer addAnimation:animationGroup
 forKey:@"SCArrowDrawHeadAnimation"];
  39. ˒ UIImageView is mega simple ˒ Use UIBezierPath to draw

    shapes ˒ Don’t try and animate with drawRect ˒ Discover the CALayer subclasses ˒ CALayer offers software design advantages ˒ CALayer when interaction not required in summary