1. 程式人生 > >iOS文件補完計劃--UIView

iOS文件補完計劃--UIView

UIView可以說是我們日常工作中接觸最多的一個物件、是所有檢視控制元件(不包括檢視控制器)的基類。
主要的功能包括檢視樣式、層級、約束、自動佈局、渲染、手勢、動畫、座標轉換等等。

其中有些東西(比如原生自動佈局、而我們平時都用mas/sd)並不常用、所以只篩選了一部分平時可能用得到的地方。
由於內容實在太多、所以有些複雜的地方只是簡單總結一下並且給出一些參考連結方便查閱


目錄主要分為以下幾個樣式:
常用、會用、瞭解

目錄

  • 建立檢視物件
  • 配置檢視的視覺外觀
    • backgroundColor
    • hidden
    • alpha
    • opaque
    • tintColor
    • tintAdjustmentMode
    • clipsToBounds
    • clearsContextBeforeDrawing
    • maskView
    • layerClass
    • layer
  • 配置與事件相關的行為
    • userInteractionEnabled
    • multipleTouchEnabled
    • exclusiveTouch
  • 配置邊界和框架矩形
    • frame
    • bounds
    • center
    • transform
  • 管理檢視層次結構
    • superview
    • subviews
    • window
    • addSubview
    • removeFromSuperview
    • bringSubviewToFront
    • sendSubviewToBack
    • insertSubview:atIndex:
    • insertSubview:aboveSubview:
    • insertSubview: belowSubview:
    • exchangeSubviewAtIndex:withSubviewAtIndex:
    • isDescendantOfView:
  • 觀察與檢視層級的更改
    • didAddSubview/willRemoveSubview等六個方法
  • 配置內容邊距
    • LayoutMargins相關
  • 螢幕的安全區域
    • safeAreaInsets
    • safeAreaLayoutGuide
    • safeAreaInsetsDidChange
    • insetsLayoutMarginsFromSafeArea
  • 測量Auto Layout
    • systemLayoutSizeFittingSize
    • systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority
    • intrinsicContentSize
    • invalidateIntrinsicContentSize
    • Compression Resistance priority(抗壓縮)
    • Hugging priority(抗拉伸)
  • 觸發自動佈局
    • needsUpdateConstraints
    • setNeedsUpdateConstraints
    • updateConstraints
    • updateConstraintsIfNeeded
  • 配置調整大小行為
    • contentMode
    • UIViewContentMode
    • sizeThatFits
    • sizeToFit
    • autoresizesSubviews
    • autoresizingMask
  • 佈局子檢視
    • layoutSubviews
    • setNeedsLayout
    • layoutIfNeeded
    • requiresConstraintBasedLayout
    • translatesAutoresizingMaskIntoConstraints
  • 繪製和更新檢視
    • drawRect
    • setNeedsDisplay
    • setNeedsDisplayInRect
    • contentScaleFactor
    • tintColorDidChange
  • 管理手勢識別器
    • 新增刪除和獲取
    • gestureRecognizerShouldBegin
  • 觀察焦點
    • canBecomeFocused
    • focused
  • 運動視覺效果
    • 新增刪除和獲取
  • 後臺啟動恢復
  • 捕獲檢視快照
  • 識別檢視
    • tag
    • viewWithTag
  • 座標系轉換
    • convertPoint
    • convertRect
    • 超出父檢視的View可以被點選
  • 命中測試(Hit-Testing)
    • hitTest:withEvent
    • pointInside:withEvent
    • 為響應鏈尋找最合適的FirstView
  • 結束檢視編輯
    • endEditing:
  • Block動畫
  • 首尾式動畫

UIView

包含了UIView的基本功能

  • userInteractionEnabled

設定使用者互動,預設YES允許使用者互動

@property(nonatomic,getter=isUserInteractionEnabled) BOOL userInteractionEnabled;
  • tag

控制元件標記(父控制元件可以通過tag找到對應的子控制元件),預設為0

@property(nonatomic)  NSInteger tag; 
  • 觀察焦點

  • 管理使用者介面方向

semanticContentAttribute(翻轉效果)

是否翻轉檢視

@property (nonatomic) UISemanticContentAttribute semanticContentAttribute NS_AVAILABLE_IOS(9_0);

世上總是有很多奇人異事、比如阿拉伯人的閱讀順序。
可以讓佈局自動左右翻轉、不過前提是佈局時使用Leading以及Trailing兩個約束條件而不是Left和Right。

獲取檢視方向

/** 獲取檢視的方向 */
+ (UIUserInterfaceLayoutDirection)userInterfaceLayoutDirectionForSemanticContentAttribute:(UISemanticContentAttribute)attribute NS_AVAILABLE_IOS(9_0);
 
/** 獲取相對於指定檢視的介面方向 */
+ (UIUserInterfaceLayoutDirection)userInterfaceLayoutDirectionForSemanticContentAttribute:(UISemanticContentAttribute)semanticContentAttribute relativeToLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection NS_AVAILABLE_IOS(10_0);
 
/** 返回即時內容的佈局的方向 */
@property (readonly, nonatomic) UIUserInterfaceLayoutDirection effectiveUserInterfaceLayoutDirection NS_AVAILABLE_IOS(10_0);


UIViewGeometry(幾何分類)

  • multipleTouchEnabled

是否允許多點觸控 預設NO

@property(nonatomic,getter=isMultipleTouchEnabled) BOOL multipleTouchEnabled;

建立檢視物件

/** 通過Frame初始化UI物件 */
- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
/** 用於xib初始化 */
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

配置檢視的視覺外觀

  • backgroundColor

檢視背景色

@property(nonatomic, copy) UIColor *backgroundColor;

預設值nil、也就是透明。

  • hidden

是否隱藏檢視

@property(nonatomic, getter=isHidden) BOOL hidden;

父檢視的隱藏會導致子檢視被隱藏。並且不能被點選、不能成為第一響應者

  • alpha

檢視透明度

@property(nonatomic) CGFloat alpha;

父檢視的透明度會應用到子檢視上。小於0.01則等於被隱藏了。

  • opaque

