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

The Art of Custom UI Controls

Ddd6d3bac7772fa67fc5e312a18bdaec?s=47 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.

Ddd6d3bac7772fa67fc5e312a18bdaec?s=128

sammyd

September 10, 2013
Tweet

Transcript

  1. The ART OF CUSTOM UI CONTROLS

  2. @iwantmyrealname /sammyd iwantmyreal.name

  3. @ShinobiControls /ShinobiControls shinobicontrols.com

  4. shinobicontrols shinobicontrols.com/360idev

  5. flickr/charliematters where shall we go?

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

    designing other ui controls API software user experience concerns ? where shall we go?
  7. What are UI Controls?

  8. What are UI Controls? Reusable components to aid

  9. What are UI Controls? Reusable components to aid designers

  10. What are UI Controls? Reusable components to aid designers developers

  11. What are UI Controls? Reusable components to aid designers developers

    users
  12. Why go custom?

  13. Why go custom? same flexibility

  14. Why go custom? same flexibility new Functionality

  15. Why go custom? same flexibility new Functionality

  16. Example Shinobi Knob Control

  17. Let me at the code...

  18. Let me at the code... hold ON

  19. UX Design visual design principles collect use cases flickr/charliematters

  20. 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?
  21. collect use cases flickr/charliematters iOS control circular version of UISlider

    play nice with iOS7 customizable appearance
  22. visual design principles Intensity HUE Orientation SIZE POSITION flickr/charliematters

  23. visual design principles flickr/charliematters

  24. visual design principles flickr/charliematters 34

  25. visual design principles flickr/charliematters 34 have to read text no

    idea of the range
  26. visual design principles flickr/charliematters 34 34

  27. visual design principles flickr/charliematters 34 34 still not sure of

    range
  28. visual design principles flickr/charliematters 34 34 34

  29. visual design principles flickr/charliematters 34 34 34 hue difference

  30. visual design principles flickr/charliematters 34 34 34 uses orientation difference

  31. Your API is hard to change world visible testable documentation

  32. Your API is hard to change world visible testable documentation

  33. 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;
  34. 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;
  35. Your API is hard to change world visible testable documentation

  36. Your API is hard to change world visible testable documentation

    - (void)test_valueShouldBeClippedToBounds { ... } - (void)test_valueShouldBeClippedWhenNewExtremaProvided { ... }
  37. Your API is hard to change world visible testable documentation

  38. 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;
  39. 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;
  40. Your API should be discussed & iterated created early minimal

    platform friendly
  41. 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
  42. Your API should be discussed & iterated created early minimal

    platform friendly
  43. Your API should be discussed & iterated created early minimal

    platform friendly
  44. 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;
  45. Your API should be minimal @property (nonatomic, assign) CGFloat value;

    @property (nonatomic, assign) CGFloat minimumValue; @property (nonatomic, assign) CGFloat maximumValue; discussed & iterated created early
  46. 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;
  47. Your API should be discussed & iterated created early minimal

    platform friendly
  48. 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
  49. Software Design interaction pattern appropriate subclassing separation of concerns match

    the platform fl ickr/wscullin
  50. Software Design interaction pattern appropriate subclassing separation of concerns match

    the platform fl ickr/wscullin
  51. appropriate subclassing fl ickr/wscullin Responds to events Forms responder chain

    UIResponder KnobControl
  52. appropriate subclassing fl ickr/wscullin Responds to events Forms responder chain

    UIResponder KnobControl Drawing?
  53. appropriate subclassing fl ickr/wscullin Describes UI drawing Gesture recognizers High-level

    animation UIResponder UIView KnobControl
  54. appropriate subclassing fl ickr/wscullin Describes UI drawing Gesture recognizers High-level

    animation UIResponder UIView KnobControl Interaction?
  55. appropriate subclassing fl ickr/wscullin UIResponder UIView KnobControl In-built target-action pattern

    iOS standard subclasses UIControl
  56. appropriate subclassing fl ickr/wscullin @interface SKKnobControl : UIControl ... @end

    UIResponder UIView KnobControl UIControl
  57. interaction pattern appropriate subclassing separation of concerns match the platform

    fl ickr/wscullin Software Design
  58. 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
  59. interaction pattern appropriate subclassing fl ickr/wscullin extremely simple integral part

    of UIControl multiple targets handled target-action
  60. interaction pattern appropriate subclassing fl ickr/wscullin extremely simple integral part

    of UIControl multiple targets handled target-action limited events additional methods
  61. 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
  62. interaction pattern appropriate subclassing fl ickr/wscullin target-action KVO integral part

    of NSObject multiple targets handled don’t have to alter control
  63. 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
  64. 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
  65. 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
  66. 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
  67. interaction pattern appropriate subclassing fl ickr/wscullin target-action KVO command fewer

    methods complete customization
  68. interaction pattern appropriate subclassing fl ickr/wscullin target-action KVO command fewer

    methods complete customization multiple handlers hard potential for retain cycles syntax fun
  69. 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
  70. interaction pattern appropriate subclassing fl ickr/wscullin completely customizable methods well-understood

    apple pattern useful for complex controls target-action KVO command delegation
  71. 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
  72. 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
  73. 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
  74. 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
  75. interaction pattern appropriate subclassing separation of concerns match the platform

    fl ickr/wscullin Software Design
  76. interaction pattern separation of concerns fl ickr/wscullin \ API Appearance

    Interaction Logic
  77. 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
  78. interaction pattern separation of concerns fl ickr/wscullin \ API Appearance

    Interaction Logic @interface SKKnobGestureRecognizer : UIPanGestureRecognizer ... @end
  79. interaction pattern separation of concerns fl ickr/wscullin \ API Appearance

    Interaction Logic
  80. interaction pattern appropriate subclassing separation of concerns match the platform

    fl ickr/wscullin Software Design
  81. separation of concerns match the platform fl ickr/wscullin UIView Core

    Graphics CALayer Open GLES
  82. separation of concerns match the platform fl ickr/wscullin UIView Core

    Graphics CALayer Open GLES
  83. 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); }
  84. 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); }
  85. 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); }
  86. separation of concerns match the platform fl ickr/wscullin UIView Core

    Graphics CALayer Open GLES
  87. separation of concerns match the platform fl ickr/wscullin UIView Core

    Graphics CALayer Open GLES
  88. separation of concerns match the platform fl ickr/wscullin UIView Core

    Graphics CALayer Open GLES
  89. separation of concerns match the platform fl ickr/wscullin UIView Core

    Graphics CALayer Open GLES
  90. separation of concerns match the platform fl ickr/wscullin UIView Core

    Graphics CALayer Open GLES
  91. separation of concerns match the platform fl ickr/wscullin UIView Core

    Graphics CALayer Open GLES
  92. separation of concerns match the platform fl ickr/wscullin UIView Core

    Graphics CALayer Open GLES
  93. separation of concerns match the platform fl ickr/wscullin UIView Core

    Graphics CALayer Open GLES @floriankugler floriankugler.com
  94. 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);
  95. 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]; } }
  96. Other concerns accessibility distribution localization legacy flickr/twicepix

  97. Other concerns accessibility distribution legacy flickr/twicepix localization

  98. Other concerns accessibility distribution legacy flickr/twicepix localization

  99. Other concerns accessibility distribution legacy flickr/twicepix localization

  100. SK Knob Control

  101. SK Knob Control sammyd / SKKnobControl

  102. Sounds like a lot of Work

  103. cocoapods

  104. cocoacontrols

  105. shinobicontrols

  106. GO Create! flickr/charliematters shinobicontrols.com/360idev @iwantmyrealname github/sammyd