iOS API Design

Preview:

Citation preview

iOS API Design

what’s the coolest thing about iOS development?

- I’m sure everyone has a different opinion on this

- Able to make delightful user interfaces- This is JVFloatLabeledTextField- Engrossing interfaces are at the core of the iPhone

- Able to make delightful user interfaces- This is JVFloatLabeledTextField- Engrossing interfaces are at the core of the iPhone

sharing is caring

- With dependency managers like Carthage and CocoaPods, it’s easy to create and share your designs

show you care with your API

- But just sharing your design isn’t enough- You have to make it easy for people to use it- But this reminds me of a great quote from Kent Beck…

“🙅”-Kent Beck

- Everything is a tradeoff. (paraphrasing here)- What makes a good API? There are very few clear answers, instead we have to evaluate tradeoffs

- What’s the API for JVFloatLabeledTextField?

- What’s the API for JVFloatLabeledTextField?

@interface JVFloatLabeledTextField : UITextField

@property (nonatomic, assign) CGFloat floatingLabelYPadding UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR;

@end

- Really nice, allows you to use UIAppearance

@interface JVFloatLabeledTextField : UITextField

@property (nonatomic, assign) CGFloat floatingLabelYPadding UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR;

@end

- Really nice, allows you to use UIAppearance

CGRect frame = CGRectMake(0.f, 0.f, 100.f, 44.f); JVFloatLabeledTextField *textField = [[JVFloatLabeledTextField alloc] initWithFrame:frame];

textField.placeholder = @"Price"; textField.floatingLabelYPadding = 10.f; textField.floatingLabelTextColor = [UIColor blueColor];

- Pretty simple to use- But it’s a specific class

CGRect frame = CGRectMake(0.f, 0.f, 100.f, 44.f); JVFloatLabeledTextField *textField = [[JVFloatLabeledTextField alloc] initWithFrame:frame];

textField.placeholder = @"Price"; textField.floatingLabelYPadding = 10.f; textField.floatingLabelTextColor = [UIColor blueColor];

- Pretty simple to use- But it’s a specific class

CGRect frame = CGRectMake(0.f, 0.f, 100.f, 44.f); JVFloatLabeledTextField *textField = [[JVFloatLabeledTextField alloc] initWithFrame:frame];

textField.placeholder = @"Price"; textField.floatingLabelYPadding = 10.f; textField.floatingLabelTextColor = [UIColor blueColor];

- Pretty simple to use- But it’s a specific class

subclasses force people to use your class exclusively

- This is limiting

- MHTextField provides a next/previous input accessory view and scrolling, but is also a subclass- Subclasses force us to choose one over the other

- MHTextField provides a next/previous input accessory view and scrolling, but is also a subclass- Subclasses force us to choose one over the other

@interface UITextField (JVFloatLabeledTextField)

@property (nonatomic, assign) CGFloat floatingLabelYPadding UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR;

@end

@interface JVFloatLabeledTextField : UITextField

- A category in ObjC, or an extension in Swift, allows people to use the implementation with *any* text field- But we can’t add properties on categories/extensions

@interface UITextField (JVFloatLabeledTextField)

@property (nonatomic, assign) CGFloat floatingLabelYPadding UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR;

@end

- A category in ObjC, or an extension in Swift, allows people to use the implementation with *any* text field- But we can’t add properties on categories/extensions

@interface UITextField (JVFloatLabeledTextField)

@property (nonatomic, assign) CGFloat floatingLabelYPadding UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR;

@end

- A category in ObjC, or an extension in Swift, allows people to use the implementation with *any* text field- But we can’t add properties on categories/extensions

@interface UITextField (JVFloatLabeledTextField) { CGFloat _floatingLabelYPadding; }

@property (nonatomic, assign) CGFloat floatingLabelYPadding UI_APPEARANCE_SELECTOR;

@end

- More precisely, defining a property automatically synthesizes an instance variable- Can’t do that on a category/extension

@interface UITextField (JVFloatLabeledTextField) { CGFloat _floatingLabelYPadding; }

@property (nonatomic, assign) CGFloat floatingLabelYPadding UI_APPEARANCE_SELECTOR;

@end

- More precisely, defining a property automatically synthesizes an instance variable- Can’t do that on a category/extension

const void * const YPaddingKey = &YPaddingKey;

