ios ui component api design
TRANSCRIPT
UIコンポーネントAPIデザイン「パラメータ・オブジェクト」パターン
Brian Gesiak
2014年4月9日
Research Student, The University of Tokyo
@modocache #potatotips
内容•課題:カスタマイズ用のAPIをどう提供するか
•細かいところまでカスタマイズしたい •継承より組み立てを好む
•解決策:設定オブジェクト •設定オブジェクト
!
•課題:コールバックのAPI
•ブロックやdelegateメソッドのパラメータが確定してしまうとなかなか変えられない
•解決策:パラメータ・オブジェクト
カスタマイズ用のAPIの一例JVFloatLabeledTextField
カスタマイズ用のAPIの一例JVFloatLabeledTextField
カスタマイズ用のAPIの一例JVFloatLabeledTextField
カスタマイズ用のAPIの一例JVFloatLabeledTextField
@interface JVFloatLabeledTextField : UITextField !@property (nonatomic, strong) NSNumber *floatingLabelYPadding UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelActiveTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, assign) NSInteger animateEvenIfNotFirstResponder UI_APPEARANCE_SELECTOR; !@end
カスタマイズ用のAPIの一例JVFloatLabeledTextField
@interface JVFloatLabeledTextField : UITextField !@property (nonatomic, strong) NSNumber *floatingLabelYPadding UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelActiveTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, assign) NSInteger animateEvenIfNotFirstResponder UI_APPEARANCE_SELECTOR; !@end
カスタマイズ用のAPIの一例JVFloatLabeledTextField
@interface JVFloatLabeledTextField : UITextField !@property (nonatomic, strong) NSNumber *floatingLabelYPadding UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelActiveTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, assign) NSInteger animateEvenIfNotFirstResponder UI_APPEARANCE_SELECTOR; !@end
カスタマイズ用のAPIの一例JVFloatLabeledTextField
@interface JVFloatLabeledTextField : UITextField !@property (nonatomic, strong) NSNumber *floatingLabelYPadding UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelActiveTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, assign) NSInteger animateEvenIfNotFirstResponder UI_APPEARANCE_SELECTOR; !@end
継承するのか?継承より組み立てを好む
継承するのか?
• JVFloatLabeledTextFieldはUITextFieldのサブクラス
継承より組み立てを好む
継承するのか?
• JVFloatLabeledTextFieldはUITextFieldのサブクラス•機能がほしければこのクラスを使うしかない
継承より組み立てを好む
継承するのか?
• JVFloatLabeledTextFieldはUITextFieldのサブクラス•機能がほしければこのクラスを使うしかない•機能を追加したければサブクラスを新たに定義するしかない
継承より組み立てを好む
継承するのか?
• JVFloatLabeledTextFieldはUITextFieldのサブクラス•機能がほしければこのクラスを使うしかない•機能を追加したければサブクラスを新たに定義するしかない
• JVFloatLabeledTextFieldは継承ヒエラルキーに自分をねじ込んでいる
継承より組み立てを好む
継承するのか?
• JVFloatLabeledTextFieldはUITextFieldのサブクラス•機能がほしければこのクラスを使うしかない•機能を追加したければサブクラスを新たに定義するしかない
• JVFloatLabeledTextFieldは継承ヒエラルキーに自分をねじ込んでいる
継承より組み立てを好む
•カテゴリーだったら、どのUITextFieldでも使える
@interface JVFloatLabeledTextField : UITextField !@property (nonatomic, strong) NSNumber *floatingLabelYPadding UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelActiveTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, assign) NSInteger animateEvenIfNotFirstResponder UI_APPEARANCE_SELECTOR; !@end
継承するのか?継承より組み立てを好む
@interface JVFloatLabeledTextField : UITextField !@property (nonatomic, strong) NSNumber *floatingLabelYPadding UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelActiveTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, assign) NSInteger animateEvenIfNotFirstResponder UI_APPEARANCE_SELECTOR; !@end
継承するのか?継承より組み立てを好む
@interface JVFloatLabeledTextField : UITextField !@property (nonatomic, strong) NSNumber *floatingLabelYPadding UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelActiveTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, assign) NSInteger animateEvenIfNotFirstResponder UI_APPEARANCE_SELECTOR; !@end
継承するのか?継承より組み立てを好む
@interface JVFloatLabeledTextField : UITextField !@property (nonatomic, strong) NSNumber *floatingLabelYPadding UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelActiveTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, assign) NSInteger animateEvenIfNotFirstResponder UI_APPEARANCE_SELECTOR; !@end
継承するのか?継承より組み立てを好む
@interface UITextField (JVFloatLabeledTextField)
@interface JVFloatLabeledTextField : UITextField !@property (nonatomic, strong) NSNumber *floatingLabelYPadding UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelActiveTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, assign) NSInteger animateEvenIfNotFirstResponder UI_APPEARANCE_SELECTOR; !@end
継承するのか?継承より組み立てを好む
@interface UITextField (JVFloatLabeledTextField)
@interface JVFloatLabeledTextField : UITextField !@property (nonatomic, strong) NSNumber *floatingLabelYPadding UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIFont *floatingLabelFont UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *floatingLabelActiveTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, assign) NSInteger animateEvenIfNotFirstResponder UI_APPEARANCE_SELECTOR; !@end
継承するのか?継承より組み立てを好む
objc_setAssociatedObject@interface UITextField (JVFloatLabeledTextField)
継承するのか?継承より組み立てを好む
継承するのか?継承より組み立てを好む
static void *JVFloatingLabelYPaddingKey = &JVFloatingLabelYPaddingKey; !- (void)setFloatingLabelYPadding:(NSNumber *)floatingLabelYPadding { objc_setAssociatedObject(self, JVFloatingLabelYPaddingKey, floatingLabelYPadding, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } !- (NSNumber *)floatingLabelYPadding { return objc_getAssociatedObject(self, JVFloatingLabelYPaddingKey); } !/// Add custom setters and getters for all properties
継承するのか?継承より組み立てを好む
static void *JVFloatingLabelYPaddingKey = &JVFloatingLabelYPaddingKey; !- (void)setFloatingLabelYPadding:(NSNumber *)floatingLabelYPadding { objc_setAssociatedObject(self, JVFloatingLabelYPaddingKey, floatingLabelYPadding, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } !- (NSNumber *)floatingLabelYPadding { return objc_getAssociatedObject(self, JVFloatingLabelYPaddingKey); } !/// Add custom setters and getters for all properties
継承するのか?継承より組み立てを好む
static void *JVFloatingLabelYPaddingKey = &JVFloatingLabelYPaddingKey; !- (void)setFloatingLabelYPadding:(NSNumber *)floatingLabelYPadding { objc_setAssociatedObject(self, JVFloatingLabelYPaddingKey, floatingLabelYPadding, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } !- (NSNumber *)floatingLabelYPadding { return objc_getAssociatedObject(self, JVFloatingLabelYPaddingKey); } !/// Add custom setters and getters for all properties
継承するのか?継承より組み立てを好む
static void *JVFloatingLabelYPaddingKey = &JVFloatingLabelYPaddingKey; !- (void)setFloatingLabelYPadding:(NSNumber *)floatingLabelYPadding { objc_setAssociatedObject(self, JVFloatingLabelYPaddingKey, floatingLabelYPadding, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } !- (NSNumber *)floatingLabelYPadding { return objc_getAssociatedObject(self, JVFloatingLabelYPaddingKey); } !/// Add custom setters and getters for all properties
継承するのか?継承より組み立てを好む
static void *JVFloatingLabelYPaddingKey = &JVFloatingLabelYPaddingKey; !- (void)setFloatingLabelYPadding:(NSNumber *)floatingLabelYPadding { objc_setAssociatedObject(self, JVFloatingLabelYPaddingKey, floatingLabelYPadding, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } !- (NSNumber *)floatingLabelYPadding { return objc_getAssociatedObject(self, JVFloatingLabelYPaddingKey); } !/// Add custom setters and getters for all properties
継承するのか?継承より組み立てを好む
static void *JVFloatingLabelYPaddingKey = &JVFloatingLabelYPaddingKey; !- (void)setFloatingLabelYPadding:(NSNumber *)floatingLabelYPadding { objc_setAssociatedObject(self, JVFloatingLabelYPaddingKey, floatingLabelYPadding, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } !- (NSNumber *)floatingLabelYPadding { return objc_getAssociatedObject(self, JVFloatingLabelYPaddingKey); } !/// Add custom setters and getters for all properties
継承するのか?継承より組み立てを好む
static void *JVFloatingLabelYPaddingKey = &JVFloatingLabelYPaddingKey; !- (void)setFloatingLabelYPadding:(NSNumber *)floatingLabelYPadding { objc_setAssociatedObject(self, JVFloatingLabelYPaddingKey, floatingLabelYPadding, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } !- (NSNumber *)floatingLabelYPadding { return objc_getAssociatedObject(self, JVFloatingLabelYPaddingKey); } !/// Add custom setters and getters for all properties
スケールしない
設定オブジェクトカスタマイズ用のパラメータを束ねる
設定オブジェクトカスタマイズ用のパラメータを束ねる
設定オブジェクトカスタマイズ用のパラメータを束ねる
設定オブジェクトカスタマイズ用のパラメータを束ねる
設定オブジェクトカスタマイズ用のパラメータを束ねる
設定オブジェクトの一例MDCSwipeToChoose
設定オブジェクトの一例MDCSwipeToChoose
設定オブジェクトの一例MDCSwipeToChoose
MDCSwipeOptions *options = [MDCSwipeOptions new]; options.delegate = self; options.onPan = ^(MDCPanState *state){ switch (state.direction) { case MDCSwipeDirectionLeft: self.webView.alpha = 0.5f - state.thresholdRatio; break; case MDCSwipeDirectionRight: self.webView.alpha = 0.5f + state.thresholdRatio; break; case MDCSwipeDirectionNone: self.webView.alpha = 0.5f; break; } }; ![self.webView mdc_swipeToChooseSetup:options];
設定オブジェクトの一例MDCSwipeToChoose
MDCSwipeOptions *options = [MDCSwipeOptions new]; options.delegate = self; options.onPan = ^(MDCPanState *state){ switch (state.direction) { case MDCSwipeDirectionLeft: self.webView.alpha = 0.5f - state.thresholdRatio; break; case MDCSwipeDirectionRight: self.webView.alpha = 0.5f + state.thresholdRatio; break; case MDCSwipeDirectionNone: self.webView.alpha = 0.5f; break; } }; ![self.webView mdc_swipeToChooseSetup:options];
設定オブジェクトの一例MDCSwipeToChoose
MDCSwipeOptions *options = [MDCSwipeOptions new]; options.delegate = self; options.onPan = ^(MDCPanState *state){ switch (state.direction) { case MDCSwipeDirectionLeft: self.webView.alpha = 0.5f - state.thresholdRatio; break; case MDCSwipeDirectionRight: self.webView.alpha = 0.5f + state.thresholdRatio; break; case MDCSwipeDirectionNone: self.webView.alpha = 0.5f; break; } }; ![self.webView mdc_swipeToChooseSetup:options];
設定オブジェクトの一例MDCSwipeToChoose
MDCSwipeOptions *options = [MDCSwipeOptions new]; options.delegate = self; options.onPan = ^(MDCPanState *state){ switch (state.direction) { case MDCSwipeDirectionLeft: self.webView.alpha = 0.5f - state.thresholdRatio; break; case MDCSwipeDirectionRight: self.webView.alpha = 0.5f + state.thresholdRatio; break; case MDCSwipeDirectionNone: self.webView.alpha = 0.5f; break; } }; ![self.webView mdc_swipeToChooseSetup:options];
設定オブジェクトの一例MDCSwipeToChoose
MDCSwipeOptions *options = [MDCSwipeOptions new]; options.delegate = self; options.onPan = ^(MDCPanState *state){ switch (state.direction) { case MDCSwipeDirectionLeft: self.webView.alpha = 0.5f - state.thresholdRatio; break; case MDCSwipeDirectionRight: self.webView.alpha = 0.5f + state.thresholdRatio; break; case MDCSwipeDirectionNone: self.webView.alpha = 0.5f; break; } }; ![self.webView mdc_swipeToChooseSetup:options];
設定オブジェクトの一例MDCSwipeToChoose
MDCSwipeOptions *options = [MDCSwipeOptions new]; options.delegate = self; options.onPan = ^(MDCPanState *state){ switch (state.direction) { case MDCSwipeDirectionLeft: self.webView.alpha = 0.5f - state.thresholdRatio; break; case MDCSwipeDirectionRight: self.webView.alpha = 0.5f + state.thresholdRatio; break; case MDCSwipeDirectionNone: self.webView.alpha = 0.5f; break; } }; ![self.webView mdc_swipeToChooseSetup:options];
設定オブジェクトの一例MDCSwipeToChoose
MDCSwipeOptions *options = [MDCSwipeOptions new]; options.delegate = self; options.onPan = ^(MDCPanState *state){ switch (state.direction) { case MDCSwipeDirectionLeft: self.webView.alpha = 0.5f - state.thresholdRatio; break; case MDCSwipeDirectionRight: self.webView.alpha = 0.5f + state.thresholdRatio; break; case MDCSwipeDirectionNone: self.webView.alpha = 0.5f; break; } }; ![self.webView mdc_swipeToChooseSetup:options];
設定オブジェクトの一例MDCSwipeToChoose
MDCSwipeOptions *options = [MDCSwipeOptions new]; options.delegate = self; options.onPan = ^(MDCPanState *state){ switch (state.direction) { case MDCSwipeDirectionLeft: self.webView.alpha = 0.5f - state.thresholdRatio; break; case MDCSwipeDirectionRight: self.webView.alpha = 0.5f + state.thresholdRatio; break; case MDCSwipeDirectionNone: self.webView.alpha = 0.5f; break; } }; ![self.webView mdc_swipeToChooseSetup:options];
設定オブジェクトの一例MDCSwipeToChoose
MDCSwipeOptions *options = [MDCSwipeOptions new]; options.delegate = self; options.onPan = ^(MDCPanState *state){ switch (state.direction) { case MDCSwipeDirectionLeft: self.webView.alpha = 0.5f - state.thresholdRatio; break; case MDCSwipeDirectionRight: self.webView.alpha = 0.5f + state.thresholdRatio; break; case MDCSwipeDirectionNone: self.webView.alpha = 0.5f; break; } }; ![self.webView mdc_swipeToChooseSetup:options];
設定オブジェクトの一例MDCSwipeToChoose
MDCSwipeOptions *options = [MDCSwipeOptions new]; options.delegate = self; options.onPan = ^(MDCPanState *state){ switch (state.direction) { case MDCSwipeDirectionLeft: self.webView.alpha = 0.5f - state.thresholdRatio; break; case MDCSwipeDirectionRight: self.webView.alpha = 0.5f + state.thresholdRatio; break; case MDCSwipeDirectionNone: self.webView.alpha = 0.5f; break; } }; ![self.webView mdc_swipeToChooseSetup:options];
パラメータ・オブジェクトブロックのシグネチャの変化を回避する
options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio){ if (direction == MDCSwipeDirectionLeft) { NSLog(@"Panning to the left..."); } };
パラメータ・オブジェクトブロックのシグネチャの変化を回避する
options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio){ if (direction == MDCSwipeDirectionLeft) { NSLog(@"Panning to the left..."); } };
パラメータ・オブジェクトブロックのシグネチャの変化を回避する
options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio){ if (direction == MDCSwipeDirectionLeft) { NSLog(@"Panning to the left..."); } };
パラメータ・オブジェクトブロックのシグネチャの変化を回避する
options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio){ if (direction == MDCSwipeDirectionLeft) { NSLog(@"Panning to the left..."); } };
パラメータ・オブジェクトブロックのシグネチャの変化を回避する
options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio){ if (direction == MDCSwipeDirectionLeft) { NSLog(@"Panning to the left..."); } };
パラメータ・オブジェクトブロックのシグネチャの変化を回避する
options.onPan = ^(MDCPanState *state){ MDCSwipeDirection direction = state.direction;
options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio){ if (direction == MDCSwipeDirectionLeft) { NSLog(@"Panning to the left..."); } };
パラメータ・オブジェクトブロックのシグネチャの変化を回避する
options.onPan = ^(MDCPanState *state){ MDCSwipeDirection direction = state.direction;
パラメータ・オブジェクトAPIの微調整、バージョニングが可能
@interface MDCPanState : NSObject !@property (nonatomic, strong) UIView *view; @property (nonatomic, assign) MDCSwipeDirection direction; @property (nonatomic, assign) CGFloat thresholdRatio; !@end
パラメータ・オブジェクトAPIの微調整、バージョニングが可能
@interface MDCPanState : NSObject !@property (nonatomic, strong) UIView *view; @property (nonatomic, assign) MDCSwipeDirection direction; @property (nonatomic, assign) CGFloat thresholdRatio; !@end
DEPRECATED_ATTRIBUTE;
options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio){ if (direction == MDCSwipeDirectionLeft) { NSLog(@"Panning to the left..."); } };
options.onPan = ^(MDCPanState *state){ MDCSwipeDirection direction = state.direction;
パラメータ・オブジェクトサポートしないパラメータを少しずつ排除
options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio){ if (direction == MDCSwipeDirectionLeft) { NSLog(@"Panning to the left..."); } };
options.onPan = ^(MDCPanState *state){ MDCSwipeDirection direction = state.direction;
パラメータ・オブジェクトサポートしないパラメータを少しずつ排除
options.onPan = ^(UIView *view, MDCSwipeDirection direction, CGFloat thresholdRatio){ if (direction == MDCSwipeDirectionLeft) { NSLog(@"Panning to the left..."); } };
options.onPan = ^(MDCPanState *state){ MDCSwipeDirection direction = state.direction;
パラメータ・オブジェクトサポートしないパラメータを少しずつ排除
要約
•継承に頼らないUIコンポーネントを好む •設定オブジェクトで、クリーンなカスタマイズ用のAPIを提供できる
•パラメータ・オブジェクトというデザイン・パターンは、シグネチャの変化を未然に防ぐ
•特にブロックのパラメータに有用
ご参考までに•本日のスライド
• http://modocache.io/ios-ui-component-api-design •ぜひフォローして下さい
• Twitter: @modocache • GitHub: https://github.com/modocache
• JVFloatLabeledTextField • https://github.com/jverdi/JVFloatLabeledTextField
• MDCSwipeToChoose(おいらに☆を!) • https://github.com/modocache/MDCSwipeToChoose
•「パラメータ・オブジェクト」デザイン・パターン(英語) • http://c2.com/cgi/wiki?ParameterObject