檢視是否不透明。主要是用於檢視混合

@property(nonatomic, getter=isOpaque) BOOL opaque;

UIView的預設值是YES、但UIButton等子類的預設值都是NO。
需要注意他並不是我們肉眼層面的透明。

這和檢視合成機制有關
簡而言之如果你的檢視alpha=1、完全可以將opaque=YES。讓GPU在混合檢視時不必考慮下方檢視顏色。

  • tintColor

色調顏色

@property(nonatomic, strong) UIColor *tintColor;
  1. 父檢視更改了tintColor為red,那麼它所有的一級子檢視tintColor全部為red。下一級也會根據前一級進行設定。
    除非你主動設定了子檢視的tintColor。
  2. 原生控制元件基本都有預設的tintColor、比如UIButton為藍色
    當然、也和建立的方式有關。UIButtonTypeCustom方式是沒有的。
  3. 當tintColor被修改、將會呼叫物件的tintColorDidChange:方法。
    個人感覺就這個玩意比較有用、畢竟我們更多的是自定義控制元件著色。不可能希望全螢幕變成一個顏色。

想進一步瞭解的話。這裡推薦一個部落格可以看一看: 《iOS tintColor解析》

  • tintAdjustmentMode

色調(tintColor)模式

typedef NS_ENUM(NSInteger, UIViewTintAdjustmentMode) {
    UIViewTintAdjustmentModeAutomatic,//檢視的著色調整模式與父檢視一致
    
    UIViewTintAdjustmentModeNormal,//預設值
    UIViewTintAdjustmentModeDimmed,//暗色
} NS_ENUM_AVAILABLE_IOS(7_0);

@property(nonatomic) UIViewTintAdjustmentMode tintAdjustmentMode NS_AVAILABLE_IOS(7_0);

改變這個屬性、也會呼叫tintColorDidChange:方法。

  • clipsToBounds

是否擷取掉超過子檢視超過自身的部分、預設為NO

@property(nonatomic)  BOOL  clipsToBounds; 

最大的用處還是切圓角和圖片吧。
需要注意的是layer有一個方法maskToBounds也是一個作用、clipsToBounds內部就是呼叫了maskToBounds
其實效果一樣、只不過從語義上來講分成Viewlayer兩個方法。

  • clearsContextBeforeDrawing

檢視重繪前是否先清理以前的內容,預設YES

@property(nonatomic)  BOOL   clearsContextBeforeDrawing;

如果你把這個屬性設為NO、那麼你要保證能在 drawRect:方法中正確的繪畫。
如果你的程式碼已經做了大量優化、那麼設為NO可以提高效能、尤其是在滾動時可能只需要重新繪畫檢視的一部分。
所以說、通常用不到。

  • maskView

遮罩層

@property(nullable, nonatomic,strong) UIView *maskView NS_AVAILABLE_IOS(8_0);
  1. 雖說是遮罩層、但實際上不會多出一個View。
    只是對顏色的混合有影響。
  2. 只會顯示出與maskView可見(不透明)部分重疊的部分。
  3. maskView的對應點的alpha會賦值給View對應的point。
  4. 與layer.mask基本相同、只是需要8.0的支援。

舉一個簡單的例子:

UIView * view1 = [[UIView alloc]initWithFrame:CGRectMake(0, 100, 200, 200)];
view1.backgroundColor = [UIColor blueColor];

UIView * maskView = [[UIView alloc]initWithFrame:view1.bounds];
maskView.backgroundColor = [UIColor clearColor];


UIView * view_1 = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 100, 200)];
view_1.backgroundColor = [UIColor whiteColor];
[maskView addSubview:view_1];

UIView * view_2 = [[UIView alloc]initWithFrame:CGRectMake(100, 0, 100, 200)];
view_2.backgroundColor = [UIColor whiteColor];
view_2.alpha = 0.5;
[maskView addSubview:view_2];

view1.maskView = maskView;
[self.view addSubview:view1];

你還可以通過layer或者圖片來設計出很多有趣的效果:《使用 maskView 設計動畫》《還有一個有趣的動畫庫》(layer.mask)

  • layerClass

返回當前View所使用的根Layer型別

#if UIKIT_DEFINE_AS_PROPERTIES
@property(class, nonatomic, readonly) Class layerClass;                        // default is [CALayer class]. Used when creating the underlying layer for the view.
#else
+ (Class)layerClass;                        // default is [CALayer class]. Used when creating the underlying layer for the view.
#endif

layer有很多種、比如CATextLayer適合文字、CAGradientLayer適合處理漸變、CAReplicatorLayer適合處理很多相似的圖層。

當然這些我都不太瞭解~你可以參閱《[iOS Animation]-CALayer 專用圖層》

  • layer

layer檢視圖層(可以用來設定圓角效果/陰影效果)

@property(nonatomic,readonly,strong) CALayer  *layer;  

配置與事件相關的行為

  • userInteractionEnabled

設定使用者互動、預設YES允許使用者互動。

@property(nonatomic,getter=isUserInteractionEnabled) BOOL userInteractionEnabled;

這個屬性直接影響到控制元件能否進入響應鏈或者成為第一響應者。
《iOS文件補完計劃--UIResponder》

  • multipleTouchEnabled

是否允許多指觸控。預設NO

@property(nonatomic,getter=isMultipleTouchEnabled) BOOL multipleTouchEnabled __TVOS_PROHIBITED;
  • exclusiveTouch

是否讓View獨佔Touch事件

@property(nonatomic,getter=isExclusiveTouch) BOOL       exclusiveTouch;

預設是NO。設定成YES避免在一個介面上同時點選多個button。


配置邊界和框架矩形

  • frame

位置和尺寸

@property(nonatomic) CGRect  frame;

以父控座標系的左上角為座標原點(0, 0)

  • bounds

位置和尺寸

@property(nonatomic) CGRect    bounds;

以自身標系的左上角為座標原點(0, 0)

  • center

中心點

@property(nonatomic) CGPoint   center;