- (void)setFloatingLabelYPadding:(CGFloat)yPadding { objc_setAssociatedObject( self, YPaddingKey, @(yPadding), OBJC_ASSOCIATION_RETAIN_NONATOMIC ); }

- (CGFloat)floatingLabelYPadding { return [objc_getAssociatedObject(self, YPaddingKey) floatValue]; }

- An ivar won’t be auto synthesized if you define custom setters and getters- We can dynamically tack on objects using the runtime

const void * const YPaddingKey = &YPaddingKey;

- (void)setFloatingLabelYPadding:(CGFloat)yPadding { objc_setAssociatedObject( self, YPaddingKey, @(yPadding), OBJC_ASSOCIATION_RETAIN_NONATOMIC ); }

- (CGFloat)floatingLabelYPadding { return [objc_getAssociatedObject(self, YPaddingKey) floatValue]; }

- An ivar won’t be auto synthesized if you define custom setters and getters- We can dynamically tack on objects using the runtime

const void * const YPaddingKey = &YPaddingKey;

- (void)setFloatingLabelYPadding:(CGFloat)yPadding { objc_setAssociatedObject( self, YPaddingKey, @(yPadding), OBJC_ASSOCIATION_RETAIN_NONATOMIC ); }

- (CGFloat)floatingLabelYPadding { return [objc_getAssociatedObject(self, YPaddingKey) floatValue]; }

- An ivar won’t be auto synthesized if you define custom setters and getters- We can dynamically tack on objects using the runtime

@interface UITextField (JVFloatLabeledTextField)

@property (nonatomic, assign) CGFloat floatingLabelYPadding UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR;

@end

- So we just do the same for every property, right?

@interface UITextField (JVFloatLabeledTextField)

@property (nonatomic, assign) CGFloat floatingLabelYPadding UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR;

@end

- So we just do the same for every property, right?

const void * const YPaddingKey = &YPaddingKey; const void * const FontKey = &FontKey; const void * const TextColorKey = &TextColorKey;

- (void)setFloatingLabelYPadding:(CGFloat)yPadding { objc_setAssociatedObject( self, YPaddingKey, @(yPadding), OBJC_ASSOCIATION_RETAIN_NONATOMIC ); }

- (CGFloat)floatingLabelYPadding { return [objc_getAssociatedObject(self, YPaddingKey) floatValue]; }

- Results in a ton of code for only 3 properties

}

- (UIFont *)floatingLabelFont { return objc_getAssociatedObject(self, FontKey); }

- (void)setFloatingLabelTextColor:(UIColor *)color { objc_setAssociatedObject( self, TextColorKey, color, OBJC_ASSOCIATION_RETAIN_NONATOMIC ); }

- (UIColor *)floatingLabelTextColor { return objc_getAssociatedObject(self, TextColorKey); }

- Results in a ton of code for only 3 properties

