<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # 如何設計一個 iOS 控件?(iOS 控件完全解析) > 代碼的等級:可編譯、可運行、可測試、可讀、可維護、可復用 * * * ## 前言 一個控件從外在特征來說,主要是封裝這幾點: > * 交互方式 > * 顯示樣式 > * 數據使用 對外在特征的封裝,能讓我們在多種環境下達到 PM 對產品的要求,并且提到代碼復用率,使維護工作保持在一個相對較小的范圍內;而一個好的控件除了有對外一致的體驗之外,還有其內在特征: > * 靈活性 > * 低耦合 > * 易拓展 > * 易維護 通常特征之間需要做一些取舍,比如靈活性與耦合度,有時候接口越多越能適應各種環境,但是接口越少對外產生的依賴就越少,維護起來也更容易。通常一些前期看起來還不錯的代碼,往往也會隨著時間加深慢慢“成長”,功能的增加也會帶來新的接口,很不自覺地就加深了耦合度,在開發中時不時地進行一些重構工作很有必要。總之,盡量減少接口的數量,但有足夠的定制空間,可以在一開始把接口全部隱藏起來,再根據實際需要慢慢放開。 自定義控件在?`iOS`?項目里很常見,通常頁面之間入口很多,而且使用場景極有可能大不相同,比如一個?`UIView`?既可以以代碼初始化,也可以以?`xib`?的形式初始化,而我們是需要保證這兩種操作都能產生同樣的行為。本文將會討論到以下幾點: > * 選擇正確的初始化方式 > * 調整布局的時機 > * 正確的處理 touches 方法 > * drawRectCALayer 與動畫 > * UIControl 與 UIButton > * 更友好的支持 xib > * 不規則圖形和事件觸發范圍(事件鏈的簡單介紹以及處理) > * 合理使用 KVO 如果這些問題你一看就懂的話就不用繼續往下看了。 ## 設計方針 * * * ### 選擇正確的初始化方式 `UIView`?的首要問題就是既能從代碼中初始化,也能從?`xib`?中初始化,兩者有何不同? UIView 是支持?`NSCoding`?協議的,當在 xib 或 storyboard 里存在一個 UIView 的時候,其實是將 UIView 序列化到文件里(xib 和 storyboard 都是以 XML 格式來保存的),加載的時候反序列化出來,所以: > * 當從代碼實例化 UIView 的時候,`initWithFrame`?會執行; > * 當從文件加載 UIView 的時候,`initWithCoder`?會執行。 #### 從代碼中加載 雖然 initWithFrame 是 UIView 的`Designated Initializer`,理論上來講你繼承自 UIView 的任何子類,該方法最終都會被調用,但是有一些類在初始化的時候沒有遵守這個約定,如?`UIImageView`?的?`initWithImage`?和?`UITableViewCell`?的?`initWithStyle:reuseIdentifier`: 的構造器等,所以我們在寫自定義控件的時候,最好只假設父視圖的 Designated Initializer 被調用。 如果控件在初始化或者在使用之前必須有一些參數要設置,那我們可以寫自己的 Designated Initializer 構造器,如: ~~~ - (instancetype)initWithName:(NSString *)name; ~~~ 在實現中一定要調用父類的 Designated Initializer,而且如果你有多個自定義的 Designated Initializer,最終都應該指向一個全能的初始化構造器: ~~~ - (instancetype)initWithName:(NSString *)name { self = [self initWithName:name frame:CGRectZero]; return self; } - (instancetype)initWithName:(NSString *)name frame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.name = name; } return self; } ~~~ 并且你要考慮到,因為你的控件是繼承自 UIView 或 UIControl 的,那么用戶完全可以不使用你提供的構造器,而直接調用基類的構造器,所以最好重寫父類的 Designated Initializer,使它調用你提供的 Designated Initializer ,比如父類是個 UIView: ~~~ - (instancetype)initWithFrame:(CGRect)frame { self = [self initWithName:nil frame:frame]; return self; } ~~~ 這樣當用戶從代碼里初始化你的控件的時候,就總是逃脫不了你需要執行的初始化代碼了,哪怕用戶直接調用?`init`?方法,最終還是會回到父類的 Designated Initializer 上。 #### 從 xib 或 storyboard 中加載 當控件從 xib 或 storyboard 中加載的時候,情況就變得復雜了,首先我們知道有 initWithCoder 方法,該方法會在對象被反序列化的時候調用,比如從文件加載一個 UIView 的時候: ~~~ UIView *view = [[UIView alloc] init]; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:view]; [[NSUserDefaults standardUserDefaults] setObject:data forKey:@"KeyView"]; [[NSUserDefaults standardUserDefaults] synchronize]; data = [[NSUserDefaults standardUserDefaults] objectForKey:@"KeyView"]; view = [NSKeyedUnarchiver unarchiveObjectWithData:data]; NSLog(@"%@", view); ~~~ 執行?`unarchiveObjectWithData`?的時候,?`initWithCoder`?會被調用,那么你有可能會在這個方法里做一些初始化工作,比如恢復到保存之前的狀態,當然前提是需要在?`encodeWithCoder`?中預先保存下來。 不過我們很少會自己直接把一個 View 保存到文件中,一般是在 xib 或 storyboard 中寫一個 View,然后讓系統來完成反序列化的工作,此時在`initWithCoder`?調用之后,`awakeFromNib`?方法也會被執行,既然在?`awakeFromNib`?方法里也能做初始化操作,那我們如何抉擇? 一般來說要盡量在?`initWithCoder`?中做初始化操作,畢竟這是最合理的地方,只要你的控件支持序列化,那么它就能在任何被反序列化的時候執行初始化操作,這里適合做全局數據、狀態的初始化工作,也適合手動添加子視圖。 `awakeFromNib`?相較于?`initWithCoder`?的優勢是:當?`awakeFromNib`?執行的時候,各種?`IBOutlet`?也都連接好了;而?`initWithCoder`?調用的時候,雖然子視圖已經被添加到視圖層級中,但是還沒有引用。如果你是基于 xib 或 storyboard 創建的控件,那么你可能需要對 IBOutlet 連接的子控件進行初始化工作,這種情況下,你只能在?`awakeFromNib`?里進行處理。同時 xib 或 storyboard 對靈活性是有打折的,因為它們創建的代碼無法被繼承,所以當你選擇用 xib 或 storyboard 來實現一個控件的時候,你已經不需要對靈活性有很高的要求了,唯一要做的是要保證用戶一定是通過 xib 創建的此控件,否則可能是一個空的視圖,可以在?`initWithFrame`?里放置一個?`斷言`?或者異常來通知控件的用戶。 最后還要注意視圖層級的問題,比如你要給 View 放置一個背景,你可能會在?`initWithCoder`?或?`awakeFromNib`?中這樣寫: ~~~ [self addSubview:self.backgroundView]; // 通過懶加載一個背景 View,然后添加到視圖層級上 ~~~ 你的本意是在控件的最下面放置一個背景,卻有可能將這個背景覆蓋到控件的最上方,原因是用戶可能會在 xib 里寫入這個控件,然后往它上面添加一些子視圖,這樣一來,用戶添加的這些子視圖會在你添加背景之前先進入視圖層級,你的背景被添加后就擋住了用戶的子視圖。如果你想支持用戶的這種操作,可以把?`addSubview`?替換成?`insertSubview:atIndex:`。 #### 同時支持從代碼和文件中加載 如果你要同時支持?`initWithFrame`?和?`initWithCoder`?,那么你可以提供一個?`commonInit`?方法來做統一的初始化: ~~~ - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self commonInit]; } return self; } - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self commonInit]; } return self; } - (void)commonInit { // do something ... } ~~~ `awakeFromNib`?方法里就不要再去調用?`commonInit`?了。 * * * ### 調整布局的時機 當一個控件被初始化以及開始使用之后,它的?`frame`?仍然可能發生變化,我們也需要接受這些變化,因為你提供的是?`UIView`?的接口,`UIView`?有很多種初始化方式:`initWithFrame`、`initWithCoder`、`init`?和類方法?`new`,用戶完全可以在初始化之后再設置?`frame`?屬性,而且用戶就算使用`initWithFrame`?來初始化也避免不了?`frame`?的改變,比如在橫豎屏切換的時候。為了確保當它的 Size 發生變化后其子視圖也能同步更新,我們不能一開始就把布局寫死(使用約束除外)。 #### 基于 frame 如果你是直接基于 frame 來布局的,你應該確保在初始化的時候只添加視圖,而不去設置它們的frame,把設置子視圖 frame 的過程全部放到`layoutSubviews`?方法里: ~~~ - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self commonInit]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self commonInit]; } return self; } - (void)layoutSubviews { [super layoutSubviews]; self.label.frame = CGRectInset(self.bounds, 20, 0); } - (void)commonInit { [self addSubview:self.label]; } - (UILabel *)label { if (_label == nil) { _label = [UILabel new]; _label.textColor = [UIColor grayColor]; } return _label; } ~~~ 這么做就能保證 label 總是出現在正確的位置上。? 使用 layoutSubviews 方法有幾點需要注意: > 1. 不要依賴前一次的計算結果,應該總是根據當前最新值來計算 > 2. 由于?`layoutSubviews`?方法是在自身的?`bounds`?發生改變的時候調用, 因此?`UIScrollView`?會在滾動時不停地調用,當你只關心 Size 有沒有變化的時候,可以把前一次的 Size 保存起來,通過與最新的 Size 比較來判斷是否需要更新,在大多數情況下都能改善性能 #### 基于 Auto Layout 約束 如果你是基于 Auto Layout 約束來進行布局,那么可以在?`commonInit`?調用的時候就把約束添加上去,不要重寫 layoutSubviews 方法,因為這種情況下它的默認實現就是根據約束來計算 frame。最重要的一點,把?`translatesAutoresizingMaskIntoConstraints`?屬性設為 NO,以免產生`NSAutoresizingMaskLayoutConstraint`?約束,如果你使用?`Masonry`?框架的話,則不用擔心這個問題,`mas_makeConstraints`?方法會首先設置這個屬性為?`NO`: ~~~ - (void)commonInit { ... [self setupConstraintsForSubviews]; } - (void)setupConstraintsForSubviews { [self.label mas_makeConstraints:^(MASConstraintMaker *make) { ... }]; } ~~~ #### 支持 sizeToFit 如果你的控件對尺寸有嚴格的限定,比如有一個統一的寬高比或者是固定尺寸,那么最好能實現系統給出的約定成俗的接口。 sizeToFit 用在基于 frame 布局的情況下,由你的控件去實現 sizeThatFits: 方法: ~~~ - (CGSize)sizeThatFits:(CGSize)size { CGSize fitSize = [super sizeThatFits:size]; fitSize.height += self.label.frame.size.height; // 如果是固定尺寸,就像 UISwtich 那樣返回一個固定 Size 就 OK 了 return fitSize; } ~~~ 然后在外部調用該控件的 sizeToFit 方法,這個方法內部會自動調用 sizeThatFits 并更新自身的 Size: ~~~ [self.customView sizeToFit]; ~~~ #### 在 ViewController 里調整視圖布局 當執行?`viewDidLoad`?方法時,不要依賴?`self.view`?的 Size。很多人會這樣寫: ~~~ - (void)viewDidLoad { ... self.label.width = self.view.width; } ~~~ 這樣是不對的,哪怕看上去沒問題也只是碰巧沒問題而已。當 viewDidLoad 方法被調用的時候,self.view 才剛剛被初始化,此時它的容器還沒有對它的 frame 進行設置,如果 view 是從 xib 加載的,那么它的 Size 就是 xib 中設置的值;如果它是從代碼加載的,那么它的 Size 和屏幕大小有關系,除了 Size 以外,Origin 也不會準確。整個過程看起來像這樣: > 當訪問 ViewController 的 view 的時候,ViewController 會先執行 loadViewIfRequired 方法,如果 view 還沒有加載,則調用 loadView,然后是 viewDidLoad 這個鉤子方法,最后是返回 view,容器拿到 view 后,根據自身的屬性(如 edgesForExtendedLayout、判斷是否存在 tabBar、判斷 navigationBar 是否透明等)添加約束或者設置 frame。 你至少應該設置?`autoresizingMask`?屬性: ~~~ - (void)viewDidLoad { ... self.label.width = self.view.width; self.label.autoresizingMask = UIViewAutoresizingFlexibleWidth; } ~~~ 或者在?`viewDidLayoutSubviews`?里處理: ~~~ - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; self.label.width = self.view.width; } ~~~ 如果是基于 Auto Layout 來布局,則在 viewDidLoad 里添加約束即可。 * * * ### 正確的處理 touches 方法 如果你需要重寫?`touches`?方法,那么應該完整的重寫這四個方法: ~~~ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event; ~~~ 當你的視圖在這四個方法執行的時候,如果已經對事件進行了處理,就不要再調用 super 的 touches 方法,super 的 touches 方法默認實現是在響應鏈里繼續轉發事件(UIView 的默認實現)。如果你的基類是?`UIScrollView`?或者?`UIButton`?這些已經重寫了事件處理的類,那么當你不想處理事件的時候可以調用?`self.nextResponder`?的?`touches`?方法來轉發事件,其他的情況就調用?`super`?的?`touches`?方法來轉發,比如?`UIScrollView`?可以這樣來轉發`觸摸`?事件: ~~~ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (!self.dragging) { [self.nextResponder touchesBegan: touches withEvent:event]; } [super touchesBegan: touches withEvent: event]; } - (void)touchesMoved... - (void)touchesEnded... - (void)touchesCancelled... ~~~ 這么實現以后,當你僅僅只是“碰”一個 UIScrollView 的時候,該事件就有可能被?`nextResponder`?處理。? 如果你沒有實現自己的事件處理,也沒有調用?`nextResponder`?和?`super`,那么響應鏈就會斷掉。另外,盡量用手勢識別器去處理自定義事件,它的好處是你不需要關心響應鏈,邏輯處理起來也更加清晰,事實上,`UIScrollView`?也是通過手勢識別器實現的: > @property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer NS_AVAILABLE_IOS(5_0);? > @property(nonatomic, readonly) UIPinchGestureRecognizer *pinchGestureRecognizer NS_AVAILABLE_IOS(5_0); * * * ### drawRect、CALayer 與動畫 `drawRect`?方法很適合做自定義的控件,當你需要更新 UI 的時候,只要用?`setNeedsDisplay`?標記一下就行了,這么做又簡單又方便;控件也常常用于封裝動畫,但是動畫卻有可能被移除掉。? 需要注意的地方: 1. 在?`drawRect`?里盡量用?`CGContext`?繪制 UI。如果你用?`addSubview`?插入了其他的視圖,那么當系統在每次進入繪制的時候,會先把當前的上下文清除掉(此處不考慮?`clearsContextBeforeDrawing`?的影響),然后你也要清除掉已有的?`subviews`,以免重復添加視圖;用戶可能會往你的控件上添加他自己的子視圖,然后在某個情況下清除所有的子視圖(我就喜歡這么做): ~~~ [subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; ~~~ 2. 用?`CALayer`?代替?`UIView`。`CALayer`?節省內存,而且更適合去做一個“圖層”,因為它不會接收事件、也不會成為響應鏈中的一員,但是它能夠響應父視圖(或?`layer`)的尺寸變化,這種特性很適合做單純的數據展示: ~~~ CALayer *imageLayer = [CALayer layer]; imageLayer.frame = rect; imageLayer.contents = (id)image; [self.view.layer addSublayer:imageLayer]; ~~~ 3. 如果有可能的話使用?`setNeedsDisplayInRect`?代替?`setNeedsDisplay`?以優化性能,但是遇到性能問題的時候應該先檢查自己的繪圖算法和繪圖時機,我個人其實從來沒有使用過?`setNeedsDisplayInRect`。 4. 當你想做一個無限循環播放的動畫的時候,可能會創建幾個封裝了動畫的 CALayer,然后把它們添加到視圖層級上,就像我在[`iOS 實現脈沖雷達以及動態增減元素 By Swift`](http://blog.csdn.net/zhangao0086/article/details/38170359)?中這么做的:? ![](https://box.kancloud.cn/2016-01-18_569ca44e4c365.jpg)? 效果還不錯,實現又簡單,但是當你按下 Home 鍵并再次返回到 app 的時候,原本好看的動畫就變成了一灘死水: > 這是因為在按下 Home 鍵的時候,所有的動畫被移除了,具體的,每個 layer 都調用了 removeAllAnimations 方法。 如果你想重新播放動畫,可以監聽?`UIApplicationDidBecomeActiveNotification`?通知,就像我在?[`上述博客`](http://blog.csdn.net/zhangao0086/article/details/38170359)?中做的那樣。 5. `UIImageView`?的?`drawRect`?永遠不會被調用: > Special Considerations > > The UIImageView class is optimized to draw its images to the display. UIImageView will not call drawRect: in a subclass. If your subclass needs custom drawing code, it is recommended you use UIView as the base class. 6. `UIView`?的?`drawRect`?也不一定會調用,我在 12 年的博客:[`定制UINavigationBar`](http://blog.csdn.net/zhangao0086/article/details/7568998)?中曾經提到過 UIKit 框架的實現機制: > 眾所周知一個視圖如何顯示是取決于它的 drawRect 方法,因為調這個方法之前 UIKit 也不知道如何顯示它,但其實 drawRect 方法的目的也是畫圖(顯示內容),而且我們如果以其他的方式給出了內容(圖)的話, drawRect 方法就不會被調用了。 > > > 注:實際上 UIView 是 CALayer 的delegate,如果 CALayer 沒有內容的話,會回調給 UIView 的 displayLayer: 或者 drawLayer:inContext: 方法,UIView 在其中調用 drawRect ,draw 完后的圖會緩存起來,除非使用 setNeedsDisplay 或是一些必要情況,否則都是使用緩存的圖。 `UIView`?和?`CALayer`?都是模型對象,如果我們以這種方式給出內容的話,`drawRect`?也就不會被調用了: ~~~ self.customView.layer.contents = (id)[UIImage imageNamed:@"AppIcon"]; // 哪怕是給它一個 nil,這兩句等價 self.customView.layer.contents = nil; ~~~ 我猜測是在?`CALayer`?的?`setContents`?方法里有個標記,無論傳入的對象是什么都會將該標記打開,但是調用?`setNeedsDisplay`?的時候會將該標記去除。 * * * ### UIControl 與 UIButton 如果要做一個可交互的控件,那么把?`UIControl`?作為基類就是首選,這個完美的基類支持各種狀態: > * enabled > * selected > * highlighted > * tracking > * …… 還支持多狀態下的觀察者模式: ~~~ @property(nonatomic,readonly) UIControlState state; - (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents; - (void)removeTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents; ~~~ 這個基類可以很方便地為視圖添加各種點擊狀態,最常見的用法就是將?`UIViewController`?的?`view`?改成?`UIControl`,然后就能快速實現`resignFirstResponder`。 `UIButton`?自帶圖文接口,支持更強大的狀態切換,`titleEdgeInsets`?和?`imageEdgeInsets`?也比較好用,配合兩個基類的屬性更好,先設置對齊規則,再設置 insets: ~~~ @property(nonatomic) UIControlContentVerticalAlignment contentVerticalAlignment; @property(nonatomic) UIControlContentHorizontalAlignment contentHorizontalAlignment; ~~~ `UIControl`?和?`UIButton`?都能很好的支持 xib,可以設置各種狀態下的顯示和 Selector,但是對 UIButton 來說這些并不夠,因為?`Normal`、`Highlighted`?和?`Normal | Highlighted`?是三種不同的狀態,如果你需要實現根據當前狀態顯示不同高亮的圖片,可以參考我下面的代碼:? ![](https://box.kancloud.cn/2016-01-18_569ca44e93c54.jpg)![](https://box.kancloud.cn/2016-01-18_569ca44ea2a49.jpg) ~~~ - (void)updateStates { [super setTitle:[self titleForState:UIControlStateNormal] forState:UIControlStateNormal | UIControlStateHighlighted]; [super setImage:[self imageForState:UIControlStateNormal] forState:UIControlStateNormal | UIControlStateHighlighted]; [super setTitle:[self titleForState:UIControlStateSelected] forState:UIControlStateSelected | UIControlStateHighlighted]; [super setImage:[self imageForState:UIControlStateSelected] forState:UIControlStateSelected | UIControlStateHighlighted]; } ~~~ 或者使用初始化設置: ~~~ - (void)commonInit { [self setImage:[UIImage imageNamed:@"Normal"] forState:UIControlStateNormal]; [self setImage:[UIImage imageNamed:@"Selected"] forState:UIControlStateSelected]; [self setImage:[UIImage imageNamed:@"Highlighted"] forState:UIControlStateHighlighted]; [self setImage:[UIImage imageNamed:@"Selected_Highlighted"] forState:UIControlStateSelected | UIControlStateHighlighted]; } ~~~ > 總之盡量使用原生類的接口,或者模仿原生類的接口。 大多數情況下根據你所需要的特性來選擇現有的基類就夠了,或者用?`UIView + 手勢識別器`?的組合也是一個好方案,盡量不要用?`touches`?方法(`userInteractionEnabled`?屬性對?`touches`?和`手勢識別器`的作用一樣),這是我在?[`DKCarouselView`?](https://github.com/zhangao0086/DKCarouselView/tree/1.4.0)中內置的一個可點擊的 ImageView,也可以繼承 UIButton,不過 UIButton 更側重于狀態,ImageView 側重于圖片本身: ~~~ typedef void(^DKCarouselViewTapBlock)(); @interface DKClickableImageView : UIImageView @property (nonatomic, assign) BOOL enable; @property (nonatomic, copy) DKCarouselViewTapBlock tapBlock; @end @implementation DKClickableImageView - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { [self commonInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { if ((self = [super initWithCoder:aDecoder])) { [self commonInit]; } return self; } - (void)commonInit { self.userInteractionEnabled = YES; self.enable = YES; UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)]; [self addGestureRecognizer:tapGesture]; } - (IBAction)onTap:(id)sender { if (!self.enable) return; if (self.tapBlock) { self.tapBlock(); } } @end ~~~ * * * ### 更友好的支持 xib 你的控件現在應該可以正確的從文件、代碼中初始化了,但是從 xib 中初始化以后可能還需要通過代碼來進行一些設置,你或許覺得像上面那樣設置 Button 的狀態很惡心而且不夠直觀,但是也沒辦法,這是由于 xib 雖然對原生控件,如?`UIView`、`UIImageView`、`UIScrollView`?等支持較好(想設置圓角、邊框等屬性也沒辦法,只能通過 layer 來設置),但是對自定義控件卻沒有什么辦法,當你拖一個 UIView 到 xib 中,然后把它的 Class 改成你自己的子類后,xib 如同一個瞎子一樣,不會有任何變化。————好在這些都成了過去。 Xcode 6 引入了兩個新的宏:`IBInspectable`?和?`IBDesignable`。 #### IBInspectable 該宏會讓 xib 識別屬性,它支持這些數據類型:布爾、字符串、數字(NSNumber)、 CGPoint、CGSize、CGRect、UIColor 、 NSRange 和 UIImage。? 比如我們要讓自定義的 Button 能在 xib 中設置?`UIControlStateSelected | UIControlStateHighlighted`?狀態的圖片,就可以這么做: ~~~ // CustomButton @property (nonatomic, strong) IBInspectable UIImage *highlightSelectedImage; - (void)setHighlightSelectedImage:(UIImage *)highlightSelectedImage { _highlightSelectedImage = highlightSelectedImage; [self setImage:highlightSelectedImage forState:UIControlStateHighlighted | UIControlStateSelected]; } ~~~ 只需要在屬性上加個?`IBInspectable`?宏即可,然后 xib 中就能顯示這個自定義的屬性:? ![](https://box.kancloud.cn/2016-01-18_569ca44eb2035.jpg) xib 會把屬性名以大駝峰樣式顯示,如果有多個屬性,xib 也會自動按屬性名的第一個單詞分組顯示,如:? ![](https://box.kancloud.cn/2016-01-18_569ca44ec63d7.jpg) 通過使用?`IBInspectable`?宏,你可以把原本只能通過代碼來設置的屬性,也放到 xib 里來,代碼就顯得更加簡潔了。 #### IBDesignable xib 配合?`IBInspectable`?宏雖然可以讓屬性設置變得簡單化,但是只有在運行期間你才能看到控件的真正效果,而使用?`IBDesignable`?可以讓`Interface Builder`?實時渲染控件,這一切只需要在類名加上?`IBDesignable`?宏即可: ~~~ IB_DESIGNABLE @interface CustomButton : UIButton @property (nonatomic, strong) IBInspectable UIImage *highlightSelectedImage; @end ~~~ 這樣一來,當你在 xib 中調整屬性的時候,畫布也會實時更新。 > 關于對 IBInspectable / IBDesignable 的詳細介紹可以看這里:[http://nshipster.cn/ibinspectable-ibdesignable/](http://nshipster.cn/ibinspectable-ibdesignable/)? > 這是 Twitter 上其他開發者做出的效果:? > ![](https://box.kancloud.cn/2016-01-18_569ca44ed7cb3.jpg)? > ![](https://box.kancloud.cn/2016-01-18_569ca44eee3bc.jpg) 相信通過使用?`IBInspectable / IBDesignable`?,會讓控件使用起來更加方便、也更加有趣。 * * * ### 不規則圖形和事件觸發范圍 不規則圖形在 iOS 上并不多見,想來設計師也怕麻煩。不過 iOS 上的控件說到底都是各式各樣的矩形,就算你修改?`cornerRadius`,讓它看起來像這樣:? ![](https://box.kancloud.cn/2016-01-18_569ca44f13ad8.jpg) 也只是看起來像這樣罷了,它的實際事件觸發范圍還是一個矩形。 #### 問題描述 想象一個復雜的可交互的控件,它并不是單獨工作的,可能需要和另一個控件交互,而且它們的事件觸發范圍可能會重疊,像這個選擇聯系人的列表: ![](https://box.kancloud.cn/2016-01-18_569ca44f287e7.jpg) 在設計的時候讓上面二級菜單在最大的范圍內可以被點擊,下面的一級菜單也能在自己的范圍內很好的工作,正常情況下它們的觸發范圍是這樣的: ![](https://box.kancloud.cn/2016-01-18_569ca44f7dbf4.jpg) 我們想要的是這樣的: ![](https://box.kancloud.cn/2016-01-18_569ca44f8f295.jpg) 想要實現這樣的效果需要對事件分發有一定的了解。首先我們來想想,當觸摸屏幕的時候發生了什么? #### 當觸摸屏幕的時候發生了什么? 當屏幕接收到一個 touch 的時候,iOS 需要找到一個合適的對象來處理事件( touch 或者手勢),要尋找這個對象,需要用到這個方法: ~~~ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; ~~~ 該方法會首先在?`application`?的?`keyWindow`?上調用(`UIWindow`?也是?`UIView`?的子類),并且該方法的返回值將被用來處理事件。如果這個?`view`(無論是?`window`?還是普通的?`UIView`) 的?`userInteractionEnabled`?屬性被設置為?`NO`,則它的?`hitTest:`?永遠返回?`nil`,這意味著它和它的子視圖沒有機會去接收和處理事件。如果?`userInteractionEnabled`?屬性為?`YES`,則會先判斷產生觸摸的?`point`?是否發生在自己的?`bounds`?內,如果沒有也將返回`nil`;如果?`point`?在自己的范圍內,則會為自己的每個子視圖調用?`hitTest:`?方法,只要有一個子視圖通過這個方法返回一個?`UIView`?對象,那么整個方法就一層一層地往上返回;如果沒有子視圖返回?`UIView`?對象,則父視圖將會把自己返回。 所以,在事件分發中,有這么幾個關鍵點: > 1. 如果父視圖不能響應事件(userInteractionEnabled 為 NO),則其子視圖也將無法響應事件。 > 2. 如果子視圖的 frame 有一半在外面,就像這樣:? > ![](https://box.kancloud.cn/2016-01-18_569ca44fa29a1.jpg)? > 則在外面的部分是無法響應事件的,因為它超出了父視圖的范圍。 > 3. 整個事件鏈只會返回一個 Hit-Test View 來處理事件。 > 4. 子視圖的順序會影響到 Hit-Test View 的選擇:最先通過 hitTest: 方法返回的 UIView 才會被返回,假如有兩個子視圖平級,并且它們的 frame 一樣,但是誰是后添加的誰就優先返回。 了解了事件分發的這些特點后,還需要知道最后一件事:`UIView`?如何判斷產生事件的?`point`?是否在自己的范圍內? 答案是通過?`pointInside`?方法,這個方法的默認實現類似于這樣: ~~~ // point 被轉化為對應視圖的坐標系統 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return CGRectContainsPoint(self.bounds, point); } ~~~ 所以,當我們想改變一個?`View`?的事件觸發范圍的時候,重寫?`pointInside`?方法就可以了。 #### 回到問題 針對這種視圖一定要處理它們的事件觸發范圍,也就是?`pointInside`?方法,一般來說,我們先判斷 point 是不是在自己的范圍內(通過調用?`super`?來判斷),然后再判斷該 point 符不符合我們的處理要求: > 這個例子我用 Swift 來寫 ~~~ override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { let inside = super.pointInside(point, withEvent: event) if inside { let radius = self.layer.cornerRadius let dx = point.x - self.bounds.size.width / 2 let dy = point.y - radius let distace = sqrt(dx * dx + dy * dy) return distace < radius } return inside } ~~~ 如果你要實現非矩形的控件,那么請在開發時處理好這類問題。? 這里附上一個很容易測試的小 Demo: ~~~ class CustomView: UIControl { override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = UIColor.redColor() } required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.backgroundColor = UIColor.redColor() } override func layoutSubviews() { super.layoutSubviews() self.layer.cornerRadius = self.bounds.size.width / 2 } override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool { self.backgroundColor = UIColor.grayColor() return super.beginTrackingWithTouch(touch, withEvent: event) } override func endTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) { super.endTrackingWithTouch(touch, withEvent: event) self.backgroundColor = UIColor.redColor() } override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { let inside = super.pointInside(point, withEvent: event) if inside { let radius = self.layer.cornerRadius let dx = point.x - self.bounds.size.width / 2 let dy = point.y - radius let distace = sqrt(dx * dx + dy * dy) return distace < radius } return inside } } ~~~ * * * ### 合理使用 KVO 某些視圖的接口比較寶貴,被你用掉后外部的使用者就無法使用了,比如?`UITextField`?的?`delegate`,好在?`UITextField`?還提供了通知和?`UITextInput`方法可以使用;像?`UIScrollView`?或者基于 UIScrollView 的控件,你既不能設置它的 delegate,又沒有其他的替代方法可以使用,對于像以下這種需要根據某些屬性實時更新的控件來說,`KVO`?真是極好的: 這是一個動態高度 Header 的例子([DKStickyHeaderView](https://github.com/zhangao0086/DKStickyHeaderView)):? ![](https://box.kancloud.cn/2016-01-18_569ca44fcae05.jpg) 這個是一個固定在 Bottom 的例子([DKStickyFooterView](https://github.com/zhangao0086/DKStickyFooterView)):? ![](https://box.kancloud.cn/2016-01-18_569ca4505c831.jpg) 兩者都是基于 UIScrollView、基于 KVO ,不依賴外部參數: ~~~ override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) { if keyPath == KEY_PATH_CONTENTOFFSET { let scrollView = self.superview as! UIScrollView var delta: CGFloat = 0.0 if scrollView.contentOffset.y < 0.0 { delta = fabs(min(0.0, scrollView.contentOffset.y)) } var newFrame = self.frame newFrame.origin.y = -delta newFrame.size.height = self.minHeight + delta self.frame = newFrame } else { super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context) } } ~~~ 對容器類的?`ViewController`?來說也一樣有用。在 iOS8 之前沒有?`UIContentContainer`?這個正式協議,如果你要實現一個很長的、非列表、可滾動的 ViewController,那么你可能會將其中的功能分散到幾個?`ChildViewController`?里,然后把它們組合起來,這樣一來,這些?`ChildViewController`?既能被單獨作為一個 ViewController 展示,也可以被組合到一起。作為組合到一起的前提,就是需要一個至少有以下兩個方法的協議: > 1. 提供一個統一的輸入源,大多是一個 Model 或者像 userId 這樣的 > 2. 能夠返回你所需要的高度,比如設置 preferredContentSize 屬性 `ChildViewController`?動態地設置?`contentSize`,容器監聽?`contentSize`?的變化動態地設置約束或者 frame。 * * * 歡迎補充和討論
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看