以父控制元件的左上角為座標原點(0, 0)

  • transform

形變

@property(nonatomic) CGAffineTransform transform;

可以做一些Transform動畫、大概就是形變、縮放、位移咯。
比如你可以給cell做一些小動畫

NSArray *cells = tableView.visibleCells;
for (int i = 0; i < cells.count; i++) {
    UITableViewCell *cell = [cells objectAtIndex:i];
    if (i%2 == 0) {
        cell.transform = CGAffineTransformMakeTranslation(-BSScreen_Width,0);
    }else {
        cell.transform = CGAffineTransformMakeTranslation(BSScreen_Width,0);
    }
    [UIView animateWithDuration:duration delay:i*0.03 usingSpringWithDamping:0.75 initialSpringVelocity:1/0.75 options:0 animations:^{
        cell.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        
    }];
}

管理檢視層次結構

  • superview

獲取父檢視

@property(nullable, nonatomic,readonly) UIView *superview;
  • subviews

獲取所有子檢視

@property(nonatomic,readonly,copy) NSArray<__kindof UIView *> *subviews;

陣列的順序等於新增到父檢視上的順序。
你也可以嘗試用遞迴的方式遍歷所有子檢視嗯。

  • window

獲取檢視所在的Window

@property(nullable, nonatomic,readonly) UIWindow  *window;

這個我也不太懂幹嘛的。只知道如果父檢視是UIWindow一定有值、否則(我測試的都是)為空。

  • - addSubview:

新增子檢視

- (void)addSubview:(UIView *)view;

會被新增在subviews的末尾、檢視層級的最上方。

  • - removeFromSuperview

從父檢視上移除

- (void)removeFromSuperview;
  • - bringSubviewToFront:

移動指定的子檢視,使其顯示在其所以兄弟節點之上

- (void)bringSubviewToFront:(UIView *)view;
  • - sendSubviewToBack:

移動指定的子檢視,使其顯示在其所有兄弟節點之下

- (void)sendSubviewToBack:(UIView *)view;

自己試去吧~

UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(10, 150, 100, 50)];
view1.backgroundColor = [UIColor blueColor];
[self.view addSubview:view1];

UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(15, 155, 100, 50)];
view2.backgroundColor = [UIColor grayColor];
[self.view addSubview:view2];

UIView *view3 = [[UIView alloc] initWithFrame:CGRectMake(20, 160, 100, 50)];
view3.backgroundColor = [UIColor yellowColor];
[self.view addSubview:view3];

//如果將下面兩行程式碼都註釋掉   view1 會在下面   view2會在上面
//  下面這行程式碼能夠將view2  調整到父檢視的最下面
//    [self.view sendSubviewToBack:view2];
//將view調整到父檢視的最上面
[self.view bringSubviewToFront:view1];
  • - insertSubview:atIndex:

插入子檢視(將子檢視插入到subviews陣列中index這個位置)

- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index;
  • - insertSubview:aboveSubview:

插入子檢視(將子檢視插到siblingSubview之上)

- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview;
  • - insertSubview: belowSubview:

插入子檢視(將子檢視插到siblingSubview之下)

- (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview;
  • - exchangeSubviewAtIndex:withSubviewAtIndex:

交換兩個子檢視

- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2;
  • - isDescendantOfView:

檢測一個檢視是否屬於另一個的子檢視

- (BOOL)isDescendantOfView:(UIView *)view;

舉個例子:

(lldb) po [self.view isDescendantOfView:view1]
NO
(lldb) po [view1 isDescendantOfView:self.view]
YES

需要注意的是、這個判定並不侷限於一級結構。


觀察與檢視層級的更改

包含子檢視的成功新增/移除、本檢視新增到新父檢視的開始/結束。

一下方法預設不做任何操作。需要注意的是在removeFromSuperview時、也會呼叫。只不過newSuperview為空。

/** 新增自檢視完成後呼叫 */
- (void)didAddSubview:(UIView *)subview;
/** 將要移除自檢視時呼叫 */
- (void)willRemoveSubview:(UIView *)subview;
 
/** 將要移動到新父檢視時呼叫 */
- (void)willMoveToSuperview:(nullable UIView *)newSuperview;
/** 移動到新父檢視完成後呼叫 */
- (void)didMoveToSuperview;
/** 將要移動到新Window時呼叫 */
- (void)willMoveToWindow:(nullable UIWindow *)newWindow;
/** 移動到新Window完成後呼叫 */
- (void)didMoveToWindow;

配置內容邊距

官方文件對Content Margin的解釋

  • directionalLayoutMargins

iOS11 開始引入,可以根據語言的方向進行前後佈局,與 layoutMargins 相比,能更好的適配 RTL 語言。

@property (nonatomic) NSDirectionalEdgeInsets directionalLayoutMargins API_AVAILABLE(ios(11.0),tvos(11.0));

但和我們關係不大、除非某天我們進軍中東的某些國家了。

  • layoutMargins

自動佈局時。用於指定檢視和它的子檢視之間的邊距。

@property (nonatomic) UIEdgeInsets layoutMargins NS_AVAILABLE_IOS(8_0);

iOS11之後請使用directionalLayoutMargins屬性進行佈局。他將左右的概念替換成了前後。而且這兩個屬性會互相同步

預設為8個單位。如果檢視並不是完全處於安全區域內或者設定了preservesSuperviewLayoutMargins則可能更大。

不過說實話如果用masonry的話感覺這個屬性意義不大

  • preservesSuperviewLayoutMargins

是否將當前檢視的間距和父檢視相同。

@property (nonatomic) BOOL preservesSuperviewLayoutMargins NS_AVAILABLE_IOS(8_0);

設定一個檢視的邊距(檢視邊緣與其子檢視邊緣的距離)、防止其子檢視和父檢視邊緣重合。

  • -layoutMarginsDidChange

改變view的layoutMargins這個屬性時,會觸發這個方法

- (void)layoutMarginsDidChange NS_AVAILABLE_IOS(8_0);

用原生Layout來佈局說實話我沒用過。可以看看這篇帖子:《layoutMargins和preservesSuperviewLayoutMargins》


螢幕的安全區域

官方文件對的《Safe Area》解釋

iOS11之後、(為了幫助適配iPhoneX?)蘋果給我們引入了一個安全區域的概念。
安全區域幫助我們將view放置在整個螢幕的可視的部分。即使把navigationbar設定為透明的,系統也認為安全區域是從navigationbar的bottom開始,保證不被系統的狀態列、或導航欄覆蓋。

  • safeAreaInsets

反映了一個view距離該view的安全區域的邊距

@property (nonatomic,readonly) UIEdgeInsets safeAreaInsets API_AVAILABLE(ios(11.0),tvos(11.0));

這個屬性會被系統在佈局後自動設定。比如你可以在UIViewControllerviewSafeAreaInsetsDidChange方法下檢測橫屏以及豎屏下的變化。
對於UIView也有對應的方法safeAreaInsetsDidChange

  • safeAreaLayoutGuide

safeAreaLayoutGuide是一個相對抽象的概念,為了便於理解,我們可以把safeAreaLayoutGuide看成是一個“view”,這個“view”系統自動幫我們調整它的bounds,讓它不會被各種奇奇怪怪的東西擋住,包括iPhone X的劉海區域和底部的一道槓區域,可以認為在這個“view”上一定能完整顯示所有內容。

結合Masonry的用法:將四周與安全區域貼合

[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
    make.left.equalTo(self.view.mas_safeAreaLayoutGuideLeft);
    make.right.equalTo(self.view.mas_safeAreaLayoutGuideRight);
    make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom);
}];
  • - safeAreaInsetsDidChange