extension UITextField { var floatingLabelYPadding: CGFloat { set { objc_setAssociatedObject( self, YPaddingKey, newValue, UInt(OBJC_ASSOCIATION_ASSIGN) ) }

get { if let padding = objc_getAssociatedObject( self, YPaddingKey) as? CGFloat { return padding } else { return 0 }

- In Swift, it’s worse—notice we have to be explicit about the fact that the assoc object might not be set, or may not be the right type. - And so much code!

extension UITextField { var floatingLabelYPadding: CGFloat { set { objc_setAssociatedObject( self, YPaddingKey, newValue, UInt(OBJC_ASSOCIATION_ASSIGN) ) }

get { if let padding = objc_getAssociatedObject( self, YPaddingKey) as? CGFloat { return padding } else { return 0 }

- In Swift, it’s worse—notice we have to be explicit about the fact that the assoc object might not be set, or may not be the right type. - And so much code!

set { objc_setAssociatedObject( self, TextColorKey, newValue, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC) ) }

get { if let color = objc_getAssociatedObject( self, TextColorKey) as? UIColor { return color } else { return UIColor.blackColor() } } } }

- In Swift, it’s worse—notice we have to be explicit about the fact that the assoc object might not be set, or may not be the right type. - And so much code!

doesn’t scale

- Defining two methods for every new property is nuts- Too much boilerplate code, but also…

// objc4-532/runtime/objc-references.mm// class AssociationsManager manages a lock / hash table // singleton pair. Allocating an instance acquires the // lock, and calling its assocations() method // lazily allocates it.

class AssociationsManager { static OSSpinLock _lock; // associative references: // object pointer -> PtrPtrHashMap. static AssociationsHashMap *_map;

public: AssociationsManager() { OSSpinLockLock(&_lock); } ~AssociationsManager() { OSSpinLockUnlock(&_lock); } };

- Associated objects are stored in a global hashmap- The more you add, the worse performance/memory usage gets- Of course, reference counting also works this way, but…

CGFloat *floatingLabelYPadding

UIFont *floatingLabelFont

UIColor *floatingLabelTextColor

UIColor *floatingLabelActiveColor

BOOL animateEvenIfNotFirstResponder

UITextField(JVFloatLabeledTextField)

- Instead, we can encapsulate these properties in a single “options”, or “configuration” object

CGFloat *floatingLabelYPadding

UIFont *floatingLabelFont

UIColor *floatingLabelTextColor

UIColor *floatingLabelActiveColor

BOOL animateEvenIfNotFirstResponder

JVFloatLabeledOptions

- Instead, we can encapsulate these properties in a single “options”, or “configuration” object

CGFloat *floatingLabelYPadding

UIFont *floatingLabelFont

UIColor *floatingLabelTextColor

UIColor *floatingLabelActiveColor

BOOL animateEvenIfNotFirstResponder

JVFloatLabeledOptions

UITextField(JVFloatLabeledTextField) JVFloatLabeledOptions *options

- Instead, we can encapsulate these properties in a single “options”, or “configuration” object

@interface UITextField (JVFloatLabeledTextField)

@property (nonatomic, copy) JVFloatLabeledOptions *options;

@end

- Now we have just one property, options

@interface UITextField (JVFloatLabeledTextField)

@property (nonatomic, copy) JVFloatLabeledOptions *options;

@end

- Now we have just one property, options

@interface JVFloatLabeledOptions : NSObject

@property (nonatomic, assign) CGFloat floatingLabelYPadding UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR;

@property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR;

@end

- From within the implementation, we can access properties on this object- They’re ordinary properties—no runtime

🙅- So what are the tradeoffs of using categories over subclasses?

compose multiple categories

- You don’t have to choose between one set of functionality or another- You don’t have to choose floating labels *or* a cool input accessory—you can have both

runtime sorcery

- Runtime manipulation makes this approach a little less safe, a little less performant

- I used this and other patterns in an open-source UI library I developed last year, MDCSwipeToChoose- Category on UIView

- I used this and other patterns in an open-source UI library I developed last year, MDCSwipeToChoose- Category on UIView

MDCSwipeOptions *options = [MDCSwipeOptions new]; options.threshold = 130.f; options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) { bookmarkView.alpha = 0.f; dontBookmarkView.alpha = state.thresholdRatio; } else if (state.direction == MDCSwipeDirectionRight) { bookmarkView.alpha = state.thresholdRatio; dontBookmarkView.alpha = 0.f; } };

[webView mdc_swipeToChooseSetup:options];

- Add swiping behavior to any view—here a web view- We create the options object and set its properties- Then we setup the view using the options—this can only be done once, so the properties can’t be changed—a killer feature

MDCSwipeOptions *options = [MDCSwipeOptions new]; options.threshold = 130.f; options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) { bookmarkView.alpha = 0.f; dontBookmarkView.alpha = state.thresholdRatio; } else if (state.direction == MDCSwipeDirectionRight) { bookmarkView.alpha = state.thresholdRatio; dontBookmarkView.alpha = 0.f; } };

[webView mdc_swipeToChooseSetup:options];

- Add swiping behavior to any view—here a web view- We create the options object and set its properties- Then we setup the view using the options—this can only be done once, so the properties can’t be changed—a killer feature

MDCSwipeOptions *options = [MDCSwipeOptions new]; options.threshold = 130.f; options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) { bookmarkView.alpha = 0.f; dontBookmarkView.alpha = state.thresholdRatio; } else if (state.direction == MDCSwipeDirectionRight) { bookmarkView.alpha = state.thresholdRatio; dontBookmarkView.alpha = 0.f; } };

[webView mdc_swipeToChooseSetup:options];

- Add swiping behavior to any view—here a web view- We create the options object and set its properties- Then we setup the view using the options—this can only be done once, so the properties can’t be changed—a killer feature

