原文:Tips for Interface Builder
當年輕的開發者朋友們寫了一大堆的代碼時,他們會覺得很高興,並且喜歡誇耀自己寫的項目有成千上萬行代碼。
隨著經驗越來越豐富,我們意識到代碼是昂貴的,因為要花很多時間去維護、調試和尋找原因。
這也是為什麼我們應該使用工具來幫助我們避免寫出樣板代碼。Interface Builder就是這些工具中的一個。
下面說一些我喜歡的小提示和技巧。
Storyboards還是Xib
很顯然,蘋果是極力推薦我們使用storyboard的,因為下列的一些特性只在storyboard中才有:
很多人抱怨在團隊合作的時候使用storyboard很困難,因為在合並代碼的時候會有沖突。以前這個問題是會經常出現,但是現在storyboard的格式更好、更容易理解了。
即使這樣,我也不喜歡干合並代碼的活。不過幸運的是,當我們使用多個storyboard合理地劃分項目時,就可以避免很多潛在的沖突。
我比較喜歡給每一個場景創建一個storyboard,例如最新的那個app:
iOS 9給我們介紹了Storyboard References,使我們能夠更容易地使用多個storyboard來分割應用。
如果需要部署在老版本的iOS target上呢?
玩轉Storyboard, Xib或類引用
如果你運氣不夠好,未能使用iOS9作為最低的部署目標,仍舊可以用你自己的方法實現。雖然需要花費點功夫,但這些努力都是值得的。多年以來,我一直在使用一個非常簡單的擴展去擁有我自己的Storyboard, xib 和 類引用。
擴展的主要內容:
storyboard.StoryboardName.ControllerIdentifier
storyboard.StoryboardName -> loads the initial vc
class.name
xib.name
@interface UIViewController (KZReplacement) @property (nonatomic, copy) IBInspectable NSString *replacementIdentifier; //! 如果設了replacementIdentifier,就新建一個controller,否則返回self - (UIViewController *)kz_replacementController; @end
在Interface Builder裡是這樣的:
這個方法有一個簡單的實現,下面的是一個最“復雜”的:
- (UIViewController *)kz_replacementControllerWithIdentifier:(NSString *)identifier; { NSDictionary *replacementSchemes = @{ @"class" : [NSValue valueWithPointer:@selector(kz_createControllerWithClassIdentifier:)], @"storyboard" : [NSValue valueWithPointer:@selector(kz_loadControllerWithStoryboardIdentifier:)], @"xib" : [NSValue valueWithPointer:@selector(kz_loadControllerWithXibIdentifier:)] }; __block UIViewController *replacement = nil; [replacementSchemes enumerateKeysAndObjectsUsingBlock:^(NSString *scheme, NSValue *selectorValue, BOOL *stop) { const struct _NSRange range = [identifier rangeOfString:scheme]; if (range.location == 0) { *stop = YES; UIViewController *(*objc_msgSendTyped)(id, SEL, NSString *) = (UIViewController *(*)(id, SEL, NSString *))objc_msgSend; replacement = objc_msgSendTyped(self, selectorValue.pointerValue, [identifier substringFromIndex:range.length + 1]); } }]; return replacement; }
現在你只需在容器內的每一個VC執行這個方法。舉個例子,如果你的app使用了TabBar,下面的方法對你每一個UITabBarController的子類都會起作用:
- (void)awakeFromNib; { [super awakeFromNib]; [super setViewControllers:[self processViewControllersByReplacingMocks:self.viewControllers] animated:NO]; } - (void)setViewControllers:(NSArray *)viewControllers animated:(BOOL)animated; { [super setViewControllers:[self processViewControllersByReplacingMocks:viewControllers] animated:animated]; } - (void)setViewControllers:(NSArray *)viewControllers; { [super setViewControllers:[self processViewControllersByReplacingMocks:viewControllers]]; } - (NSArray *)processViewControllersByReplacingMocks:(NSArray *)viewControllersAndMocks; { NSMutableArray *array = [NSMutableArray new]; [viewControllersAndMocks enumerateObjectsUsingBlock:^(UIViewController *viewController, NSUInteger idx, BOOL *stop) { UIViewController *replacement = [viewController kz_replacementController]; [array addObject:replacement ?: viewController]; }]; return [array copy]; }
使用行為(Behaviours)
如果要給你們推薦一種模式的話,毫無疑問我會推薦“組合”(composition),這是個非常棒的模式,並且能自然而然地帶來其他好的方面。
行為是一種組合,只能基於Interface Builder配置。
之前我在objc.io的架構指南中寫過。
運行時屬性和@IBInspectable
多年前我們就可以直接在IB內設置一些自定義屬性了,但是還有很多人是不知道的。他們在Interface Builder中新建了一個UIView,然後再用代碼來設置View的一些屬性。
Xcode 6的運行時屬性升級到支持@IBInspectable了,將老且脆弱的運行時屬性:
修改成了一個可編輯的屬性,我們再也不會拼寫錯誤了:
可支持的屬性有下列這些類型:
擴展UIKit
你還可以擴展現有的類型,比如UIKit中的類:
extension UIView { @IBInspectable var kz_borderColor: UIColor? { get { if let colorRef = layer.borderColor { return UIColor(CGColor: colorRef) } return nil } set { layer.borderColor = newValue?.CGColor } } }
給UIView增加了一個漂亮的邊界顏色編輯器
消除副作用
給NSLayoutConstraint增加一個標示符會咋樣呢?iOS7增加了一個identifier字段作調試用,不過我在另外的一些地方使用了自定義的identifier。
我們的應用在不同的iOS設備的顯示略有一些差異,不幸的是Size Classes未能滿足這個特別的設計。
為了處理這個需求,我創建了一種簡單的DSL語法來描述每一種設備的值:
KZ_SPEC(Create, (@{ @"create.size.captionTextWidth" : kz_spec(260, 250, 234, 220), @"create.size.segmentCollectionView.height" : kz_spec(116, 116, 116, 116).offsetBy(16) }))
並且擴展了NSLayoutConstraint,使我們能夠為實際的界面綁定規格。這樣就避免了在view中寫樣板代碼了:
@interface NSLayoutConstraint(KZDesignSpec) @property (nonatomic, copy) IBInspectable NSString* kz_specName; @end @implementation NSLayoutConstraint (KZDesignSpe) static void const *kDesignSpecKey = &kDesignSpecKey; - (NSString *)kz_specName; { return objc_getAssociatedObject(self, kDesignSpecKey); } - (void)setKz_specName:(NSString *)kz_specName; { objc_setAssociatedObject(self, kDesignSpecKey, kz_specName, OBJC_ASSOCIATION_COPY_NONATOMIC); self.constant = kz_floatForSpec(kz_specName); self.identifier = kz_specName; } @end
快捷鍵和隱藏選項
確認你知道自己的快捷鍵以及Interface Builder的一些隱藏選項。
我還記得在我使用了2年的IB後看見一個人使用Media Tab拉出一張圖片,並創建了一個合適尺寸的UIImageView.而這時的我還是手動的......
快捷鍵有很多,這裡給出了一些不錯的。
當你在設置AutoLayout的時候,記得在拉約束的同時按Option鍵,或者使用編輯菜單。
總結
Interface Builder離完美還有很長的路,但它也遠不是一些開發者認為的一個糟糕的工具。
很多應用不能說是“用Interface Builder來實現太復雜”了,這是很多人在我問他們為什麼不用IB的時候的回答。
Foldify和Storest都大量地使用IB.
實際上,很多情況下使用IB足矣,特別是加上我之前給出的擴展,或者是你自己實現的擴展。
其他情況下使用代碼。代碼或IB,不是一個只能二選一的問題。