當View的safeAreaInsets發生變化時自動呼叫

- (void)safeAreaInsetsDidChange API_AVAILABLE(ios(11.0),tvos(11.0));
  • insetsLayoutMarginsFromSafeArea

決定在自動佈局時是否考慮safeAreaInsets的限制

@property (nonatomic) BOOL insetsLayoutMarginsFromSafeArea

示例程式碼:

self.view.insetsLayoutMarginsFromSafeArea = NO;
self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
self.tableView.backgroundColor = [UIColor orangeColor];
self.tableView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.tableView];

NSArray<__kindof NSLayoutConstraint *> *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[tableView]-|" options:0 metrics:nil views:@{@"tableView" : self.tableView}];
[self.view addConstraints:constraints];

constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[tableView]|" options:0 metrics:nil views:@{@"tableView" : self.tableView}];
[self.view addConstraints:constraints];

關於iOS11安全區域的特性、推薦一篇Bugly的帖子《iOS 11 安全區域適配總結》


測量Auto Layout

  • - systemLayoutSizeFittingSize:

返回Auto Layout後內容高度。

- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize NS_AVAILABLE_IOS(6_0);

通常用於包含多層檢視的控制元件計算(比如cell)。這裡注意與sizeThatFitssizeToFits進行區分。

UILabel *label = [[UILabel alloc] init];
label.text = @"SafeAreaS";
label.numberOfLines = 0;
label.textAlignment = NSTextAlignmentCenter;
label.backgroundColor = [UIColor greenColor];
[self.view addSubview:label];

[label mas_makeConstraints:^(MASConstraintMaker *make) {
    if (@available(iOS 11, *)) {
        make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop);
        make.left.equalTo(self.view.mas_safeAreaLayoutGuideLeft);
    } else {
        make.top.equalTo(self.mas_topLayoutGuideBottom);
        make.left.mas_equalTo(0);
    }

}];

[self.view layoutIfNeeded];
CGSize layoutSize =  [label systemLayoutSizeFittingSize:UILayoutFittingExpandedSize];
CGSize labelSize = label.frame.size;

//(lldb) po layoutSize
//(width = 79.666666666666671, height = 20.333333333333332)
//
//(lldb) po labelSize
//(width = 79.666666666666671, height = 20.333333333333332)

利用這個方法、我們可以對是適應高度的Cell高度進行快取來適配iOS8以下的情況。需要注意的是要對cell.contentView進行計算。

model.cellHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
  • systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority:

和上一個方法一樣、但是增加了寬高的優先順序。使結果更加準確

- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority NS_AVAILABLE_IOS(8_0);
  • intrinsicContentSize

返回控制元件的固有大小

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) CGSize intrinsicContentSize NS_AVAILABLE_IOS(6_0);
#else
- (CGSize)intrinsicContentSize NS_AVAILABLE_IOS(6_0);
#endif

在自動佈局時、有些控制元件(UILabel/UIButton/UIImageView等)只需要設定位置而不需要設定大小、就是利用這個屬性。

當內容改變時、呼叫invalidateIntrinsicContentSize通知系統、並且自定義intrinsicContentSize的實現、來返回合適的寬高。

舉個例子、你可以讓你的UITextField能夠自適應寬度《一個隨輸入文字寬度變化的自定義UITextField》

  • - invalidateIntrinsicContentSize

廢除檢視原本內容的intrinsicContentSize

- (void)invalidateIntrinsicContentSize NS_AVAILABLE_IOS(6_0); 

上面已經說過用法了

  • 抗壓縮與抗拉伸

Compression Resistance priority(抗壓縮)

有多大的優先順序阻止自己變小

/* 返回某個方向的抗壓縮等級 */
- (UILayoutPriority)contentCompressionResistancePriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
/* 設定某個方向的抗壓縮等級*/
- (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);

Hugging priority(抗拉伸)

有多大的優先順序阻止自己變大

/* 返回某個方向上的抗拉伸等級 */
- (UILayoutPriority)contentHuggingPriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
/* 設定某個方向上的抗拉伸等級*/
- (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);

通常是給UILabel用的《HuggingPriority和CompressionResistance 一個例子教你理解》


觸發自動佈局

  • - needsUpdateConstraints

通知使用者是否有需要更新的約束

- (BOOL)needsUpdateConstraints NS_AVAILABLE_IOS(6_0);