MDCSwipeOptions *options = [MDCSwipeOptions new]; options.threshold = 130.f; options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) { bookmarkView.alpha = 0.f; dontBookmarkView.alpha = state.thresholdRatio; } else if (state.direction == MDCSwipeDirectionRight) { bookmarkView.alpha = state.thresholdRatio; dontBookmarkView.alpha = 0.f; } };

[webView mdc_swipeToChooseSetup:options];

- Add swiping behavior to any view—here a web view- We create the options object and set its properties- Then we setup the view using the options—this can only be done once, so the properties can’t be changed—a killer feature

MDCSwipeOptions *options = [MDCSwipeOptions new]; options.threshold = 130.f; options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) { bookmarkView.alpha = 0.f; dontBookmarkView.alpha = state.thresholdRatio; } else if (state.direction == MDCSwipeDirectionRight) { bookmarkView.alpha = state.thresholdRatio; dontBookmarkView.alpha = 0.f; } };

[webView mdc_swipeToChooseSetup:options];

- Add swiping behavior to any view—here a web view- We create the options object and set its properties- Then we setup the view using the options—this can only be done once, so the properties can’t be changed—a killer feature

MDCSwipeOptions *options = [MDCSwipeOptions new]; options.threshold = 130.f; options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) { bookmarkView.alpha = 0.f; dontBookmarkView.alpha = state.thresholdRatio; } else if (state.direction == MDCSwipeDirectionRight) { bookmarkView.alpha = state.thresholdRatio; dontBookmarkView.alpha = 0.f; } };

[webView mdc_swipeToChooseSetup:options];

- Add swiping behavior to any view—here a web view- We create the options object and set its properties- Then we setup the view using the options—this can only be done once, so the properties can’t be changed—a killer feature

MDCSwipeOptions *options = [MDCSwipeOptions new]; options.threshold = 130.f; options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) { bookmarkView.alpha = 0.f; dontBookmarkView.alpha = state.thresholdRatio; } else if (state.direction == MDCSwipeDirectionRight) { bookmarkView.alpha = state.thresholdRatio; dontBookmarkView.alpha = 0.f; } };

[webView mdc_swipeToChooseSetup:options];

- Add swiping behavior to any view—here a web view- We create the options object and set its properties- Then we setup the view using the options—this can only be done once, so the properties can’t be changed—a killer feature

- The view category is configured has a threshold—when the center of the view moves out of the threshold, it gets swiped offscreen- I don’t have to worry about the threshold changing, or the pan block, or anything

- The view category is configured has a threshold—when the center of the view moves out of the threshold, it gets swiped offscreen- I don’t have to worry about the threshold changing, or the pan block, or anything

- The view category is configured has a threshold—when the center of the view moves out of the threshold, it gets swiped offscreen- I don’t have to worry about the threshold changing, or the pan block, or anything

MDCSwipeOptions *options = [MDCSwipeOptions new]; options.threshold = 130.f; options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) { bookmarkView.alpha = 0.f; dontBookmarkView.alpha = state.thresholdRatio; } else if (state.direction == MDCSwipeDirectionRight) { bookmarkView.alpha = state.thresholdRatio; dontBookmarkView.alpha = 0.f; } };

[webView mdc_swipeToChooseSetup:options];

- Another thing to note: the pan block takes a single parameter

MDCSwipeOptions *options = [MDCSwipeOptions new]; options.threshold = 130.f; options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) { bookmarkView.alpha = 0.f; dontBookmarkView.alpha = state.thresholdRatio; } else if (state.direction == MDCSwipeDirectionRight) { bookmarkView.alpha = state.thresholdRatio; dontBookmarkView.alpha = 0.f; } };

[webView mdc_swipeToChooseSetup:options];

- Another thing to note: the pan block takes a single parameter

// AFNetworking/AFSecurityPolicy.m

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust { return [self evaluateServerTrust:serverTrust forDomain:nil]; }// ...

- With public methods, you can define new methods, and have the old call the new with default parameters

