Upload
brian-gesiak
View
754
Download
4
Tags:
Embed Size (px)
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!