這個可以作為你判斷是否應該在後續程式碼裡主動呼叫updateConstraints的前提。不過通常我們都不太需要、因為佈局的修改都是自己寫的自然清楚何時呼叫。
還有就是當View尚未展示時、更新的標記一直會返回YES。

  • - setNeedsUpdateConstraints

標記準備更改約束

- (void)setNeedsUpdateConstraints NS_AVAILABLE_IOS(6_0);

通常是用作批量修改約束時的優化、避免系統多次計算。

  • - updateConstraints

當View本體的佈局被修改時被自動呼叫

- (void)updateConstraints NS_AVAILABLE_IOS(6_0) NS_REQUIRES_SUPER;

你可以在這裡自己更新View內部控制元件的佈局、需要呼叫super實現。
會被呼叫的情況:

  1. 檢視被新增到父檢視上
  2. 主動呼叫updateConstraintsIfNeeded
  3. 更新約束時

這裡需要注意的是更新約束後自動呼叫的時機:

  1. 如果你使用NSLayoutConstraint對約束進行更新
    updateConstraints方法會自動呼叫
  2. 如果使用Masonry對約束進行更新
    updateConstraints方法不會自動呼叫
    你需要手動setNeedsUpdateConstraints然後updateConstraintsIfNeeded
  • - updateConstraintsIfNeeded

立即觸發約束更新,自動更新佈局

- (void)updateConstraintsIfNeeded NS_AVAILABLE_IOS(6_0);

會把之前setNeedsUpdateConstraints標記之後的所有約束脩改、同時更新。
並且(如果控制元件已經被setNeedsUpdateConstraints標記)自動呼叫控制元件的updateConstraints方法。

所以、方法呼叫的順序為:

檢視的佈局改變時:updateConstraints被執行。
檢視佈局修改:setNeedsUpdateConstraints-->updateConstraintsIfNeeded
而當setNeedsUpdateConstraints被呼叫needsUpdateConstraints也會返回YES作為標記。

貼一個Masoney動畫的例子:《Masonry自動佈局詳解二:動畫更新約束》
但是這個帖子有些問題

// 告訴self.view約束需要更新
[self.view setNeedsUpdateConstraints];
// 呼叫此方法告訴self.view檢測是否需要更新約束,若需要則更新,下面新增動畫效果才起作用
[self.view updateConstraintsIfNeeded];

這兩句話並沒什麼必要、因為我並沒有更新self.view的約束。直接修改成這樣、是比較好的。

- (void)onGrowButtonTaped:(UIButton *)sender {
    self.scacle += 0.5;

    [self.growingButton mas_updateConstraints:^(MASConstraintMaker *make) {
        make.center.mas_equalTo(self.view);
        
        // 初始寬、高為100,優先順序最低
        make.width.height.mas_equalTo(100 * self.scacle);
        // 最大放大到整個view
        make.width.height.lessThanOrEqualTo(self.view);
    }];
    
    [UIView animateWithDuration:0.3 animations:^{
        [self.view layoutIfNeeded];
    }];
}

這也佐證了一個問題。layoutIfNeeded執行動畫與setNeedsUpdateConstraints/updateConstraintsIfNeeded並沒什麼直接關係。


配置調整大小行為

  • contentMode