// AFNetworking/AFSecurityPolicy.m

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust { return [self evaluateServerTrust:serverTrust forDomain:nil]; }

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain { // ... }

- With public methods, you can define new methods, and have the old call the new with default parameters

func evaluateServerTrust( trust: SecTrustRef) -> Bool { // ... }

- Even easier in Swift—just define default parameters

func evaluateServerTrust( trust: SecTrustRef, domain: String? = nil) -> Bool { // ... }

- Even easier in Swift—just define default parameters

options.onPan = ^(UIView *view, MDCSwipeDirection direction) {

if (direction == MDCSwipeDirectionLeft) { // ...panning to the left. } };

- Obj-C or Swift, block params and delegate callbacks lock you into an API- So if we add a parameter here…

options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio) { if (direction == MDCSwipeDirectionLeft) { // ...panning to the left. } };

- Obj-C or Swift, block params and delegate callbacks lock you into an API- So if we add a parameter here…

view.onPan = ^(UIView *view, MDCSwipeDirection direction) { if (direction == MDCSwipeDirectionLeft) { // ... } }; subview.onPan = ^(UIView *view, MDCSwipeDirection direction) { // ... }; void (^onPanBlock)(UIView *view, MDCSwipeDirection direction) = nil; subview.onPan = onPanBlock;

- …every callsite breaks

view.onPan = ^(UIView *view, MDCSwipeDirection direction) { if (direction == MDCSwipeDirectionLeft) { // ... } }; subview.onPan = ^(UIView *view, MDCSwipeDirection direction) { // ... }; void (^onPanBlock)(UIView *view, MDCSwipeDirection direction) = nil; subview.onPan = onPanBlock;

- …every callsite breaks

options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio) { if (direction == MDCSwipeDirectionLeft) { // ...panning to the left. } };

- Instead, you could encapsulate params in an object

options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio) { if (direction == MDCSwipeDirectionLeft) { // ...panning to the left. } };

options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) {

- Instead, you could encapsulate params in an object

@interface MDCPanState : NSObject

@property (nonatomic, strong, readonly) UIView *view;

@property (nonatomic, assign, readonly) MDCSwipeDirection direction; DEPRECATED_ATTRIBUTE

@property (nonatomic, assign, readonly) CGFloat thresholdRatio;

@end

- This is a design pattern that Martin Fowler calls “parameter objects”- One benefit is that it’s easy to change- Deprecation can help you slowly phase out APIs

@interface MDCPanState : NSObject

@property (nonatomic, strong, readonly) UIView *view;

@property (nonatomic, assign, readonly) MDCSwipeDirection direction; DEPRECATED_ATTRIBUTE

@property (nonatomic, assign, readonly) CGFloat thresholdRatio;

@end

- This is a design pattern that Martin Fowler calls “parameter objects”- One benefit is that it’s easy to change- Deprecation can help you slowly phase out APIs

options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) { // ...panning to the left. } };

- Users will see a warning when they attempt to use the deprecated property

options.onPan = ^(MDCPanState *state) { if (state.direction == MDCSwipeDirectionLeft) { // ...panning to the left. } };

- Users will see a warning when they attempt to use the deprecated property

@protocol MDCSwipeToChooseDelegate <NSObject>

@optional

- (void)swipeToChooseView:(UIView *)view wasChosenWithDirection:(MDCSwipeDirection)direction;

@end

- Same goes for delegates and other protocols—changing params is a major version bump- Use parameter object for future-proofing

@protocol MDCSwipeToChooseDelegate <NSObject>

@optional

- (void)swipeToChooseView:(UIView *)view wasChosenWithDirection:(MDCSwipeDirection)direction momentum:(CGFloat)momentum;

@end

- Same goes for delegates and other protocols—changing params is a major version bump- Use parameter object for future-proofing

@protocol MDCSwipeToChooseDelegate <NSObject>

@optional

- (void)swipeToChooseView:(UIView *)view wasChosenWithParameters:(MDCChosenParameters *)params;

@end

- Same goes for delegates and other protocols—changing params is a major version bump- Use parameter object for future-proofing

🙅- So what are the tradeoffs of parameter objects?

version your API

- Future-proof block or protocol APIs

overhead

- Small perf overhead of creating object.- Large dev overhead of defining new class- Use sparingly, for public APIs that may change?

1. Categories vs. Subclasses

- You can compose categories, with some runtime hacking

2. Configuration objects

- Configuration objects allow you to initialize an object with a set of parameters you can easily change

3. Always prefer immutability

- Set it and forget it—if you allow your objects to change and mutate, you’re going to be chasing subtle bugs

4. Parameter objects

- Use parameter objects to version your APIs

1. Categories vs. Subclasses 2. Configuration objects 3. Always prefer immutability4. Parameter objects

Thanks!

Recommended