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

The Art of Custom UI Controls

sammyd
September 10, 2013

The Art of Custom UI Controls

A brief foray into the world of creating custom UI controls for iOS. These are the slides which accompany the talk I presented at 360iDev 2013.

sammyd

September 10, 2013
Tweet

More Decks by sammyd

Other Decks in Programming

Transcript

  1. flickr/charliematters what are how to create a good design considerations

    designing other ui controls API software user experience concerns ? where shall we go?
  2. collect use cases flickr/charliematters What do they want? whom for

    how will they use it? are we doing it? will we do? why do they want it?
  3. hard to change world visible /** Current volume level. In

    decibels */ @property (nonatomic, assign) CGFloat volume; /** Minimum volume level. In decibels */ @property (nonatomic, assign) CGFloat minVolume; /** Maximum volume level. In decibels */ @property (nonatomic, assign) CGFloat maxVolume;
  4. hard to change world visible /** Current volume level. In

    decibels */ @property (nonatomic, assign) CGFloat volume; /** Minimum volume level. In decibels */ @property (nonatomic, assign) CGFloat minVolume; /** Maximum volume level. In decibels */ @property (nonatomic, assign) CGFloat maxVolume; /** Current opacity level. */ @property (nonatomic, assign) CGFloat opacity; /** Minimum opacity level. */ @property (nonatomic, assign) CGFloat minOpacity; /** Maximum opacity level. */ @property (nonatomic, assign) CGFloat maxOpacity;
  5. Your API is hard to change world visible testable documentation

    - (void)test_valueShouldBeClippedToBounds { ... } - (void)test_valueShouldBeClippedWhenNewExtremaProvided { ... }
  6. testable documentation #pragma mark - Knob Appearance /** Specifies the

    angle of the start of the knob control track. Defaults to -11π/8. */ @property (nonatomic, assign) CGFloat startAngle; /** Specifies the end angle of the knob control track. Defaults to 3π/8. */ @property (nonatomic, assign) CGFloat endAngle; /** Specifies the width in points of the knob control track. Defaults to 2.0. */ @property (nonatomic, assign) CGFloat lineWidth; /** Specifies the length in points of the pointer on the knob. Defaults to 6.0. */ @property (nonatomic, assign) CGFloat pointerLength; @property (nonatomic, assign) CGFloat startAngle; @property (nonatomic, assign) CGFloat endAngle; @property (nonatomic, assign) CGFloat lineWidth; @property (nonatomic, assign) CGFloat pointerLength;
  7. hard to change world visible testable documentation /** Contains the

    current value Setting this value will redraw the knob with the correct specified value. To animate to the new value use `setValue:animated:` method instead. If you set the value outside of the allowed range then it will be clipped to the appropriate extremum. */ @property (nonatomic, assign) CGFloat value;
  8. Your API should be discussed & iterated @property (nonatomic, assign)

    CGFloat startAngle; @property (nonatomic, assign) CGFloat endAngle; @property (nonatomic, assign) CGFloat lineWidth; @property (nonatomic, assign) CGFloat pointerLength; @property (nonatomic, assign) CGPoint startPosition; @property (nonatomic, assign) CGPoint endPosition; @property (nonatomic, assign) CGFloat innerRadius; @property (nonatomic, assign) CGFloat outerRadius; @property (nonatomic, assign) CGFloat pointerLength; created early minimal
  9. minimal @property (nonatomic, assign) CGFloat value; @property (nonatomic, assign) CGFloat

    minimumValue; @property (nonatomic, assign) CGFloat maximumValue; @property (nonatomic, assign) CGFloat stepSize; @property (nonatomic, assign) BOOL allowOutOfBounds; - (void)setPathForTrack:(UIBezierPath *)path; - (void)setShapeForPointer:(UIBezierPath *)path; @property (nonatomic, retain) UIColor *trackColor; @property (nonatomic, retain) UIColor *pointerColor; @property (nonatomic, retain) UIColor *lowTrackColor; @property (nonatomic, retain) UIColor *highTrackColor; @property (nonatomic, assign) BOOL blendTrackColors; @property (nonatomic, retain) UIColor *innerCircleCentralGradientColor; @property (nonatomic, retain) UIColor *innerCircleBoundaryGradientColor; @property (nonatomic, assign) BOOL showInnerCircleGradient; @property (nonatomic, retain) UIColor *textColor; @property (nonatomic, retain) UIFont *font;
  10. Your API should be minimal @property (nonatomic, assign) CGFloat value;

    @property (nonatomic, assign) CGFloat minimumValue; @property (nonatomic, assign) CGFloat maximumValue; discussed & iterated created early
  11. minimal #pragma mark - Knob Appearance /** Specifies the angle

    of the start of the knob control track. Defaults to -11π/8. */ @property (nonatomic, assign) CGFloat startAngle; /** Specifies the end angle of the knob control track. Defaults to 3π/8. */ @property (nonatomic, assign) CGFloat endAngle; /** Specifies the width in points of the knob control track. Defaults to 2.0. */ @property (nonatomic, assign) CGFloat lineWidth; /** Specifies the length in points of the pointer on the knob. Defaults to 6.0. */ @property (nonatomic, assign) CGFloat pointerLength;
  12. Your API should be @property (nonatomic, assign) CGFloat value; -

    (void)setValue:(CGFloat)value animated:(BOOL)animated; @property (nonatomic, assign) CGFloat minimumValue; @property (nonatomic, assign) CGFloat maximumValue; @property (nonatomic, assign, getter = isContinuous) BOOL continuous; discussed & iterated created early minimal platform friendly
  13. interaction pattern appropriate subclassing fl ickr/wscullin - (void)createKnobControl { ...

    [_knobControl addTarget:self action:@selector(knobValueChanged:) forControlEvents:UIControlEventValueChanged]; } - (void)knobValueChanged:(id)sender { // Handle the new value } [self sendActionsForControlEvents:UIControlEventValueChanged]; target-action Control View controller
  14. interaction pattern appropriate subclassing fl ickr/wscullin extremely simple integral part

    of UIControl multiple targets handled target-action limited events additional methods
  15. interaction pattern appropriate subclassing fl ickr/wscullin [_knobControl addObserver:self forKeyPath:@"value" options:0

    context:NULL]; - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { // Demos KVO binding with the knob control if(object == _knobControl && [keyPath isEqualToString:@"value"]) { // Handle the changed value } } target-action KVO View controller View controller
  16. interaction pattern appropriate subclassing fl ickr/wscullin target-action KVO integral part

    of NSObject multiple targets handled don’t have to alter control
  17. interaction pattern appropriate subclassing fl ickr/wscullin + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {

    if ([key isEqualToString:@"value"]) { return NO; } else { return [super automaticallyNotifiesObserversForKey:key]; } } - (void)setValue:(CGFloat)value { // Chain with the animation method version [self setValue:value animated:NO]; } target-action KVO Control Control
  18. interaction pattern appropriate subclassing fl ickr/wscullin - (void)setValue:(CGFloat)value animated:(BOOL)animated {

    if(value != _value) { // Send KVO notification [self willChangeValueForKey:@"value"]; // Save the value to the backing ivar // Make sure we limit it to the requested bounds _value = [self clipToBounds:value]; // Update the UI here ... [self didChangeValueForKey:@"value"]; } } target-action KVO Control
  19. interaction pattern appropriate subclassing fl ickr/wscullin target-action KVO integral part

    of NSObject multiple targets handled don’t have to alter control... only suitable for value changes all routed through one method ...unless advanced behavior
  20. interaction pattern appropriate subclassing fl ickr/wscullin @property (nonatomic,copy) void(^valueChangeHandler)(double value);

    if (self.selectionHandler != NULL) { self.valueChangeHandler(self.value); } - (void)createKnobControl{ ... _knobControl.valueChangeHandler = ^(double value) { // Value is the newly selected value } } target-action KVO command Control Control View controller
  21. interaction pattern appropriate subclassing fl ickr/wscullin target-action KVO command fewer

    methods complete customization multiple handlers hard potential for retain cycles syntax fun
  22. interaction pattern appropriate subclassing fl ickr/wscullin [self.delegate knob:self didChangeValue:self.value]; @interface

    ViewController <SKKnobControlDelegate> - (void)createKnobControl { ... _knobControl.delegate = self; } - (void)knob:(SKKnobControl*)knob didChangeValue:(CGFloat)value { // Handle the new value } target-action KVO command delegation Control View controller @protocol SKKnobControlDelegate <NSObject> - (void)knob:(SKKnobControl *)knob didChangeValue:(CGFloat)value; @end Delegate Protocol
  23. interaction pattern appropriate subclassing fl ickr/wscullin completely customizable methods well-understood

    apple pattern useful for complex controls target-action KVO command delegation
  24. interaction pattern appropriate subclassing fl ickr/wscullin completely customizable methods well-understood

    apple pattern useful for complex controls target-action KVO command delegation multiple delegates problematic boiler plate delegate checking potential for overkill
  25. interaction pattern appropriate subclassing fl ickr/wscullin if (self.delegate && [self.delegate

    respondsToSelector: @selector(knob:didChangeValue:)] ) { [self.delegate knob:self didChangeValue:self.value]; } id< SKKnobControlDelegate > delegateProxy = (id< SKKnobControlDelegate >)[[SDDelegateProxy alloc] initWithDelegate:self.delegate]; [delegateProxy knob:self didChangeValue:self.value]; SDDelegateProxy target-action KVO command delegation
  26. interaction pattern appropriate subclassing fl ickr/wscullin completely customizable methods well-understood

    apple pattern useful for complex controls target-action KVO command delegation multiple delegates problematic boiler plate delegate checking potential for overkill
  27. interaction pattern appropriate subclassing fl ickr/wscullin target-action KVO command delegation

    SK Knob Control update label text link UISlider with SKKnobControl random value button press
  28. interaction pattern separation of concerns fl ickr/wscullin \ API Appearance

    Interaction Logic @interface SKAnnulusSegmentLayerRenderer : NSObject #pragma mark - Properties associated with the background annulus @property (nonatomic, readonly, strong) CALayer *annulusLayer; ... #pragma mark - Animation Control Updates - (void)setPointerAngle:(CGFloat)pointerAngle animated:(BOOL)animated; @end
  29. interaction pattern separation of concerns fl ickr/wscullin \ API Appearance

    Interaction Logic @interface SKKnobGestureRecognizer : UIPanGestureRecognizer ... @end
  30. fl ickr/wscullin UIView Core Graphics CALayer Open GLES - (void)drawRect:(CGRect)rect

    { CGContextRef context = UIGraphicsGetCurrentContext(); UIBezierPath *ring = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:self.startAngle endAngle:self.endAngle clockwise:YES]; ... CGContextAddPath(context, ring.CGPath); CGContextStrokePath(context); ... UIBezierPath *pointer = [UIBezierPath bezierPath]; [pointer moveToPoint:CGPointMake(CGRectGetWidth(self.pointerLayer.bounds) - self.pointerLength - self.annulusLineWidth/2.f, CGRectGetHeight(self.pointerLayer.bounds) / 2.f)]; [pointer addLineToPoint:CGPointMake(CGRectGetWidth(self.pointerLayer.bounds), CGRectGetHeight(self.pointerLayer.bounds) / 2.f)]; ... CGContextAddPath(context, pointer.CGPath); CGContextStrokePath(context); }
  31. fl ickr/wscullin UIView Core Graphics CALayer Open GLES - (void)drawRect:(CGRect)rect

    { CGContextRef context = UIGraphicsGetCurrentContext(); UIBezierPath *ring = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:self.startAngle endAngle:self.endAngle clockwise:YES]; ... CGContextAddPath(context, ring.CGPath); CGContextStrokePath(context); ... UIBezierPath *pointer = [UIBezierPath bezierPath]; [pointer moveToPoint:CGPointMake(CGRectGetWidth(self.pointerLayer.bounds) - self.pointerLength - self.annulusLineWidth/2.f, CGRectGetHeight(self.pointerLayer.bounds) / 2.f)]; [pointer addLineToPoint:CGPointMake(CGRectGetWidth(self.pointerLayer.bounds), CGRectGetHeight(self.pointerLayer.bounds) / 2.f)]; ... CGContextAddPath(context, pointer.CGPath); CGContextStrokePath(context); }
  32. fl ickr/wscullin UIView Core Graphics CALayer Open GLES - (void)drawRect:(CGRect)rect

    { CGContextRef context = UIGraphicsGetCurrentContext(); UIBezierPath *ring = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:self.startAngle endAngle:self.endAngle clockwise:YES]; ... CGContextAddPath(context, ring.CGPath); CGContextStrokePath(context); ... UIBezierPath *pointer = [UIBezierPath bezierPath]; [pointer moveToPoint:CGPointMake(CGRectGetWidth(self.pointerLayer.bounds) - self.pointerLength - self.annulusLineWidth/2.f, CGRectGetHeight(self.pointerLayer.bounds) / 2.f)]; [pointer addLineToPoint:CGPointMake(CGRectGetWidth(self.pointerLayer.bounds), CGRectGetHeight(self.pointerLayer.bounds) / 2.f)]; ... CGContextAddPath(context, pointer.CGPath); CGContextStrokePath(context); }
  33. separation of concerns match the platform fl ickr/wscullin UIView Core

    Graphics CALayer Open GLES @floriankugler floriankugler.com
  34. separation of concerns match the platform fl ickr/wscullin _annulusLayer =

    [CALayer layer]; self.annulusLayer.delegate = self; - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { // Now have a CoreGraphics context in which to render } CGContextSaveGState(layerContext); { UIBezierPath *ring = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:self.startAngle endAngle:self.endAngle clockwise:YES]; ... CGContextAddPath(layerContext, ring.CGPath); CGContextStrokePath(layerContext); } CGContextRestoreGState(layerContext);
  35. separation of concerns match the platform fl ickr/wscullin - (void)setPointerAngle:(CGFloat)pointerAngle

    animated:(BOOL)animated { if(pointerAngle != _pointerAngle) { _pointerAngle = pointerAngle; [CATransaction begin]; if(animated) { [CATransaction setAnimationDuration:3.f]; } else { [CATransaction setDisableActions:YES]; } self.pointerLayer.transform = CATransform3DMakeRotation(pointerAngle, 0, 0, 1); [CATransaction commit]; } } - (void)setAnnulusColor:(UIColor *)annulusColor { if(annulusColor != _annulusColor) { _annulusColor = annulusColor; [self.annulusLayer setNeedsDisplay]; } }