內容顯示的模式。預設`UIViewContentModeScaleToFill

@property(nonatomic) UIViewContentMode contentMode;  

我們平時調整圖片顯示狀態用的就是這個屬性

  • UIViewContentMode

內容具體的顯示模式

typedef NS_ENUM(NSInteger, UIViewContentMode) {
    UIViewContentModeScaleToFill,       //!< 縮放內容到合適比例大小.
    UIViewContentModeScaleAspectFit,    //!< 縮放內容到合適的大小,邊界多餘部分透明.
    UIViewContentModeScaleAspectFill,   //!< 縮放內容填充到指定大小,邊界多餘的部分省略.
    UIViewContentModeRedraw,            //!< 重繪檢視邊界 (需呼叫 -setNeedsDisplay).
    UIViewContentModeCenter,            //!< 檢視保持等比縮放.
    UIViewContentModeTop,               //!< 檢視頂部對齊.
    UIViewContentModeBottom,            //!< 檢視底部對齊.
    UIViewContentModeLeft,              //!< 檢視左側對齊.
    UIViewContentModeRight,             //!< 檢視右側對齊.
    UIViewContentModeTopLeft,           //!< 檢視左上角對齊.
    UIViewContentModeTopRight,          //!< 檢視右上角對齊.
    UIViewContentModeBottomLeft,        //!< 檢視左下角對齊.
    UIViewContentModeBottomRight,       //!< 檢視右下角對齊.
};
  • - sizeThatFits:

計算內容最合適的大小、但並不改變view的size

- (CGSize)sizeThatFits:(CGSize)size;

通常用於leaf-level views、這裡注意與systemLayoutSizeFittingSize進行區分。

這裡的引數size、類似於UILabel的preferredMaxLayoutWidth屬性用於限制計算範圍。

你可以自己試試

UITextView * textView = [[UITextView alloc]initWithFrame:CGRectMake(0, 100, 20, 20)];
[self.view addSubview:textView];
//textView.text = @"asdasdasdasdasdasdasda\nasasdasdasdasdasdasdasda\nasasdasdasdasdasdasdasda\nasasdasdasdasdasdasdasda\nasasdasdasdasdasdasdasda\nasasdasdasdasdasdasdasda\nasasdasdasdasdasdasdasda\nas";

textView.text = @"asdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasdaasdasdasdasdasdasdasda";
textView.backgroundColor = [UIColor orangeColor];


CGSize s = [textView sizeThatFits:CGSizeMake(self.view.bounds.size.width, 999)];
textView.frame = CGRectMake(textView.frame.origin.x, textView.frame.origin.y, s.width, s.height);

如果我們想讓UITextField等控制元件也自適應、重寫intrinsicContentSize內部用sizeThatFits:CGSizeMake計算一下就好了唄。

  • - sizeToFit

計算內容最合適的大小、並改變view的size

- (void)sizeToFit;

會使用控制元件原有的寬高進行計算、等價於:

CGSize s = [textView sizeThatFits:textView.frame.size];
textView.frame = CGRectMake(textView.frame.origin.x, textView.frame.origin.y, s.width, s.height);

注意如果修改了size、會呼叫layoutSubviews

  • autoresizesSubviews

當本身大小發生改變時、是否自動佈局子檢視

@property(nonatomic) BOOL  autoresizesSubviews;

如果檢視的autoresizesSubviews屬性宣告被設定為YES,則其子檢視會根據autoresizingMask屬性的值自動進行尺寸調整。

  • autoresizingMask

當父檢視autoresizesSubviewsYES並且改變了大小時、該子檢視的佈局規則。

@property(nonatomic) UIViewAutoresizing autoresizingMask;

現在基本都是Auto Layout、很少用到這兩個東西了。
可以瞭解一下《IOS-AutoresizesSubviews》


佈局子檢視

  • - layoutSubviews

當控制元件被(系統)賦予了一個新的大小時觸發。

- (void)layoutSubviews;
  1. 新增到螢幕時觸發
    必須有指定的rect

  2. 呼叫setNeedsLayout時觸發

  3. size發生改變時觸發。
    觸發次數需要滿足最後兩點規則

  4. 滑動scrollview時觸發

  5. 旋轉螢幕時觸發

  6. 系統賦予大小
    我們都知道使用者所設定的frame/layout並不會直接修改控制元件frame、而是會在特定的週期由系統進行佈局繪製。也就是說、我們在一個週期內連續設定多次frame/layout、系統也只會在週期結束時佈局一次、並觸發一次layoutSubviews

  7. 控制元件只有被新增到螢幕上、才能觸發layoutSubviews。
    通常都會觸發兩次、因為你還得給他設定frame/layout。
    不過如果先設定frame然後隔了很久才新增到螢幕上、就是一次。

  • - setNeedsLayout

為該控制元件設定標記。等待更新佈局

- (void)setNeedsLayout;

setNeedsUpdateConstraints的機制一樣、他允許你對其多個子View進行佈局後統一更新。

注意它並不是實時更新、而會在下一次佈局週期中進行統一更新。

  • - layoutIfNeeded

立即更新該View所有子檢視的佈局

- (void)layoutIfNeeded;

它會將所有尚未更新的佈局立即進行更新。

通常的用處有兩個(歡迎補充):
1.佈局尚未完成、但我們需要獲取具體的Frame(當然、根據情況你也可以使用systemLayoutSizeFittingSize以節省效能)

UIView * view = [UIView new];

[self.view addSubview:view];

[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.left.right.equalTo(self.view);
    make.height.mas_equalTo(50);
}];

CGSize s = [view systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
NSLog(@"%lf",s.height);//50.000000

NSLog(@"%lf",view.frame.size.height);//0.000000
[self.view layoutIfNeeded];
NSLog(@"%lf",view.frame.size.height);//50.000000
  1. 讓約束通過動畫更新
    貼一個Masoney動畫的例子:《Masonry自動佈局詳解二:動畫更新約束》
  • requiresConstraintBasedLayout

標記View是否需要用AutoLayout進行佈局

#if UIKIT_DEFINE_AS_PROPERTIES
@property(class, nonatomic, readonly) BOOL requiresConstraintBasedLayout NS_AVAILABLE_IOS(6_0);
#else
+ (BOOL)requiresConstraintBasedLayout NS_AVAILABLE_IOS(6_0);
#endif

這個說實話不太懂、看文件和網上的意思都是如果不返回YES、有可能不呼叫updateConstraints方法。但是我自己寫起來總會呼叫、希望有大神指正。
至於為什麼要呼叫updateConstraints:《masonry小問題之requiresConstraintBasedLayout》

  • translatesAutoresizingMaskIntoConstraints

是否將AutoresizingMask轉化成Constraints約束。預設為YES

@property(nonatomic) BOOL translatesAutoresizingMaskIntoConstraints NS_AVAILABLE_IOS(6_0);

名字已經把這個屬性的作用說的很明白了、但我們還可以再解釋一下:

  1. 當我們用程式碼新增檢視時:
    檢視的translatesAutoresizingMaskIntoConstraints屬性預設為YES,可是AutoresizingMask屬性預設會被設定成None。也就是說如果我們不去動AutoresizingMask,那麼AutoresizingMask就不會對約束產生影響。
  2. 當我們使用interface builder新增檢視時:
    AutoresizingMask雖然會被設定成非None,但是translatesAutoresizingMaskIntoConstraints預設被設定成了NO。所以也不會有衝突。
  3. 會出現問題的情況:
    當有一個檢視是靠AutoresizingMask佈局的,而我們修改了translatesAutoresizingMaskIntoConstraints後會讓檢視失去約束,走投無路。例如我自定義轉場時就遇到了這樣的問題,轉場後的檢視並不在檢視的正中間。

不過這些問題似乎只有用NSLayoutConstraint才能體現出問題?

參考《AutoLayout的那些事兒》《程式碼新增constraint,設定translatesAutoresizingMaskIntoConstraints為NO的原因》


繪製和更新檢視

  • - drawRect:

自定義的繪製內容

- (void)drawRect:(CGRect)rect;

1.如果是UIView則不需要呼叫super、UIView子類需要呼叫super實現。

  1. 不要手動呼叫。
    可以通過setNeedsDisplay或者setNeedsDisplayInRect讓系統自己呼叫。
  2. UIImageView不允許使用重寫draw繪製
    因為他本身也不使用draw繪製、僅僅是使用內部的image view來顯示影象
  3. 如果自己實現了drawRect、那務必每次都實現它
    setNeedsDisplay會將繪製全部清空。系統自動呼叫時也是。
    setNeedsDisplayInRect則會清空指定的rect。

呼叫的時機:

1. 檢視第一次被新增到父檢視上的時候、由系統自動呼叫
需要注意及時hidden = YES或者alpha = 0也會呼叫、但remove後再add並不會。
2. 新增到父檢視時必須有給定的rect、才會被自動呼叫
也就是size必須不為{0,0}
3. 修改了rect被呼叫自動呼叫
4.setNeedsDisplay+setNeedsDisplayInRect
當然、必須有rect

具體使用:

在iOS中使用drawRect繪圖一般分為以下5個步驟:
1、獲取繪圖上下文
CGContextRef context = UIGraphicsGetCurrentContext();
2、建立並設定路徑
3、將路徑新增到上下文
如:線寬、線條顏色、填充顏色等
4、設定上下文狀態
5、繪製路徑
6、釋放路徑

具體可以參考:《drawRect的繪製的使用(繪製文字字元、繪製圖片、繪製圖形)》

使用DrawRect是會有一定效能問題的:

  1. contents寄宿圖
    《記憶體惡鬼drawRect》
  2. 離屏渲染
    《深刻理解移動端優化之離屏渲染》

這塊我有空準備補一下、先打個卡:
《iOS繪製和渲染》

  • - setNeedsDisplay

標記全部重繪

- (void)setNeedsDisplay;

需要注意的是並不會立即重繪、而是等到下一個週期

  • - setNeedsDisplayInRect:

標記指定rect重繪

- (void)setNeedsDisplayInRect:(CGRect)rect;

需要注意的是並不會立即重繪、而是等到下一個週期。

  • contentScaleFactor

檢視內容的縮放比例

@property(nonatomic) CGFloat  contentScaleFactor NS_AVAILABLE_IOS(4_0);

修改contentScaleFactor可以讓UIView的渲染精度提高,這樣即使在CGAffineTransform放大之後仍然能保持銳利

  • - tintColorDidChange

tintColor或者tintAdjustmentMode被修改時系統呼叫

- (void)tintColorDidChange NS_AVAILABLE_IOS(7_0);

你可以把他當成一個監聽來使用

- (void)tintColorDidChange {
  _tintColorLabel.textColor = self.tintColor;
  _tintColorBlock.backgroundColor = self.tintColor;
}

管理手勢識別器

  • 新增刪除和獲取

** 當前檢視所附加的所有手勢識別器 */
@property(nullable, nonatomic,copy) NSArray<__kindof UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);
 
/** 新增一個手勢識別器 */
- (void)addGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer NS_AVAILABLE_IOS(3_2);
/** 移除一個手勢識別器 */
- (void)removeGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer NS_AVAILABLE_IOS(3_2);
  • - gestureRecognizerShouldBegin

是否繼續識別手勢。

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer NS_AVAILABLE_IOS(6_0);

此方法在gesture recognizer檢視轉出UIGestureRecognizerStatePossible狀態時呼叫,如果返回NO,則轉換到UIGestureRecognizerStateFailed;如果返回YES,則繼續識別觸控序列.(預設情況下為YES)。

你可以用來在控制元件指定的位置使用手勢識別

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
        UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
        CGPoint translation = [pan translationInView:self.view];
        CGFloat offsetX = self.scrollView.contentOffset.x;
        if (translation.x > 0 && offsetX == 0.0 ) {
               return NO;
        }
    }
    return YES;
}

觀察焦點

canBecomeFocused

返回是否可以成為焦點, 預設NO

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic,readonly) BOOL canBecomeFocused NS_AVAILABLE_IOS(9_0); // NO by default
#else
- (BOOL)canBecomeFocused NS_AVAILABLE_IOS(9_0); // NO by default
#endif

焦點是給AppleTV用的、用遙控器選擇螢幕上的控制元件。

在一個以焦點為基礎的互動模型中,在螢幕上的單一檢視可以得到焦點,並且使用者可以通過瀏覽螢幕上不同的UI選項將焦點移動到其他檢視,從而引起焦點更新。得到焦點的檢視被用作任何使用者操作的目標事件。例如,如果一個螢幕上的按鈕被選中,當由遙控器傳送按鈕選擇事件時,目標事件將被觸發。

focused

返回是否已經被聚焦

@property (readonly, nonatomic, getter=isFocused) BOOL focused NS_AVAILABLE_IOS(9_0);

運動視覺效果

就是王者榮耀那種可以晃手機看背景圖的效果吧

  • 新增刪除和獲取

新增、刪除、檢視、我沒用過知道就得了


/** 新增運動效果,當傾斜裝置時檢視稍微改變其位置 */
- (void)addMotionEffect:(UIMotionEffect *)effect NS_AVAILABLE_IOS(7_0);
 
/** 移除運動效果 */
- (void)removeMotionEffect:(UIMotionEffect *)effect NS_AVAILABLE_IOS(7_0);
 
/** 所有新增的運動效果 */
@property (copy, nonatomic) NSArray<__kindof UIMotionEffect *> *motionEffects NS_AVAILABLE_IOS(7_0);

後臺啟動恢復

你可以讓檢視在後臺恢復時候仍然保持原有的樣子

/** 標示是否支援儲存,恢復檢視狀態資訊 */
@property (nullable, nonatomic, copy) NSString *restorationIdentifier NS_AVAILABLE_IOS(6_0);
/** 儲存檢視狀態相關的資訊 */
- (void) encodeRestorableStateWithCoder:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);
/** 恢復和保持檢視狀態相關資訊 */
- (void) decodeRestorableStateWithCoder:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);

UIView的例子就不舉了、看看UIViewController的挺好《iOS的App實現狀態恢復》

與新增定位或者播放音訊等真後臺方式不同、這個僅僅是恢復頁面。


捕獲檢視快照

  • - snapshotViewAfterScreenUpdates:

對某個檢視進行快照

- (UIView *)snapshotViewAfterScreenUpdates:(BOOL)afterUpdates;

該方法有一個BOOL型別的引數、這個引數表示是否立即生成快照、還是在需要更新檢視的時候生成。

UIView *showView = [[UIView alloc] initWithFrame:CGRectMake(0, 100, 100, 100)];

showView.backgroundColor = [UIColor redColor];

[self.view addSubview:showView];

self.vvvv = showView;

UIView *snap1 = [showView snapshotViewAfterScreenUpdates:NO];

snap1.center = self.view.center;

[self.view addSubview:snap1];

設定YES、會等到當前佇列的所有方法完成之後、才會生成快照。
在設定NO的情況、延時生成快照、也能達到YES的效果、原理是一樣的。

  • - resizableSnapshotViewFromRect:afterScreenUpdates:withCapInsets:

比上面的方法多了兩個引數、意味著你可以把檢視進行分割操作

- (UIView *)resizableSnapshotViewFromRect:(CGRect)rect 
                       afterScreenUpdates:(BOOL)afterUpdates 
                            withCapInsets:(UIEdgeInsets)capInsets;
  • - drawViewHierarchyInRect:afterScreenUpdates:

比之前的多了一個rect引數、其他並沒發現什麼去區別~

- (BOOL)drawViewHierarchyInRect:(CGRect)rect 
             afterScreenUpdates:(BOOL)afterUpdates;

關於afterUpdates引數:
儘量設定為NO、否則如果檢視中途被釋放掉會殷勤crash。


識別檢視

  • tag

識別標識、預設為0

@property(nonatomic) NSInteger tag;
  • - viewWithTag

範圍子View中某個tag的View

- (__kindof UIView *)viewWithTag:(NSInteger)tag;
  1. 搜尋包括二級子檢視
  2. 以佇列的形式搜尋、搜尋到一個則返回。

座標系轉換

將一個View中的Rect或Point轉化到另一個View的座標系中

/** 將point由point所在檢視轉換到目標檢視view中,返回在目標檢視view中的point值 */
- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
/** 將point由point所在檢視轉換到目標檢視view中,返回在目標檢視view中的point值 */
- (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;
/** 將rect由rect所在檢視轉換到目標檢視view中,返回在目標檢視view中的rect */
- (CGRect)convertRect:(CGRect)rect toView:(nullable UIView *)view;
/** 將rect從view中轉換到當前檢視中,返回在當前檢視中的rect */
- (CGRect)convertRect:(CGRect)rect fromView:(nullable UIView *)view;

每種轉換都有兩個方法、但實際向作用都一樣。
無非是引數位置的區別~
將view1中的一個檢視、轉化到view2的座標系中

[view1 convertRect:view1.childView toView:view2];
[view2 convertRect:view1.childView fromView:view1];

需要注意的是view引數如果為nil、則會返回基於當前window的座標。
並且、兩個view必須歸屬於同一個window。

比如讓超出父檢視的View可以被點選

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *view = [super hitTest:point withEvent:event];
    if (view == nil) {
        for (UIView *subView in self.subviews) {
            //將point從self的座標系、對映給subView的座標系
            CGPoint myPoint = [subView convertPoint:point fromView:self];
            //判斷point是否在subView上
            if (CGRectContainsPoint(subView.bounds, myPoint)) {
                return subView;
            }
        }
    }
    return view;
}

需要注意的是

如果你想讓某個控制元件攔截某個事件、為了保險起見儘量修改父檢視的hitTest方法、以免被其他原本能夠響應的控制元件捷足先登。


命中測試(Hit-Testing)

  • - hitTest:withEvent:

詢問事件在當前檢視中的響應者,同時又是作為事件傳遞的橋樑

- (UIView *)hitTest:(CGPoint)point 
          withEvent:(UIEvent *)event;

上面例子中過載的方法便是這個。

以下幾種狀態的檢視無法響應事件:

  1. 不允許互動
    userInteractionEnabled = NO
  2. 隱藏
    hidden = YES 如果父檢視隱藏,那麼子檢視也會隱藏,隱藏的檢視3. 無法接收事件
    透明度
    alpha < 0.01 如果設定一個檢視的透明度<0.01,會直接影響子檢視的透明度。alpha:0.0~0.01為透明。

預設情況下:

  1. 若當前檢視無法響應事件
    則返回nil
  2. 若當前檢視可以響應事件
    但無子檢視可以響應事件、則返回自身作為當前檢視層次中的事件響應者
  3. 若當前檢視可以響應事件
    同時有子檢視可以響應、則返回子檢視層次中的事件響應者

其內部實現大致為:

  1. 看自身能否響應時間
  2. 看點是否在自身上
  3. 看子檢視是否能夠響應
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    //3種狀態無法響應事件
     if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil; 
    //觸控點若不在當前檢視上則無法響應事件
    if ([self pointInside:point withEvent:event] == NO) return nil; 
    //從後往前遍歷子檢視陣列 
    int count = (int)self.subviews.count; 
    for (int i = count - 1; i >= 0; i--) 
    { 
        // 獲取子檢視
        UIView *childView = self.subviews[i]; 
        // 座標系的轉換,把觸控點在當前檢視上座標轉換為在子檢視上的座標
        CGPoint childP = [self convertPoint:point toView:childView]; 
        //詢問子檢視層級中的最佳響應檢視
        UIView *fitView = [childView hitTest:childP withEvent:event]; 
        if (fitView) 
        {
            //如果子檢視中有更合適的就返回
            return fitView; 
        }
    } 
    //沒有在子檢視中找到更合適的響應檢視,那麼自身就是最合適的
    return self;
}
  • - pointInside:withEvent:

判斷觸控點是否在自身座標範圍內

- (BOOL)pointInside:(CGPoint)point 
          withEvent:(UIEvent *)event;

預設實現是若在座標範圍內則返回YES,否則返回NO。

所以之前那個超出範圍點選的方法中

if (CGRectContainsPoint(subView.bounds, myPoint)) {
    return subView;
}

換成

if ([subView pointInside:myPoint withEvent:event]) {
    return subView;
}

也是一樣可行的。

通過修改pointInside的判定《擴大UIButton的點選範圍》

  • 為響應鏈尋找最合適的FirstView

從事件傳遞到APP中開始、尋找最合適的View
UIApplication -> UIWindow -> 父View -> 子view

  1. 逐級呼叫hitTest:withEvent方法
  2. hitTest:withEvent方法內部通過pointInside:withEvent:進行判斷。
    通過則返回自身

結束檢視編輯

  • - endEditing:

強制讓自身或者子檢視上的UIR