03-iOS 多媒体技术| 图形处理框架-UIKit要点回顾2【UIView、UIViewController、UIWindow、生命周期、事件响应者链等】

1,822 阅读53分钟

前言

本篇文章紧接着上一篇文章中图形处理框架-UIKit要点回顾1继续讨论UIKit核心要点:

  • UIView
  • UIViewController
  • UIWindow
  • 事件响应者链

一、UIView

1. UIView 简介

官方对UIView的介绍:

  • UIView是App构建用户界面的基础模块,该类UIView定义所有View公共的行为
  • 它是所有可视化视图内容的基类,可以包含按钮标签文本字段图像等内容,并可以响应用户交互。
  • 以下是 UIView 的一些主要特点和功能:
  • 绘图和布局
    • UIView 可以在屏幕上绘制内容,并管理其子视图的布局。
    • 通过实现 draw(_:) 方法可以自定义视图的绘制。
    • 使用 subviews 属性可以访问视图的子视图,通过添加、删除、调整子视图的位置和大小来管理布局。
  • 用户交互
    • UIView 可以响应用户的触摸事件,如单击、双击、长按等。
    • 通过添加手势识别器(UIGestureRecognizer)来识别和处理特定的手势。
    • 通过实现 touchesBegan(_:with:)touchesMoved(_:with:)touchesEnded(_:with:) 等方法来处理触摸事件。
  • 视图层级结构
    • 视图可以嵌套在其他视图中,形成层级结构。
    • 通过调整视图在层级结构中的顺序,可以控制视图的显示顺序和覆盖关系。
  • 动画效果
    • 使用 UIView 的动画方法(如 animate(withDuration:animations:))可以实现简单的动画效果,如淡入淡出、移动、缩放等。
  • 视图属性
    • UIView 具有许多属性,用于控制其外观和行为,如背景颜色、边框、阴影等。
    • 可以通过属性设置来自定义视图的外观和行为,或者通过子类化来创建自定义的视图类型。
  • 自动布局
    • UIView 支持自动布局(Auto Layout),可以使用约束(Constraints)来描述视图之间的相对位置和大小关系。
    • 自动布局可以适应不同尺寸的屏幕和设备,提供了灵活的界面设计方案。

2. 基本组成部分|属性和方法

typedef NS_ENUM(NSInteger, UISemanticContentAttribute) {
    UISemanticContentAttributeUnspecified = 0,  //!< 未指定,默认值
    UISemanticContentAttributePlayback,         //!< 打开/ RW / FF等播放控制按钮
    UISemanticContentAttributeSpatial,          //!< 控制导致某种形式的定向改变UI中,如分段控制文本对齐方式或在游戏中方向键
    UISemanticContentAttributeForceLeftToRight, //!< 视图总是从左向右布局.
    UISemanticContentAttributeForceRightToLeft  //!< 视图总是从右向左布局.
} NS_ENUM_AVAILABLE_IOS(9_0);

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, CALayerDelegate>
 
/** 返回主layer所使用的类 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(class, nonatomic, readonly) Class layerClass;
#else
+ (Class)layerClass;
#endif
 
/** 通过Frame初始化UI对象 */
- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
/** 用于xib初始化 */
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
 
/** 设置用户交互,默认YES允许用户交互 */
@property(nonatomic,getter=isUserInteractionEnabled) BOOL userInteractionEnabled;
/** 控件标记(父控件可以通过tag找到对应的子控件),默认为0 */
@property(nonatomic)                                 NSInteger tag;
/** 视图图层(可以用来设置圆角效果/阴影效果) */
@property(nonatomic,readonly,strong)                 CALayer  *layer;
 
/** 返回是否可以成为焦点, 默认NO */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic,readonly) BOOL canBecomeFocused NS_AVAILABLE_IOS(9_0);
#else
- (BOOL)canBecomeFocused NS_AVAILABLE_IOS(9_0);
#endif
/** 是否可以被聚焦 */
@property (readonly, nonatomic, getter=isFocused) BOOL focused NS_AVAILABLE_IOS(9_0);
 
/** 左右滑动翻转效果 */
@property (nonatomic) UISemanticContentAttribute semanticContentAttribute NS_AVAILABLE_IOS(9_0);
 
/** 获取视图的方向 */
+ (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);
 
@end

3. 几何特性相关|frame、bounds、center、transform

/** 自动调整大小方式 */
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,     //!< 不自动调整.
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,//!< 自动调整与superView左边的距离,保证与superView右边的距离不变.
    UIViewAutoresizingFlexibleWidth        = 1 << 1,//!< 自动调整自己的宽度,保证与superView左边和右边的距离不变.
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,//!< 自动调整与superView的右边距离,保证与superView左边的距离不变.
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,//!< 自动调整与superView顶部的距离,保证与superView底部的距离不变.
    UIViewAutoresizingFlexibleHeight       = 1 << 4,//!< 自动调整自己的高度,保证与superView顶部和底部的距离不变.
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5 //!< 自动调整与superView底部的距离,也就是说,与superView顶部的距离不变.
};

@interface UIView(UIViewGeometry)
 
/** 位置和尺寸(以父控件的左上角为坐标原点(0, 0)) */
@property(nonatomic) CGRect            frame;
 
/** 位置和尺寸(以自己的左上角为坐标原点(0, 0)) */
@property(nonatomic) CGRect            bounds;
/** 中心点(以父控件的左上角为坐标原点(0, 0)) */
@property(nonatomic) CGPoint           center;
/** 变形属性(平移\缩放\旋转) */
@property(nonatomic) CGAffineTransform transform;
/** 视图内容的缩放比例 */
@property(nonatomic) CGFloat           contentScaleFactor NS_AVAILABLE_IOS(4_0);
 
/** 是否支持多点触摸,默认NO */
@property(nonatomic,getter=isMultipleTouchEnabled) BOOL multipleTouchEnabled __TVOS_PROHIBITED;
/** 是否独占整个Touch事件,默认NO */
@property(nonatomic,getter=isExclusiveTouch) BOOL       exclusiveTouch __TVOS_PROHIBITED;
 
/** 在指定点上点击测试指定事件 */
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
/** 判断当前的点击或者触摸事件的点是否在当前的view中,默认返回YES */
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
 
/** 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值 */
- (CGPoint)convertPoint:(CGPoint)point toView:(nullable UIView *)view;
/** 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值 */
- (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;
 
/** 自动调整子视图尺寸,默认YES则会根据autoresizingMask属性自动调整子视图尺寸 */
@property(nonatomic) BOOL               autoresizesSubviews;
/** 自动调整子视图与父视图的位置,默认UIViewAutoresizingNone */
@property(nonatomic) UIViewAutoresizing autoresizingMask;
 
/** 返回“最佳”大小适合给定的大小 */
- (CGSize)sizeThatFits:(CGSize)size;
/** 调整为刚好合适子视图大小 */
- (void)sizeToFit;
 
@end

4. UIView层级管理|superview、subviews、window

@interface UIView(UIViewHierarchy)
 
/** 获取父视图 */
@property(nullable, nonatomic,readonly) UIView       *superview;
/** 获取所有子视图 */
@property(nonatomic,readonly,copy) NSArray<__kindof UIView *> *subviews;
/** 获取视图所在的Window */
@property(nullable, nonatomic,readonly) UIWindow     *window;
 
/** 从父视图中移除控件 */
- (void)removeFromSuperview;
/** 插入子视图(将子视图插入到subviews数组中index这个位置) */
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index;
/** 交换subviews数组中所存放子视图的位置 */
- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2;
 
/** 添加子视图(新添加的视图在subviews数组的后面, 显示在最上面) */
- (void)addSubview:(UIView *)view;
/** 插入子视图(将子视图插到siblingSubview之下) */
- (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview;
/** 插入子视图(将子视图插到siblingSubview之上) */
- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview;
 
/** 将子视图拉到最上面来显示 */
- (void)bringSubviewToFront:(UIView *)view;
/** 将子视图拉到最下面来显示 */
- (void)sendSubviewToBack:(UIView *)view;
 
##pragma mark - 系统自动调用(留给子类去实现)
/** 添加自视图完成后调用 */
- (void)didAddSubview:(UIView *)subview;
/** 将要移除自视图时调用 */
- (void)willRemoveSubview:(UIView *)subview;
 
/** 将要移动到新父视图时调用 */
- (void)willMoveToSuperview:(nullable UIView *)newSuperview;
/** 移动到新父视图完成后调用 */
- (void)didMoveToSuperview;
/** 将要移动到新Window时调用 */
- (void)willMoveToWindow:(nullable UIWindow *)newWindow;
/** 移动到新Window完成后调用 */
- (void)didMoveToWindow;
 
/** 判断view是否为子类 */
- (BOOL)isDescendantOfView:(UIView *)view;
/** 通过tag获得对应的子视图 */
- (nullable __kindof UIView *)viewWithTag:(NSInteger)tag;
 
/** 对现在有布局有调整更改后,使用这个方法进行更新 */
- (void)setNeedsLayout;
/** 强制进行更新layout */
- (void)layoutIfNeeded;
 
/** 控件的frame发生改变的时候就会调用,一般在这里重写布局子控件的位置和尺寸 */
- (void)layoutSubviews;
 
/** 设置view之间的间距,该属性只对autolayout布局有效 */
@property (nonatomic) UIEdgeInsets layoutMargins NS_AVAILABLE_IOS(8_0);
/** 是否将当前视图的间距和父视图相同,默认是NO */
@property (nonatomic) BOOL preservesSuperviewLayoutMargins NS_AVAILABLE_IOS(8_0);
/** 改变view的layoutMargins这个属性时,会触发这个方法 */
- (void)layoutMarginsDidChange NS_AVAILABLE_IOS(8_0);
 
/** 视图间距引导 */
@property(readonly,strong) UILayoutGuide *layoutMarginsGuide NS_AVAILABLE_IOS(9_0);
 
/** 获取此区域的内的布局引导 */
@property (nonatomic, readonly, strong) UILayoutGuide *readableContentGuide  NS_AVAILABLE_IOS(9_0);
@end

5. UIView渲染|裁剪、透明度、自定义绘制

//!< UIView内容填充模式.
typedef NS_ENUM(NSInteger, UIViewContentMode) {
    UIViewContentModeScaleToFill,       //!< 缩放内容到合适比例大小.
    UIViewContentModeScaleAspectFit,    //!< 缩放内容到合适的大小,边界多余部分透明.
    UIViewContentModeScaleAspectFill,   //!< 缩放内容填充到指定大小,边界多余的部分省略.
    UIViewContentModeRedraw,            //!< 重绘视图边界 (需调用 -setNeedsDisplay).
    UIViewContentModeCenter,            //!< 视图保持等比缩放.
    UIViewContentModeTop,               //!< 视图顶部对齐.
    UIViewContentModeBottom,            //!< 视图底部对齐.
    UIViewContentModeLeft,              //!< 视图左侧对齐.
    UIViewContentModeRight,             //!< 视图右侧对齐.
    UIViewContentModeTopLeft,           //!< 视图左上角对齐.
    UIViewContentModeTopRight,          //!< 视图右上角对齐.
    UIViewContentModeBottomLeft,        //!< 视图左下角对齐.
    UIViewContentModeBottomRight,       //!< 视图右下角对齐.
};
 
typedef NS_ENUM(NSInteger, UIViewTintAdjustmentMode) {
    UIViewTintAdjustmentModeAutomatic,  //!< 自动的,与父视图相同.
    
    UIViewTintAdjustmentModeNormal,     //!< 未经修改的.
    UIViewTintAdjustmentModeDimmed,     //!< 饱和、暗淡的原始色.
} NS_ENUM_AVAILABLE_IOS(7_0);

@interface UIView(UIViewRendering)
 
/** 重写drawRect方法,在可以这里进行绘图操作。*/
- (void)drawRect:(CGRect)rect;
 
/** 标记整个视图的边界矩形需要重绘, 调用这个方法会自动调用drawRect方法 */
- (void)setNeedsDisplay;
/** 标记在指定区域内的视图的边界需要重绘, 调用这个方法会自动调用drawRect方法 */
- (void)setNeedsDisplayInRect:(CGRect)rect;
 
/** 是否裁剪超出Bounds范围的子控件,默认NO */
@property(nonatomic)                 BOOL              clipsToBounds;
/** 设置背景颜色,默认nil */
@property(nullable, nonatomic,copy)  UIColor          *backgroundColor UI_APPEARANCE_SELECTOR;
/** 设置透明度(范围0.0~1.0),默认1.0 */
@property(nonatomic)                 CGFloat           alpha;
/** 设置是否不透明,默认YES不透明 */
@property(nonatomic,getter=isOpaque) BOOL              opaque;
/** 视图重绘前是否先清理以前的内容,默认YES */
@property(nonatomic)                 BOOL              clearsContextBeforeDrawing;
/** 设置是否隐藏,默认NO不隐藏 */
@property(nonatomic,getter=isHidden) BOOL              hidden;
/** 内容显示的模式,默认UIViewContentModeScaleToFill */
@property(nonatomic)                 UIViewContentMode contentMode;
/** 拉伸属性,如图片拉伸 */
@property(nonatomic)                 CGRect            contentStretch NS_DEPRECATED_IOS(3_0,6_0) __TVOS_PROHIBITED;
 
/** 蒙板view */
@property(nullable, nonatomic,strong)          UIView           *maskView NS_AVAILABLE_IOS(8_0);
 
/** 改变应用程序的外观的颜色。默认为nil */
@property(null_resettable, nonatomic, strong) UIColor *tintColor NS_AVAILABLE_IOS(7_0);
 
/** 可以使tintColor变暗,因此整个视图层次变暗 */
@property(nonatomic) UIViewTintAdjustmentMode tintAdjustmentMode NS_AVAILABLE_IOS(7_0);
 
/** 覆盖这个方法的目的是为了当tintColor改变的时候自定义一些行为 */
- (void)tintColorDidChange NS_AVAILABLE_IOS(7_0);
 
@end
 

6. UIView动画

typedef NS_OPTIONS(NSUInteger, UIViewKeyframeAnimationOptions) {
    UIViewKeyframeAnimationOptionLayoutSubviews            = UIViewAnimationOptionLayoutSubviews, //!< 动画过程中保证子视图跟随运动.
    UIViewKeyframeAnimationOptionAllowUserInteraction      = UIViewAnimationOptionAllowUserInteraction, //!< 动画过程中允许用户交互.
    UIViewKeyframeAnimationOptionBeginFromCurrentState     = UIViewAnimationOptionBeginFromCurrentState, //!< 所有视图从当前状态开始运行.
    UIViewKeyframeAnimationOptionRepeat                    = UIViewAnimationOptionRepeat, //!< 重复运行动画.
    UIViewKeyframeAnimationOptionAutoreverse               = UIViewAnimationOptionAutoreverse, //!< 动画运行到结束点后仍然以动画方式回到初始点.
    UIViewKeyframeAnimationOptionOverrideInheritedDuration = UIViewAnimationOptionOverrideInheritedDuration, //!< 忽略嵌套动画时间设置.
    UIViewKeyframeAnimationOptionOverrideInheritedOptions  = UIViewAnimationOptionOverrideInheritedOptions, //!< 不继承父动画设置或动画类型.
    
    UIViewKeyframeAnimationOptionCalculationModeLinear     = 0 << 10, //!< 连续运算模式, 默认.
    UIViewKeyframeAnimationOptionCalculationModeDiscrete   = 1 << 10, //!< 离散运算模式.
    UIViewKeyframeAnimationOptionCalculationModePaced      = 2 << 10, //!< 均匀执行运算模式.
    UIViewKeyframeAnimationOptionCalculationModeCubic      = 3 << 10, //!< 平滑运算模式.
    UIViewKeyframeAnimationOptionCalculationModeCubicPaced = 4 << 10  //!< 平滑均匀运算模式.
} NS_ENUM_AVAILABLE_IOS(7_0);

/** UIView动画选项 */
typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
    UIViewAnimationOptionLayoutSubviews            = 1 <<  0, //!< 动画过程中保证子视图跟随运动.
    UIViewAnimationOptionAllowUserInteraction      = 1 <<  1, //!< 动画过程中允许用户交互.
    UIViewAnimationOptionBeginFromCurrentState     = 1 <<  2, //!< 所有视图从当前状态开始运行.
    UIViewAnimationOptionRepeat                    = 1 <<  3, //!< 重复运行动画.
    UIViewAnimationOptionAutoreverse               = 1 <<  4, //!< 动画运行到结束点后仍然以动画方式回到初始点.
    UIViewAnimationOptionOverrideInheritedDuration = 1 <<  5, //!< 忽略嵌套动画时间设置.
    UIViewAnimationOptionOverrideInheritedCurve    = 1 <<  6, //!< 忽略嵌套动画速度设置.
    UIViewAnimationOptionAllowAnimatedContent      = 1 <<  7, //!< 动画过程中重绘视图(注意仅仅适用于转场动画).
    UIViewAnimationOptionShowHideTransitionViews   = 1 <<  8, //!< 视图切换时直接隐藏旧视图、显示新视图,而不是将旧视图从父视图移除(仅仅适用于转场动画).
    UIViewAnimationOptionOverrideInheritedOptions  = 1 <<  9, //!< 不继承父动画设置或动画类型.
    
    UIViewAnimationOptionCurveEaseInOut            = 0 << 16, //!< 动画先缓慢,然后逐渐加速.
    UIViewAnimationOptionCurveEaseIn               = 1 << 16, //!< 动画逐渐变慢.
    UIViewAnimationOptionCurveEaseOut              = 2 << 16, //!< 动画逐渐加速.
    UIViewAnimationOptionCurveLinear               = 3 << 16, //!< 动画匀速执行,默认值.
    
    UIViewAnimationOptionTransitionNone            = 0 << 20, //!< 没有转场动画效果.
    UIViewAnimationOptionTransitionFlipFromLeft    = 1 << 20, //!< 从左侧翻转效果.
    UIViewAnimationOptionTransitionFlipFromRight   = 2 << 20, //!< 从右侧翻转效果.
    UIViewAnimationOptionTransitionCurlUp          = 3 << 20, //!< 向后翻页的动画过渡效果.
    UIViewAnimationOptionTransitionCurlDown        = 4 << 20, //!< 向前翻页的动画过渡效果.
    UIViewAnimationOptionTransitionCrossDissolve   = 5 << 20, //!< 旧视图溶解消失显示下一个新视图的效果.
    UIViewAnimationOptionTransitionFlipFromTop     = 6 << 20, //!< 从上方翻转效果.
    UIViewAnimationOptionTransitionFlipFromBottom  = 7 << 20, //!< 从底部翻转效果.
    
    UIViewAnimationOptionPreferredFramesPerSecondDefault     = 0 << 24, //!< 默认的帧每秒.
    UIViewAnimationOptionPreferredFramesPerSecond60          = 3 << 24, //!< 60帧每秒的帧速率.
    UIViewAnimationOptionPreferredFramesPerSecond30          = 7 << 24, //!< 30帧每秒的帧速率.
    
} NS_ENUM_AVAILABLE_IOS(4_0);

/** 动画的曲线枚举 */
typedef NS_ENUM(NSInteger, UIViewAnimationCurve) {
    UIViewAnimationCurveEaseInOut,  //!< 慢进慢出(默认值).
    UIViewAnimationCurveEaseIn,     //!< 慢进.
    UIViewAnimationCurveEaseOut,    //!< 慢出.
    UIViewAnimationCurveLinear,     //!< 匀速.
};

/** UIView动画过渡效果 */
typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    UIViewAnimationTransitionNone,          //!< 无效果.
    UIViewAnimationTransitionFlipFromLeft,  //!< 沿视图垂直中心轴左到右移动.
    UIViewAnimationTransitionFlipFromRight, //!< 沿视图垂直中心轴右到左移动.
    UIViewAnimationTransitionCurlUp,        //!< 由底部向上卷起.
    UIViewAnimationTransitionCurlDown,      //!< 由顶部向下展开.
};

@interface UIView(UIViewAnimation)
 
/** 开始动画 */
+ (void)beginAnimations:(nullable NSString *)animationID context:(nullable void *)context;
/** 提交动画 */
+ (void)commitAnimations;
 
/** 设置动画代理, 默认nil */
+ (void)setAnimationDelegate:(nullable id)delegate;
/** 动画将要开始时执行方法(必须要先设置动画代理), 默认NULL */
+ (void)setAnimationWillStartSelector:(nullable SEL)selector;
/** 动画已结束时执行方法(必须要先设置动画代理), 默认NULL */
+ (void)setAnimationDidStopSelector:(nullable SEL)selector;
/** 设置动画时长, 默认0.2秒 */
+ (void)setAnimationDuration:(NSTimeInterval)duration;
/** 动画延迟执行时间, 默认0.0秒 */
+ (void)setAnimationDelay:(NSTimeInterval)delay;
/** 设置在动画块内部动画属性改变的开始时间, 默认now ([NSDate date]) */
+ (void)setAnimationStartDate:(NSDate *)startDate;
/** 设置动画曲线, 默认UIViewAnimationCurveEaseInOut */
+ (void)setAnimationCurve:(UIViewAnimationCurve)curve;
/** 动画的重复播放次数, 默认0 */
+ (void)setAnimationRepeatCount:(float)repeatCount;
/** 设置是否自定翻转当前的动画效果, 默认NO */
+ (void)setAnimationRepeatAutoreverses:(BOOL)repeatAutoreverses;
/** 设置动画从当前状态开始播放, 默认NO */
+ (void)setAnimationBeginsFromCurrentState:(BOOL)fromCurrentState;
 
/** 在动画块中为视图设置过渡动画 */
+ (void)setAnimationTransition:(UIViewAnimationTransition)transition forView:(UIView *)view cache:(BOOL)cache;
 
/** 设置是否激活动画 */
+ (void)setAnimationsEnabled:(BOOL)enabled;
/** 返回一个布尔值表示动画是否结束 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(class, nonatomic, readonly) BOOL areAnimationsEnabled;
#else
+ (BOOL)areAnimationsEnabled;
#endif
/** 先检查动画当前是否启用,然后禁止动画,执行block内的方法,最后重新启用动画,而且这个方法不会阻塞基于CoreAnimation的动画 */
+ (void)performWithoutAnimation:(void (NS_NOESCAPE ^)(void))actionsWithoutAnimation NS_AVAILABLE_IOS(7_0);
 
/** 当前动画的持续时间 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(class, nonatomic, readonly) NSTimeInterval inheritedAnimationDuration NS_AVAILABLE_IOS(9_0);
#else
+ (NSTimeInterval)inheritedAnimationDuration NS_AVAILABLE_IOS(9_0);
#endif
 
@end
 
@interface UIView(UIViewAnimationWithBlocks)
 
/** 用于对一个或多个视图的改变的持续时间、延时、选项动画完成时的操作 */
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);
 
/** 用于对一个或多个视图的改变的持续时间、选项动画完成时的操作,默认:delay = 0.0, options = 0 */
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);
 
/** 用于对一个或多个视图的改变的持续时间内动画完成时的操作,默认:delay = 0.0, options = 0, completion = NULL */
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations NS_AVAILABLE_IOS(4_0);
 
/** 使用与物理弹簧运动相对应的定时曲线执行视图动画 */
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);
 
/** 为指定的容器视图创建转换动画 */
+ (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);
 
/** 使用给定的参数在指定视图之间创建转换动画 */
+ (void)transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0); // toView added to fromView.superview, fromView removed from its superview
 
/** 在一个或多个视图上执行指定的系统提供的动画,以及定义的可选并行动画 */
+ (void)performSystemAnimation:(UISystemAnimation)animation onViews:(NSArray<__kindof UIView *> *)views options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))parallelAnimations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);
 
@end
 
/** UIView的关键帧动画 */
@interface UIView (UIViewKeyframeAnimations)
 
/** 创建一个动画块对象,可用于为当前视图设置基于关键帧的动画 */
+ (void)animateKeyframesWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewKeyframeAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);
/** 添加指定开始时间、持续时间的关键帧动画(起始和持续时间是0.0和1.0之间的值) */
+ (void)addKeyframeWithRelativeStartTime:(double)frameStartTime relativeDuration:(double)frameDuration animations:(void (^)(void))animations NS_AVAILABLE_IOS(7_0);
 
@end
 

7. UIView手势处理


@interface UIView (UIViewGestureRecognizers)
 
/** 当前视图所附加的所有手势识别器 */
@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);
/** 开始一个手势识别器 */
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer NS_AVAILABLE_IOS(6_0);
@end

8. UIView的生命周期相关的API

UIView生命周期相关函数:

//构造方法,初始化时调用,不会调用init方法
- (instancetype)initWithFrame:(CGRect)frame;
//添加子控件时调用
- (void)didAddSubview:(UIView *)subview ;
//构造方法,内部会调用initWithFrame方法
- (instancetype)init;
//xib归档初始化视图后调用,如果xib中添加了子控件会在didAddSubview方法调用后调用
- (instancetype)initWithCoder:(NSCoder *)aDecoder;
//唤醒xib,可以布局子控件
- (void)awakeFromNib;
//父视图将要更改为指定的父视图,当前视图被添加到父视图时调用
- (void)willMoveToSuperview:(UIView *)newSuperview;
//父视图已更改
- (void)didMoveToSuperview;
//其窗口对象将要更改
- (void)willMoveToWindow:(UIWindow *)newWindow;
//窗口对象已经更改
- (void)didMoveToWindow;
//布局子控件
- (void)layoutSubviews;
//绘制视图
- (void)drawRect:(CGRect)rect;
//从父控件中移除
- (void)removeFromSuperview;
//销毁
- (void)dealloc;
//将要移除子控件
- (void)willRemoveSubview:(UIView *)subview;

8.1 没有子控件的UIView

显示过程:

//(superview)
- (void)willMoveToSuperview:(nullable UIView *)newSuperview
- (void)didMoveToSuperview

//(window)
- (void)willMoveToWindow:(nullable UIWindow *)newWindow
- (void)didMoveToWindow

- (void)layoutSubviews

移出过程:

//(window)
- (void)willMoveToWindow:(nullable UIWindow *)newWindow
- (void)didMoveToWindow

//(superview)
- (void)willMoveToSuperview:(nullable UIView *)newSuperview
- (void)didMoveToSuperview

- (void)removeFromSuperview
- (void)dealloc

但是在移出时newWindow和newSuperview 都是nil。

8.2 包含子控件的UIView

  • 当增加一个子控件时,就会执行 didAddSubview,之后也会执行一次layoutSubview
  • 在view释放后,执行完,dealloc就会多次执行willRemoveSubview.先add的view,先释放掉。

8.3 layoutSubview

在上面的方法中,经常发现layoutSubview会被调用,下面说下layoutSubview的调用情况:

  • 1、addSubview会触发layoutSubviews,如果addSubview 如果连续2个 只会执行一次,具体原因下面说。
  • 2、设置view的Frame会触发layoutSubviews,必须是frame的值设置前后发生了变化。
  • 3、滚动一个UIScrollView会触发layoutSubviews。
  • 4、旋转Screen会触发父UIView上的layoutSubviews事件。
  • 5、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。

TIP

  • 1、如果要立即执行layoutSubview
    • 要先调用[view setNeedsLayout],把标记设为需要布局.
    • 然后马上调用[view layoutIfNeeded],实现布局.

    其中的原理是:执行setNeedsLayout后会在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews。
    这样刷新会产生延迟,所以我们需要马上执行layoutIfNeeded。就会开始遍历Subviews的链,判断该receiver是否需要layout。如果需要立即执行layoutSubview

  • 2、addSubview
    • 每一个视图只能有唯一的一个父视图。如果当前操作视图已经有另外的一个父视图,则addSubview的操作会把它先从上一个父视图中移除(包括响应者链),再加到新的父视图上面。
    • 连续2次的addSubview,只会执行一次layoutsubview。因为一次的runLoop结束后,如果有需要刷新,执行一次即可。

9. UIView屏幕快照

#pragma mark - View快照
@interface UIView (UISnapshotting)
/** 将当前显示的view截取成一个新的view */
- (nullable UIView *)snapshotViewAfterScreenUpdates:(BOOL)afterUpdates NS_AVAILABLE_IOS(7_0);
/** 缩放一个view默认是从中心点进行缩放的 */
- (nullable UIView *)resizableSnapshotViewFromRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates withCapInsets:(UIEdgeInsets)capInsets NS_AVAILABLE_IOS(7_0);
/** 屏幕快照 */
- (BOOL)drawViewHierarchyInRect:(CGRect)rect afterScreenUpdates:(BOOL)afterUpdates NS_AVAILABLE_IOS(7_0);
@end

10. UIView其它特性

//
//  UIView.h
//
//  Created by VanZhang on 2017/5/22.
//  Copyright © 2017年 . All rights reserved.
//
//  详解 UIResponder.h
//  Version iOS 10.3
//
 
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>
#import <UIKit/UIResponder.h>
#import <UIKit/UIInterface.h>
#import <UIKit/UIKitDefines.h>
#import <UIKit/UIAppearance.h>
#import <UIKit/UIDynamicBehavior.h>
#import <UIKit/NSLayoutConstraint.h>
#import <UIKit/UITraitCollection.h>
#import <UIKit/UIFocus.h>
 
NS_ASSUME_NONNULL_BEGIN
 
 
typedef NS_ENUM(NSUInteger, UISystemAnimation) {
    UISystemAnimationDelete,    //!< 系统删除动画
} NS_ENUM_AVAILABLE_IOS(7_0);
 
@protocol UICoordinateSpace <NSObject>
 
/** 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值 */
- (CGPoint)convertPoint:(CGPoint)point toCoordinateSpace:(id <UICoordinateSpace>)coordinateSpace NS_AVAILABLE_IOS(8_0);
/** 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值 */
- (CGPoint)convertPoint:(CGPoint)point fromCoordinateSpace:(id <UICoordinateSpace>)coordinateSpace NS_AVAILABLE_IOS(8_0);
/** 将rect由rect所在视图转换到目标视图view中,返回在目标视图view中的rect */
- (CGRect)convertRect:(CGRect)rect toCoordinateSpace:(id <UICoordinateSpace>)coordinateSpace NS_AVAILABLE_IOS(8_0);
/** 将rect从view中转换到当前视图中,返回在当前视图中的rect */
- (CGRect)convertRect:(CGRect)rect fromCoordinateSpace:(id <UICoordinateSpace>)coordinateSpace NS_AVAILABLE_IOS(8_0);
 
/** 获取bounds */
@property (readonly, nonatomic) CGRect bounds NS_AVAILABLE_IOS(8_0);
 
@end
 
@class UIBezierPath, UIEvent, UIWindow, UIViewController, UIColor, UIGestureRecognizer, UIMotionEffect, CALayer, UILayoutGuide;
 

 
 
@interface UIView (UIViewMotionEffects)
 
/** 添加运动效果,当倾斜设备时视图稍微改变其位置 */
- (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);
 
@end
 
 
 
#pragma mark - View状态保存恢复
@interface UIView (UIStateRestoration)
/** 标示是否支持保存,恢复视图状态信息 */
@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);
@end
 

 
NS_ASSUME_NONNULL_END

11. UIView自动布局相关


typedef NS_ENUM(NSInteger, UILayoutConstraintAxis) {
    UILayoutConstraintAxisHorizontal = 0,   //!< 水平约束.
    UILayoutConstraintAxisVertical = 1      //!< 竖直约束.
};
 
 
@interface UIView (UIConstraintBasedLayoutInstallingConstraints)
 
/** 获取所有约束 */
@property(nonatomic,readonly) NSArray<__kindof NSLayoutConstraint *> *constraints NS_AVAILABLE_IOS(6_0);
 
/** 添加一个约束 */
- (void)addConstraint:(NSLayoutConstraint *)constraint NS_AVAILABLE_IOS(6_0);
/** 添加多个约束 */
- (void)addConstraints:(NSArray<__kindof NSLayoutConstraint *> *)constraints NS_AVAILABLE_IOS(6_0);
/** 移除一个约束 */
- (void)removeConstraint:(NSLayoutConstraint *)constraint NS_AVAILABLE_IOS(6_0);
/** 移除多个约束 */
- (void)removeConstraints:(NSArray<__kindof NSLayoutConstraint *> *)constraints NS_AVAILABLE_IOS(6_0);
@end
 
 
@interface UIView (UIConstraintBasedLayoutCoreMethods)
/** 更新视图和其子视图的约束 */
- (void)updateConstraintsIfNeeded NS_AVAILABLE_IOS(6_0);
/** 为视图更新约束,可以重写这个方法来设置当前view局部的布局约束 */
- (void)updateConstraints NS_AVAILABLE_IOS(6_0) NS_REQUIRES_SUPER;
/** 视图的约束是否需要更新 */
- (BOOL)needsUpdateConstraints NS_AVAILABLE_IOS(6_0);
/** 设置视图的约束需要更新,调用这个方法,系统会调用updateConstraints去更新布局 */
- (void)setNeedsUpdateConstraints NS_AVAILABLE_IOS(6_0);
@end
 
 
@interface UIView (UIConstraintBasedCompatibility)
 
/** 是否启用自动布局约束,默认YES. IB默认是NO */
@property(nonatomic) BOOL translatesAutoresizingMaskIntoConstraints NS_AVAILABLE_IOS(6_0);
 
/** 是否使用约束布局 */
#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
 
@end
 
 
@interface UIView (UIConstraintBasedLayoutLayering)
 
/** 返回给定框架的视图的对齐矩阵 */
- (CGRect)alignmentRectForFrame:(CGRect)frame NS_AVAILABLE_IOS(6_0);
/** 返回给定对齐矩形的视图的frame */
- (CGRect)frameForAlignmentRect:(CGRect)alignmentRect NS_AVAILABLE_IOS(6_0);
 
/** 返回从视图的frame上定义的对齐矩阵的边框 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) UIEdgeInsets alignmentRectInsets NS_AVAILABLE_IOS(6_0);
#else
- (UIEdgeInsets)alignmentRectInsets NS_AVAILABLE_IOS(6_0);
#endif
 
/** 返回满足基线约束条件的视图 */
- (UIView *)viewForBaselineLayout NS_DEPRECATED_IOS(6_0, 9_0, "Override -viewForFirstBaselineLayout or -viewForLastBaselineLayout as appropriate, instead") __TVOS_PROHIBITED;
 
/** 返回用于满足第一基线约束的视图 */
@property(readonly,strong) UIView *viewForFirstBaselineLayout NS_AVAILABLE_IOS(9_0);
 
/** 返回用于满足上次基线约束的视图 */
@property(readonly,strong) UIView *viewForLastBaselineLayout NS_AVAILABLE_IOS(9_0);
 
 
UIKIT_EXTERN const CGFloat UIViewNoIntrinsicMetric NS_AVAILABLE_IOS(6_0); // -1
/** 返回接收对象的原本大小 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) CGSize intrinsicContentSize NS_AVAILABLE_IOS(6_0);
#else
- (CGSize)intrinsicContentSize NS_AVAILABLE_IOS(6_0);
#endif
/** 废除视图原本内容的size */
- (void)invalidateIntrinsicContentSize NS_AVAILABLE_IOS(6_0);
 
/** 设置当视图要变大时,视图的压缩改变方式,返回一个优先权(确定view有多大的优先级阻止自己变大) */
- (UILayoutPriority)contentHuggingPriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
/** 设置放先权 */
- (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
 
/** 设置当视图要变小时,视图的压缩改变方式,是水平缩小还是垂直缩小,并返回一个优先权(确定有多大的优先级阻止自己变小) */
- (UILayoutPriority)contentCompressionResistancePriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
/** 设置优先权 */
- (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
@end
 
// Size To Fit
 
UIKIT_EXTERN const CGSize UILayoutFittingCompressedSize NS_AVAILABLE_IOS(6_0);
UIKIT_EXTERN const CGSize UILayoutFittingExpandedSize NS_AVAILABLE_IOS(6_0);
 
@interface UIView (UIConstraintBasedLayoutFittingSize)
/** 返回满足持有约束的视图的size */
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize NS_AVAILABLE_IOS(6_0);
/** 返回满足它所包含的约束的视图的大小 */
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority NS_AVAILABLE_IOS(8_0);
@end
 
@interface UIView (UILayoutGuideSupport)
 
/** 此视图拥有布局向导对象的数组 */
@property(nonatomic,readonly,copy) NSArray<__kindof UILayoutGuide *> *layoutGuides NS_AVAILABLE_IOS(9_0);
 
/** 向视图中添加布局向导 */
- (void)addLayoutGuide:(UILayoutGuide *)layoutGuide NS_AVAILABLE_IOS(9_0);
 
/** 向视图中添加布局向导 */
- (void)removeLayoutGuide:(UILayoutGuide *)layoutGuide NS_AVAILABLE_IOS(9_0);
@end
 
@class NSLayoutXAxisAnchor,NSLayoutYAxisAnchor,NSLayoutDimension;
@interface UIView (UIViewLayoutConstraintCreation)
/** 布局视图的前缘框的布局锚点 */
@property(readonly, strong) NSLayoutXAxisAnchor *leadingAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的后缘边框的布局锚点 */
@property(readonly, strong) NSLayoutXAxisAnchor *trailingAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的左边框的布局锚点 */
@property(readonly, strong) NSLayoutXAxisAnchor *leftAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的右边框的布局锚点 */
@property(readonly, strong) NSLayoutXAxisAnchor *rightAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的顶边框的布局锚点 */
@property(readonly, strong) NSLayoutYAxisAnchor *topAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的底边框的布局锚点 */
@property(readonly, strong) NSLayoutYAxisAnchor *bottomAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的宽度 */
@property(readonly, strong) NSLayoutDimension *widthAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的高度 */
@property(readonly, strong) NSLayoutDimension *heightAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的水平中心轴 */
@property(readonly, strong) NSLayoutXAxisAnchor *centerXAnchor NS_AVAILABLE_IOS(9_0);
/** 布局视图的垂直中心轴 */
@property(readonly, strong) NSLayoutYAxisAnchor *centerYAnchor NS_AVAILABLE_IOS(9_0);
/** 一个代表对视图中的文本的最高线基线布置锚 */
@property(readonly, strong) NSLayoutYAxisAnchor *firstBaselineAnchor NS_AVAILABLE_IOS(9_0);
/** 一个代表对视图中的文本的最低线基线布置锚 */
@property(readonly, strong) NSLayoutYAxisAnchor *lastBaselineAnchor NS_AVAILABLE_IOS(9_0);
 
@end
 
@interface UIView (UIConstraintBasedLayoutDebugging)
 
/** 获得实体在不同方向上所有的布局约束 */
- (NSArray<__kindof NSLayoutConstraint *> *)constraintsAffectingLayoutForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
 
/** 可以知道当前视图的布局是否会有歧义 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL hasAmbiguousLayout NS_AVAILABLE_IOS(6_0);
#else
- (BOOL)hasAmbiguousLayout NS_AVAILABLE_IOS(6_0);
#endif
 
/** 这个方法会随机改变视图的layout到另外一个有效的layout。这样我们就可以很清楚的看到哪一个layout导致了整体的布局约束出现了错误,或者我们应该增加更多的布局约束 */
- (void)exerciseAmbiguityInLayout NS_AVAILABLE_IOS(6_0);
@end
 
/** 约束调试,只在DEBUG环境下被调用 */
@interface UILayoutGuide (UIConstraintBasedLayoutDebugging)
 
/** 获得实体在不同方向上所有的布局约束 */
- (NSArray<__kindof NSLayoutConstraint *> *)constraintsAffectingLayoutForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(10_0);
 
/** 可以知道当前视图的布局是否会有歧义 */
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) BOOL hasAmbiguousLayout NS_AVAILABLE_IOS(10_0);
#else
- (BOOL)hasAmbiguousLayout NS_AVAILABLE_IOS(10_0);
#endif
@end

12. View的常用派生类简介

UIView派生系

  • UIControl:
    UIControl 主要负责管理用户的触摸事件,并根据用户的操作来更新自身的状态。
  • UIScrollView:
    用于展示可滚动内容的滚动视图控件,支持滚动缩放滚动事件处理等功能。

  • UILabel:
    用于显示文本内容的标签控件,支持文字的自动换行字体颜色等属性设置。
  • UIImageView:
    用于显示图像的图像视图控件,支持显示本地图像远程图像,并可以设置内容模式动画效果等。
  • UIProgressView:
    用于显示进度的进度条控件,支持水平或垂直方向的显示、进度值设置动画效果
  • UIActivityIndicatorView:
    用于显示加载指示器的活动指示器控件,支持显示和隐藏动画效果等。
  • UIPickerView:
    用于选择器的选择器视图控件,支持显示多列数据滚动选择事件处理等功能。
  • UIStackView:
    用于自动布局的栈视图控件,支持水平垂直方向的排列、子视图的布局等功能。

UIControl派生系

  • UIButton:
    用于响应用户点击事件的按钮控件,支持不同状态下不同外观样式事件处理
  • UITextField:
    用于输入文本内容的文本输入框控件,支持单行多行输入键盘类型设置占位符文本等。
  • UISwitch:
    用于切换开关状态的开关控件,支持显示开关状态切换动画等。
  • UISlider:
    用于选择连续数值的滑块控件,支持滑块的最小值最大值当前值设置和事件处理
  • UIDatePicker:
    用于选择日期和时间的日期选择器控件,支持显示日期和时间滚动选择事件处理等功能。

UIScrollView派生系

  • UITextView:
    用于显示和编辑多行文本内容的文本视图控件,支持显示富文本滚动编辑等功能。
  • UITableView:
    用于展示列表数据表格视图控件,支持单列或多列列表分组滚动数据源代理等功能。
  • UICollectionView:
    用于展示多列数据集合视图控件,支持自定义布局分区单元格数据源代理等功能。

二、UIViewController

1. UIViewController的职责

官方对UIViewController的介绍:
UIViewController用于管理App视图结构层次的对象,它的主要职责如下:

  • 提供了丰富的生命周期方法事件处理机制
    • 管理生命周期:
      提供生命周期方法,包括视图加载显示隐藏销毁等阶段
    • 事件处理机制:
      • UIResponder中的几种事件:
        • UIViewController 是由 UIResponder 派生的,因此 UIResponder负责处理的几种事件,UIViewController中同样有效:
          包括:触摸事件按压事件加速事件远程控制事件键盘事件等。
          回顾了解可以参照这篇文章的UIResponder部分
      • 响应系统事件:负责响应系统事件,包括:
        • 设备方向变化
        • 内存警告
        • 视图控制器切换
      • 其它事件:
        手势事件
  • 负责 管理 UIKit 应用程序的视图层次结构的对象
    • 通过VC的生命周期方法,负责处理UI的加载显示布局交互旋转隐藏卸载等任务
    • 实现容器视图控制器(在接下来的篇幅介绍相关API)
  • 页面切换:
    • 通过容器控制器(UINavigationController、UITabBarController)来管理界面之间的切换和导航
    • 通过 URLRouter(URL+OpenURL+容器控制器 配合) 进行页面切换
  • 页面传值:
    可以通过属性委托通知Block回调(闭包回调)路由跳转传参等方式进行数据传递和通信
  • ...

2. UIViewController|生命周期相关 API

我们先介绍 UIViewController生命周期相关的API:

  1. loadView():
    • 简介:用于创建或加载视图控制器的视图层次结构。
    • 说明:如果视图控制器通过storyboard创建,通常不需要重写这个方法。
  2. viewDidLoad():
    • 简介:视图已经加载完成,此时可以进行一些初始化操作,如添加子视图、设置视图的初始状态等。
  3. viewWillAppear(_ animated: Bool):
    • 简介:视图即将显示在屏幕上,此时视图控制器可以做一些在界面显示之前需要准备的工作,比如更新数据。
    • 参数:animated表示视图是否以动画形式显示。
  4. viewDidAppear(_ animated: Bool):
    • 简介:视图已经显示在屏幕上,此时可以执行一些需要在界面显示完成后立即执行的操作,比如启动定时器。
    • 参数:animated表示视图是否以动画形式显示。
  5. viewWillDisappear(_ animated: Bool):
    • 简介:视图即将从屏幕上消失,此时可以做一些在界面消失之前需要处理的工作,比如保存数据。
    • 参数:animated表示视图是否以动画形式消失。
  6. viewDidDisappear(_ animated: Bool):
    • 简介:视图已经从屏幕上消失,此时可以执行一些需要在界面消失后立即执行的操作,比如停止定时器。
    • 参数:animated表示视图是否以动画形式消失。
  7. viewWillLayoutSubviews():
    • 简介:视图将要布局子视图时调用,可以在此方法中更新子视图的布局。
  8. viewDidLayoutSubviews():
    • 简介:视图已经布局子视图完成时调用,可以在此方法中执行一些与子视图布局相关的操作。

2.1 重写生命周期方法

我们重写UIViewController生命周期方法,以便于后面Demo实践:

#pragma mark- 对象 初始化 和 销毁
+ (void)initialize {
    NSLog(@"========   类初始化方法: initialize   =======\n");
}

- (instancetype)init {
     self = [super init];
    NSLog(@"========   实例初始化方法: init   =======\n");
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    NSLog(@"========   从归档初始化:  initWithCoder:(NSCoder *)aDecoder   =======\n");
    return self;
}

- (void)dealloc {
    NSLog(@"========   释放: dealloc   =======\n");
}

#pragma mark- 系统事件|内存警告
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    NSLog(@"========   收到内存警告: didReceiveMemoryWarning   =======\n");
}

#pragma mark- life cycle
- (void)loadView {
    [super loadView];
    NSLog(@"========   加载视图: loadView   =======\n");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    NSLog(@"========   将要加载视图: viewDidLoad   =======\n");
}

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    NSLog(@"========   将要布局子视图: viewWillLayoutSubviews   =======\n");
}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    NSLog(@"========   已经布局子视图: viewDidLayoutSubviews   =======\n");
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"========   视图将要出现: viewWillAppear:(BOOL)animated   =======\n");
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"========   视图已经出现: viewDidAppear:(BOOL)animated   =======\n");
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    NSLog(@"========   视图将要消失: viewWillDisappear:(BOOL)animated   =======\n");
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    NSLog(@"========   视图已经消失: viewDidDisappear:(BOOL)animated   =======\n");
}

2.2 单ViewController|生命周期事件

我们创建Demo工程:

  • 将重写 UIViewController生命周期相关方法 和 类 初始化反初始化 相关的方法 插入ViewController
  • 然后我们启动工程,观察VC的生命周期事件的执行情况。

控制台打印结果:

========   类初始化方法: initialize   =======
========   实例初始化方法: init   =======
========   加载视图: loadView   =======
========   将要加载视图: viewDidLoad   =======
========   视图将要出现: viewWillAppear:(BOOL)animated   =======
========   将要布局子视图: viewWillLayoutSubviews   =======
========   已经布局子视图: viewDidLayoutSubviews   =======
========   视图已经出现: viewDidAppear:(BOOL)animated   =======
========   视图将要消失: viewWillDisappear:(BOOL)animated   =======
========   视图已经消失: viewDidDisappear:(BOOL)animated   =======
========   释放: dealloc   =======

ViewController生命周期方法执行顺序:

  • (push页面阶段开始)initialize —> init
    • —> loadView —> viewDidLoad
    • —> viewWillAppear
      • —> viewWillLayoutSubviews
      • —> viewDidLayoutSubviews
    • —> viewDidAppear
    • (pop页面阶段开始)—> viewWillDisappear
    • —> viewDidDisappear
  • —> dealloc

2.3 VC1 push VC2|VC的生命周期事件

  • 创建两个VC(FirstViewControllerSecondViewController)
  • 将重写 UIViewController生命周期相关方法 和 类 初始化反初始化 相关的方法 分别插入FirstViewControllerSecondViewController
  • 编写代码,由FirstViewControllerpush打开SecondViewController
  • 然后我们启动工程,观察SecondViewController的生命周期事件的执行情况。

控制台打印结果:

======== SecondViewController   类初始化方法: initialize   =======
======== SecondViewController   实例初始化方法: init   =======
======== SecondViewController   加载视图: loadView   =======
======== SecondViewController   将要加载视图: viewDidLoad   =======
======== FirstViewController   视图将要消失: viewWillDisappear:(BOOL)animated   =======
======== SecondViewController   视图将要出现: viewWillAppear:(BOOL)animated   =======
======== SecondViewController   将要布局子视图: viewWillLayoutSubviews   =======
======== SecondViewController   已经布局子视图: viewDidLayoutSubviews   =======
======== FirstViewController   视图已经消失: viewDidDisappear:(BOOL)animated   =======
======== SecondViewController   视图已经出现: viewDidAppear:(BOOL)animated   =======
======== SecondViewController   视图将要消失: viewWillDisappear:(BOOL)animated   =======
======== FirstViewController   视图将要出现: viewWillAppear:(BOOL)animated   =======
======== SecondViewController   视图已经消失: viewDidDisappear:(BOOL)animated   =======
======== FirstViewController   视图已经出现: viewDidAppear:(BOOL)animated   =======
======== SecondViewController   释放: dealloc   =======

SecondViewController生命周期方法执行顺序:

  • (push页面阶段开始)initialize —> init
    • —> loadView
    • —> viewDidLoad
      • —> FirstViewController viewWillDisappear
    • —> viewWillAppear
      • —> viewWillLayoutSubviews
      • —> viewDidLayoutSubviews
      • —> FirstViewController viewDidDisappear
    • —> viewDidAppear
    • (pop页面阶段开始)—> viewWillDisappear
      • —> FirstViewController viewWillAppear
    • —> viewDidDisappear
      • —> FirstViewController viewDidAppear
  • —> dealloc Tips,以上执行情况,没有标注 FirstViewController的均为 SecondViewController在执行工作

3. UIViewController|容器控制器相关API

在iOS5的时候为了解耦、更加清晰的处理页面View的逻辑,UIViewController提供了addChildViewController方法,将ViewController作为容器处理视图控制器的切换,将比较复杂的UI使用子ViewController来管理。

我们在一个页面要以菜单分类的形式展示不同菜单下的内容,且每个菜单下的内容UI构成不相同时,可以把每个菜单的内容放到单独一个VC去管理。由一个主VC作为容器处理视图控制器管理展示,

相关属性和方法介绍:

///子视图控制器数组
@property(nonatomic,readonly) NSArray *childViewControllers

///向父VC中添加子VC
- (void)addChildViewController:(UIViewController *)childController

///将子VC从父VC中移除
- (void) removeFromParentViewController

///fromViewController 当前显示在父视图控制器中的子视图控制器
///toViewController 将要显示的姿势图控制器
///duration 动画时间
/// options 动画效果(渐变,从下往上等等,具体查看API)
///animations 转换过程中得动画
///completion 转换完成
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion

///当向父VC添加子VC之后,该方法会自动调用;
- (void)willMoveToParentViewController:(UIViewController *)parent

///从父VC移除子VC之后,该方法会自动调用
- (void)didMoveToParentViewController:(UIViewController *)parent

4. UIViewController|几种初始化方式

这一块相对简单,我们列举一下就好:

    1. 纯代码创建:
      在代码中使用UIViewController的init(nibName:bundle:)或者init()方法创建视图控制器,并设置相应的属性。
    let viewController = MyViewController()
    
    1. Storyboard创建:
      在Storyboard中创建UIViewController,并设置对应的类名。
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let viewController = storyboard.instantiateViewController(withIdentifier: "MyViewController") as! MyViewController
    
    1. XIB文件创建:
      在XIB文件中创建UIViewController,并设置对应的类名。
    let viewController = MyViewController(nibName: "MyViewController", bundle: nil)
    
    1. 使用UIStoryboard的instantiateInitialViewController方法:
      用于从Storyboard中实例化初始化的视图控制器。
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let initialViewController = storyboard.instantiateInitialViewController() as! MyViewController
    
    1. 自定义初始化方法:
      有时候视图控制器可能有一些自定义的初始化方法,可以根据需要进行调用。
    let viewController = MyViewController(customParameter: parameter)
    

5. UIViewController|几种页面传值方式

这一块相对简单,我们列举一下就好:

正向传值:

  • 初始化方法 传值
  • 属性 传值

逆向传值:

  • Delegate
  • 回调闭包Block/Closure

可逆向传值也可正向传值的几种方式

  • 全局单例 传值
  • 通知广播 传值
  • 模块管理工具模块间通讯EventBus,事件管理派发 传值
  • 跳转路由 传值(本质还是属性传值)

6. UIViewController|几种页面跳转方式

这一块相对简单,我们列举一下就好:

  1. Segue跳转:
    在Storyboard中通过Segue连接不同的视图控制器,在跳转时会执行Segue的相关代码。
        // 使用performSegue(withIdentifier:sender:)方法手动执行Segue跳转
        performSegue(withIdentifier: "SegueIdentifier", sender: self)
    
        // 准备跳转前的准备工作
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if segue.identifier == "SegueIdentifier" {
                // 根据segue.destination获取目标视图控制器
                let destinationVC = segue.destination as! DestinationViewController
                // 设置目标视图控制器的属性等
            }
        }
    
  2. Modal方式跳转:
    以模态形式显示目标视图控制器,覆盖在当前视图控制器之上。
    // 以模态形式显示目标视图控制器
    present(destinationVC, animated: true, completion: nil)
    
    // 关闭模态视图控制器返回到上一个视图控制器
    dismiss(animated: true, completion: nil)
    
  3. 通过容器控制器管理:
    • a. Navigation Controller:
      使用Navigation Controller管理多个视图控制器的堆栈,通过push和pop操作实现页面跳转
    // 在Navigation Controller中推入目标视图控制器
    navigationController?.pushViewController(destinationVC, animated: true)
    
    // 返回上一个视图控制器
    navigationController?.popViewController(animated: true)
    
    // 返回到根视图控制器
    navigationController?.popToRootViewController(animated: true)
    
    • b. Tab Bar Controller:
      使用Tab Bar Controller管理多个视图控制器,通过Tab Bar切换不同的视图控制器。

      // 通过Tab Bar切换到指定的视图控制器
      tabBarController?.selectedIndex = index
      
    • c. UIViewController自定义容器控制器:
      自定义容器视图控制器,管理多个子视图控制器,并通过代码控制子视图控制器的显示和隐藏。

      // 添加子视图控制器
      addChild(destinationVC)
      view.addSubview(destinationVC.view)
      destinationVC.didMove(toParent: self)
      
      // 移除子视图控制器
      destinationVC.willMove(toParent: nil)
      destinationVC.view.removeFromSuperview()
      destinationVC.removeFromParent()
      

7. UIViewController|自定义转场动画

本文先介绍一下自定义转场动画的核心要点,对具体动画的实现等,在后面介绍动画相关章节的时候,会有更详尽的分享。

7.1 核心要点

  • 切换页面转场的几种方式:
    • 通过 UIViewController Modal出一个新VC的页面
    • 通过容器控制器 切换 页面
      • 通过 UINavigationController进行PushPop操作,作VC间的页面切换
      • 通过 UITabBarControllerselectIndex 重新赋值,,进行选中VC的切换
  • 转场方式:
    • 默认转场动画: 系统的 ModalPushPopselectVC切换
    • 自定义转场动画:
      • 交互性(实现动画的实例+手势交互)
      • 非交互形(实现动画的实例)
  • 注意:
    • 系统默认转场动画,是系统提供了默认实现动画实例
    • 因此,我们要自定义转场动画,也要
      • 提供自定义的实现动画实例
      • 在页面转场的时机,将 自定义的实现动画实例 提交 给系统API
        • 系统 通过 Delegate回调方法 把 页面切换的时机告诉我们

因此,接下来我们就要 重点介绍 转场动画 相关的 几个协议(OC、Swift版本的API基本一样.这里用OCAPI介绍)

7.2 实现自定义动画对象|UIViewControllerAnimatedTransitioning

实现自定义动画步骤:

    1. 自定义动画对象:
      自定义Class,遵守UIViewControllerAnimatedTransitioning协议
    1. 实现协议中的核心API:
    • 动画执行时间:
      - transitionDuration:transitionContext
    • 动画具体实现
      - animateTransition:
    • 动画执行结束的回调
      - animationEnded:
    1. 在页面转场的时机回调方法中,返回给系统该自定义Class的实例,告诉系统动画实现的细节
  • 协议中的API介绍如下:
@protocol UIViewControllerAnimatedTransitioning <NSObject>
// 设置 转场动画的持续时间
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;

/*
 * @ param id <UIViewControllerContextTransitioning> 转场动画的上下文对象
 *    负责 提供 页面切换的上下文,也就是前后两个VC的View等信息
 *    自定义动画的本质,就是编写自定义动画代码,在这个回调中,对前后切换页面的View或layer 添加自定义的动画进行切换
 */
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

@optional
// 动画结束的 回调
- (void)animationEnded:(BOOL) transitionCompleted;

@end

7.3 页面转场上下文对象|UIViewControllerContextTransitioning

  • 协议定义了 在执行自定义转场动画时所需的一些方法属性
  • 遵守 该协议,并实现了协议中API的 实例对象由系统的回调方法提供
  • 该实例用于提供有关视图控制器之间转场动画的上下文信息(常用属性和方法介绍):
@protocol UIViewControllerContextTransitioning <NSObject>
// 容器视图,用于容纳转场过程中的View
@property(nonatomic, readonly) UIView *containerView;
...
@property(nonatomic, readonly) BOOL transitionWasCancelled;
...
// 用户标记转场动画是否完成,必须在动画执行完成之后 调用。入参用context实例的transitionWasCancelled属性值的相反值
- (void)completeTransition:(BOOL)didComplete;
// 通过该方法 获取 上下文 切换 的两个FromVC、ToVC
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
// 通过该方法 获取 上下文 切换 的两个FromView、ToView
- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key API_AVAILABLE(ios(8.0));
...
// 通过该方法 获取 VC 的 最终frame,可以间接获得view的center,size。进行缩放,位移等动画
- (CGRect)finalFrameForViewController:(UIViewController *)vc;
@end

实战示例代码片段:

// This method can only be a no-op if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
    self.transitionContext = transitionContext;
    self.containerView = [transitionContext containerView];
    self.fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    self.toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    // iOS8之后才有
    if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
        self.fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
        self.toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    } else {
        self.fromView = self.fromViewController.view;
        self.toView = self.toViewController.view;
    }
    
    ...
    self.toView.frame = [self.transitionContext finalFrameForViewController:self.toViewController];
    // 在动画 执行完成的地方要 必须执行的代码:
    BOOL wasCancelled = [self.transitionContext transitionWasCancelled];
    [self.transitionContext completeTransition:!wasCancelled];
    ...
}

7.4 自定义Modal转场动画|UIViewControllerTransitioningDelegate

这个协议规定了VC1Modal推出 VC2 和 从VC2 dismiss返回 VC1 的两套接口

  • 交互型
    • Modal推出: - animationControllerForPresentedController: presentingController: sourceController:
    • dismiss返回: - animationControllerForDismissedController:
  • 非交互型(一般添加pan手势进行交互)
    • Modal推出: - interactionControllerForPresentation:
    • dismiss返回: - interactionControllerForDismissal:

@protocol UIViewControllerTransitioningDelegate <NSObject>

@optional
// 非交互型: 我们直接把我们实现的 自定义动画实例,返回即可「present动画和dismiss动画可相同,也可不同」
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

// 交互型: 我们要在此提供 实现了 协议`UIViewControllerInteractiveTransitioning`的实例,用于告诉系统,动画的执行进度(这依赖我们 编写的 交互代码,若是用手势交互,则是拖拽的x和参考系x值的百分比...)
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
...
@end

7.5 添加交互逻辑|UIViewControllerInteractiveTransitioning

通过 使用 遵守 该协议的 对象,可以获取 开始交互的时机 和 VC页面切换的 上下文对象,进而添加 交互 逻辑,经常用pan手势添加交互逻辑。编写交互逻辑要点如下:

    1. 在回调方法中,获取 开始交互的时机
    1. 给vc的view添加交互逻辑
    1. 根据交互逻辑 计算出 转场 动画 的 百分比,把百分比值percent 提交给 VC页面切换的 上下文对象。以达到,通过交互控制转场动画的效果
    1. 这依然依赖我们实现的自定义转场动画
    1. 我们可以用 继承系统的UIPercentDrivenInteractiveTransition类,专注于编写交互逻辑。并在合适的时机告知系统 动画执行的 情况(百分比进展、取消、结束)
    • - (void)updateInteractiveTransition:(CGFloat)percentComplete;
    • - (void)cancelInteractiveTransition;
    • - (void)finishInteractiveTransition;
@protocol UIViewControllerInteractiveTransitioning <NSObject>
- (void)startInteractiveTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
...
@end

3. UIPercentDrivenInteractiveTransition

@interface UIPercentDrivenInteractiveTransition : NSObject <UIViewControllerInteractiveTransitioning>
@property (readonly) CGFloat duration;
....
// 这三个API底层都是调用 UIViewControllerContextTransitioning 上下文对象中的一样API
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;
@end

7.6 UINavigationController|自定义转场动画

注意区分:

  • VC1 通过UINavigationController push 推出 VC2; 或者 VC2 pop 返回 VC1 ,是在遵守并了协议 UINavigationControllerDelegate的转场动画方法中进行实现
  • 而不是 遵守了 UIViewControllerTransitioningDelegate 协议 的相关方法;
  • 对于 转场 动画的具体实现交互逻辑的具体实现, 是可以一致的。
  • 相关核心API如下:
@protocol UINavigationControllerDelegate <NSObject>
...
// 自定义交互逻辑实现接口
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                          interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController API_AVAILABLE(ios(7.0));
// 自定义转场动画接口
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC  API_AVAILABLE(ios(7.0));

@end

7.7 UITabBarController|自定义转场动画

注意区分:

  • UITabBarController select 一个新的 index 进行 页面切换,是在遵守并了协议 UITabBarControllerDelegate的转场动画方法中进行实现
  • 而不是 遵守了 UIViewControllerTransitioningDelegate 协议 的相关方法;
  • 对于 转场 动画的具体实现交互逻辑的具体实现, 是可以一致的。
  • 相关核心API如下:
@protocol UITabBarControllerDelegate <NSObject>
...
// 自定义交互逻辑实现接口
- (nullable id <UIViewControllerInteractiveTransitioning>)tabBarController:(UITabBarController *)tabBarController
                      interactionControllerForAnimationController: (id <UIViewControllerAnimatedTransitioning>)animationController API_AVAILABLE(ios(7.0)) API_UNAVAILABLE(visionos);

// 自定义转场动画接口
- (nullable id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
            animationControllerForTransitionFromViewController:(UIViewController *)fromVC
                                              toViewController:(UIViewController *)toVC  API_AVAILABLE(ios(7.0)) API_UNAVAILABLE(visionos);
@end

8. 自定义地转场动画的实战

对于转场动画的具体实战,我们在 总结 iOS中的动画实现 的 文章 再进一步 展开讲述。此处 仅是 讲解转场动画实现的基础

三、UIWindow

1. UIWindow核心要点

简介

官网对 UIWindow 的介绍:

The backdrop for your app’s user interface and the object that dispatches events to your views.

我们用将官网的介绍用中文解释:

  • UIWindow是应用程序用户界面的背景
  • UIWindow负责派发各类事件给视图对象Views

UIWindow核心要点

这样的介绍显然太过简洁难懂,我们结合多年的项目实施经验给予更详细的诠释:

  1. 作用:
    • 每个应用程序都 至少有一个UIWindow 对象,它是应用程序中的主窗口
    • UIWindow是应用程序中视图层次结构的顶层容器,它负责管理应用程序中所有视图的显示和布局。
  2. 层级关系:
    • UIWindow对象位于视图层次结构的最顶层所有其他视图都是它的子视图子视图的子视图
  3. 创建方式:
    • 可以通过UIWindow类的init(frame:)方法或init(windowScene:)方法来创建一个窗口对象。
    • 通常情况下,UIWindow对象是由系统自动创建并管理的,开发者无需手动创建。
  4. 关键属性:
    • rootViewController:窗口的根视图控制器,决定了窗口中显示的内容。
    • windowScene:窗口所属的场景对象,用于多窗口管理。
  5. 常用方法:
    • makeKeyAndVisible():将窗口设置为主窗口,并显示在屏幕上
    • resignKey():将窗口从主窗口中移除
  6. 事件处理:
    • UIWindow对象是响应者链中的一部分,可以处理触摸事件、运动事件等。
    • 通常情况下,UIWindow对象会将触摸事件传递给其子视图根视图控制器进行处理。
  7. 窗口管理:
    • iOS应用程序可以包含多个窗口,每个窗口可以显示不同的内容。
    • 多窗口管理通常用于支持多任务处理多屏幕显示等功能。
  8. 使用场景:
    • UIWindow通常用于显示应用程序的主界面弹出窗口警告框等。
    • 也可以用于实现一些特殊效果,如悬浮按钮悬浮窗口等。

  1. 其它:
    • iOS程序启动完毕后,创建的第一个视图控件就是UIWindow
      • 接着创建控制器的View
      • 最后将控制器的View添加到UIWindow上,于是控制器的View就显示在屏幕上了
    • 状态栏键盘是特殊的UIWindow

那么UIWindow是如何将View显示到屏幕上的呢?

  • 这里有三个重要的对象 UIScreenUIWindowUIView
  • UIScreen 对象识别物理屏幕连接到设备
  • UIWindow 对象提供绘画支持给屏幕
  • UIView 执行绘画,当窗口要显示内容的时候,UIView 绘画出他们的内容并附加到窗口上。

2. UIWindow的创建

2.1 UIWindow是什么时候创建的?

当我们新建一个项目,直接在stroyboard为view设置一个背景颜色,然后运行项目,就能看到换了背景颜色的view,这说明系统已经帮我们创建了一个UIWindow,那么这个UIWindow是什么时候创建的?

我们找到程序的入口main函数,来看程序的启动过程

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

此时我们可以根据UIApplicationMain函数了解程序启动的过程

  1. 根据传递的类名创建UIApplication对象,这是第一个对象
  2. 创建UIApplication代理对象,并给UIApplicaiton对象设置代理
  3. 开启 主运行循环 main events loop处理事件,保持程序一直运行
  4. 加载info.plist,判断是否指定mian(xib 或者 storyboard)如果指定就去加载

当我们把指定的Main Interface 中mian给删除的时候,重新运行程序,就会发现我们之前设置的view没有办法显示了。

Main Interface 中 Main删除

此时我们基本可以想到,UIWindow应该是在加载storyboard的时候系统创建的,那么系统是如何加载storyboard的呢?
系统在加载storyboard的时候会做以下三件事情:

    1. 创建窗口
    1. 加载mian.storyboard 并实例化view controller
    1. 分配新视图控制器到窗口root viewcontroller,然后使窗口显在示屏幕上。

因此,当系统加载完info.plist,判断后发现没有main,就不会加载storyboard,也就不会帮我们创建UIWindow,那么我们需要自己在程序启动完成的时候也就是在didFinishLaunchingWithOptions方法中创建。

2.2 如何创建UIWindow?

首先根据系统加载storyboard时做的三件事情,我们可以总结出UIWindow创建步骤

    1. 创建窗口对象
    1. 创建窗口的根控制器,并且赋值
    1. 显示窗口

并且我们在AppDelegate.h中发现属性window

@property (strong, nonatomic) UIWindow *window;

那么我们来看一下如何创建UIWindow

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //创建窗口对象
    self.window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    //创建窗口的根控制器,并且赋值
    UIViewController *rootVc = [[UIViewController alloc]init];
    self.window.rootViewController = rootVc;
    //显示窗口
    [self.window makeKeyAndVisible];
    return YES;
}

窗口显示注意点:

    1. 我们看到系统为我们创建的window属性是strong强引用,是为了不让窗口销毁,所以需要强引用
    1. 窗口的尺寸必须设置,一般设置为屏幕大小。
    1. [self.window addSubview:rootVc.view];
    • 可直接将控制器的view添加到UIWindow中,并不理会它对应的控制器
    • 但是这种方法违背了MVC原则,当我们需要处理一些业务逻辑的时候就很麻烦了。
    • 当发生屏幕旋转事件的时候
      • UIApplication对象会将旋转事件传递给 UIWindow
      • UIWindow又会将旋转事件传递给它的根控制器,由根控制器决定是否需要旋转
      • UIApplication对象 -> UIWindow -> 根控制器。
        [self.window addSubview:rootVc.view];没有设置根控制器,所以不能跟着旋转)。
    1. 设置根控制器可以将对应界面的事情交给对应的控制器去管理。

makeKeyAndVisible的底层实现

那么[self.window makeKeyAndVisible];这个方法为什么就能显示窗口呢?我们来看一下[self.window makeKeyAndVisible];的底层实现了哪些功能:

当我们不调用这个方法,打印self.window。

UIWindow: 0x7f920503cc80; frame = (0 0; 414 736); hidden = YES; gestureRecognizers = ; layer = >

我们可以看到 hidden = YES;那么hidden = NO就可以显示窗口了
另外,我们在[self.window makeKeyAndVisible];前后分别输出一下application.keyWindow

NSLog(@"%@",application.keyWindow);
[self.window makeKeyAndVisible];
NSLog(@"%@",application.keyWindow);

打印内容

UIWindow[6259:1268399] (null)
UIWindow[6259:1268399] ; layer = >

我们可以看到调用[self.window makeKeyAndVisible];方法之后application.keyWindow就有值了,那么[self.window makeKeyAndVisible];的底层实现就很明显了。

  1. 可以显示窗口 self.window.hidden = NO;
  2. 成为应用程序的主窗口 application.keyWindow = self.window,这个会报错,因为application.keyWindow是readonly,所以我们没有办法直接赋值。

2.3 通过storyboard加载控制器

刚才我们提到过系统在加载storyboard的时候会做以下三件事情

  1. 创建窗口
  2. 加载mian.storyboard 并实例化ViewController
  3. 分配新视图控制器到窗口rootViewcontroller,然后使窗口显在示屏幕上。

那么我们用代码来模拟实现一下

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // 1.创建窗口
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

    // 2.加载main.storyboard,创建main.storyboard描述的控制器
    // UIStoryboard专门用来加载stroyboard
    // name:storyboard名称不需要后缀
    UIStoryboard *stroyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];

    // 加载sotryboard描述的控制器
    // 加载箭头指向的控制器
    UIViewController *vc = [stroyboard instantiateInitialViewController];
    //根据绑定标识加载 
    //UIViewController *vc = [stroyboard instantiateViewControllerWithIdentifier:@"red"];

    // 设置窗口的根控制器
    self.window.rootViewController = vc;

    // 3.显示窗口
    [self.window makeKeyAndVisible];

    return YES;
}

2.4 通过xib加载控制器

通过xib加载控制器和通过storyboard加载控制器类似,直接上代码

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

    // 创建窗口的根控制器
    // 通过xib创建控制器
    ViewController *vc = [[ViewController alloc] initWithNibName:@"VC" bundle:nil];

    //vc.view.backgroundColor = [UIColor redColor];

    self.window.rootViewController = vc;

    [self.window makeKeyAndVisible];

    return YES;
}

3.UIWindow的层级

UIWindow是有层级的,层级高的显示在最外面,当层级相同时,越靠后调用的显示在外面。

UIKIT_EXTERN const UIWindowLevel UIWindowLevelNormal; //默认,值为0
UIKIT_EXTERN const UIWindowLevel UIWindowLevelAlert; //值为2000 
UIKIT_EXTERN const UIWindowLevel UIWindowLevelStatusBar ; // 值为1000

所以UIWindowLevelNormal < UIWindowLevelStatusBar< UIWindowLevelAlert
并且层级是可以做加减的self.window.windowLevel = UIWindowLevelAlert+1;

四、事件响应者链

1. 事件响应者UIResponder

我们在上一篇文章已经介绍过了 事件响应者UIResponder 我们回顾一下它的主要职责:

  • UIResponder是iOS中所有响应者对象的基类,包括视图View视图控制器ViewController应用程序对象Application等。
  • UIResponder负责响应并处理来自用户的触摸事件按压事件加速事件远程控制事件键盘事件其他事件

对象派生链

我们在上一篇文章中也介绍了UIKit框架中对象的 继承架构图,从图中我们可以明确得到一个类的派生关系链:

  • UIResponder
    • UIView
      • ...
    • UIViewController
      • ...
    • UIApplication

换言之:

  • UIResponder是iOS中所有响应者对象的基类,包括UIApplication,UIViewController和UIView等都是继承自它。
    • 都有一个 nextResponder 方法,用于获取响应链中当前对象的下一个响应者,也通过nextResponder来串成响应链
  • 视图View视图控制器ViewController应用程序对象Application等都可以作为事件响应者对象

UIResponder 的头文件的几个属性和方法


NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>

#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif

--------------省略部分代码------------

  // Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

2. 事件响应者链(Responder Chain)

事件响应者链(Responder Chain)是iOS中用于处理事件响应的一种机制,它是由一系列UIResponder对象(UIResponder派生类的实例对象)构成的链式结构,用于确定事件响应的传递路径。

2.1 UIView的两个核心API

在介绍 事件响应者链 前,我们需要先了解两个API:

 // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//返回寻找到的最终响应这个事件的视图
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  

// default returns YES if point is in bounds
//判断某一个点击的位置是否在视图范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   

2.1.1 hitTest:withEvent:

/**
 * @return 本次点击事件需要的最佳 View
 */
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
  • UIKit 使用基于视图的 hit-testing 来确定 Touch 事件在哪里产生
    • UIKit 将 Touch 位置与视图层级中的视图对象的边界进行了比较。
    • UIView 的 hitTest:withEvent: 方法在视图层级中执行,寻找最深的包含指定 Touch 的子视图
    • 这个视图将成为 Touch 事件的第一响应者
  • 注意:
    • 如果 Touch 位置超过视图边界,hitTest:withEvent 方法将忽略这个视图和它的所有子视图。
      • 结果就是,当视图的clipsToBounds 属性为 NO,子视图超过视图边界也不会返回,即使它们包含发生的 Touch。
    • 当 touch 第一次产生时 UIKit 创建 UITouch 对象,在 touch 结束时释放这个 UITouch对象。
    • 当 touch 位置或者其他参数改变时,UIKit 更新 UITouch 对象新的信息

案例说明1

image.png

  • 把父视图的 userInteractionEnabled 设置为 NO,按钮 1 和按钮 2 都不会响应了
  • 如果点击按钮 2 视图,响应的是按钮 2,那么为什么点击按钮 2 和按钮 1 的交界处会是按钮 2 响应呢?
  • 事件传递给窗口或控件的后,就调用 hitTest:withEvent: 方法寻找更合适的 view。如果子控件是合适的 view,则在子控件再调用 hitTest:withEvent: 查看子控件是不是合适的 view,一直遍历,直到找到最合适的 view 或者废弃事件。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
   // ①、判断当前控件能否接收事件
   if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;

   // ②、判断触摸点在不在当前控件内
   if ([self pointInside:point withEvent:event] == NO) return nil;

   // ②、倒序遍历自己的子控件
   NSInteger count = self.subviews.count;
   for (NSInteger 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;  // 找到了最合适的 view
       }
   }
   // 循环结束,表示没有比自己更合适的 view
   return self;  
}

  • 所有当父视图 userInteractionEnabled 关闭时,return nil,子视图无法继续寻找最合适的 view。
  • 从后往前遍历子控件,图中按钮 2 在按钮 1 视图层级之上,所以按钮 2 是最合适的 view,还没有轮到按钮 1。

案例说明2

image.png

  • 视图层级从后往前依次是 C->D->A、E->F->B->父视图,父视图的 subviews = @[ B, A ]。当点击界面发生触摸事件时,遍历父视图的子视图,倒序遍历,先遍历的 A 视图。
  • 如果 A 视图 alpha < 0.01 || userInteractionEnabled = YES || hidden = NO,则 A 视图不是合适的View,返回 nil。开始遍历父视图的另一个子视图 B。
  • 如果 A 视图 alpha > 0.01 && userInteractionEnabled = YES && hidden = NO,则 A 视图可以接收触摸事件,并且触摸点在 A 视图内,则 A 视图为一个合适的 View,但还要继续从后往前遍历 A 视图的子视图;如果 A 视图的所有子视图返回 nil,则 A 视图则为最终合适的 view。
  • 如果 C 视图可以接收触摸事件且触摸点在 C 视图中,并且 C 视图的所有子视图返回 nil。
  • 如果 C 视图调用 hitTest:withEvent: 处理返回 nil,则查看 B 视图满足条件。以此类推。

2.1.2 pointInside:withEvent:

  • 判断触摸点是否在视图内:

    /**
     * @brief  判断一个点是否落在范围内
     */
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    
  • 如果现在要扩大按钮 2 的点击范围怎么办?如果要让按钮 1 只点击左右区域 40 像素有效,其他地方都不响应呢?image.png

    • 扩大响应范围:
      - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
          /* Inset `rect' by `(dx, dy)' -- i.e., offset its origin by `(dx, dy)', and decrease its size by `(2*dx, 2*dy)'. 
      
             CGRectInset 效果为 origin.x/y + dx/dy,size.width/height - 2 * dx/dy,这里 dx = -10,dy = -10
           */
          bounds = CGRectInset(self.bounds, -10, -10);
      
          return CGRectContainsPoint(bounds, point);
      }
      
    • 不规则的点击区域:
      /**
       * @brief  改变图片的点击范围
       */
      - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
          // 控件范围宽度 +40,高度 +40
          CGRect bounds = CGRectInset(self.bounds, -20, -20);
          UIBezierPath * path1 = [UIBezierPath bezierPathWithRect:CGRectMake(-20, 0, 40, 120)];
          UIBezierPath * path2 = [UIBezierPath bezierPathWithRect:CGRectMake(self.frame.size.width - 20, 0, 40, 120)];
          if (([path1 containsPoint:point] || [path2 containsPoint:point])&& CGRectContainsPoint(bounds, point)){
      
              return YES;  // 如果在 path 区域内返回 YES
          }
          return NO;
      }
      
  • 可以看出:

    • 在不规则区域内(红框)点击,[self pointInside:point withEvent:event] == YES,按钮 1 是最合适的 view,调用按钮 1 的点击事件。
    • 不在不规则区域内点击,无法调用按钮 1 的点击事件,[self pointInside:point withEvent:event] == NO
    • 在按钮 1 和按钮 2 重合区域(绿框)内点击
      • 调用按钮 2 的点击事件,因为按钮 2 图层在按钮 1 之上,遍历 subviews 时,从后往前遍历,先查看按钮 2
      • 按钮 2 调用 -hitTest:withEvent: 返回是最合适的 view,调用按钮 2 的点击方法。

2.1 事件传递链

1. 事件的分发和传递

  • 当 iOS 程序中发生触摸事件后,系统会将事件加入到 UIApplication 管理的一个任务队列中;
  • UIApplication 将处于任务队列最前端的事件向下分发,即 UIWindow
  • UIWindow 将事件向下分发,即 UIView
  • UIView
    • 首先看自己是否能处理事件:
    • 触摸点是否在自己身上。如果能,那么继续寻找子视图。
    • 遍历子控件,重复以上两步。
    • 如果没有找到,那么自己就是事件处理者。
    • 如果自己不能处理,那么不做任何处理。
  • 其中 UIView 不接受事件处理的情况主要有以下三种:
    • alpha < 0.01
    • userInteractionEnabled = NO
      • UIImageView的该属性值默认为NO
    • hidden = YES
  • 这个从父控件子控件寻找处理事件最合适的 view 的过程,如果父视图不接受事件处理,那么子视图也不能接收事件
  • 事件只要触摸了就会产生,关键在于是否有最合适的 view 来处理和接收事件
  • 如果遍历到最后都没有最合适的 view 来接收事件,则该事件被废弃

2.2 响应者链

  • 响应链是从最合适的 view 开始传递,处理事件传递给下一个响应者,
    • 响应者链的传递方法是事件传递的反方法
    • 如果所有响应者都不处理事件,则事件被丢弃。
    • 我们通常用响应者链来获取上几级响应者,方法是 UIResponder.nextResponder。
  • 在 App 中没有单一的响应链,UIKit 定义了默认的规则关于对象如何从一个响应者传递到另一个响应者,但是你可以重写响应者对象的方法来改变这些规则。
  • 通过重写响应对象的 nextResponder 属性改变响应链。许多 UIKit 的类已经重写了这个属性然后返回了指定的对象。
  • UIView
    • 如果视图是 ViewController 的根视图,下一个响应者为 ViewController
    • 否则是视图的父视图。
  • UIViewController
    • 如果视图控制器是 window 的根视图下一个响应者为 window 对象。
    • 如果视图控制器是由另一个视图控制器推出来,那么下一个响应者为正在推出的视图控制器。
  • UIWindow 下一个响应者为 UIApplication 对象。
  • UIApplication 下一个响应者为 app delegate,但是代理应该是 UIResponder 的一个实例,而不是 UIView、UIViewController 或者 App 对象本身

3. 事件的第一响应者

  • 事件的每个类型, UIKit 指定一个第一响应者, 然后最先发送事件到这个对象。第一响应者基于事件的类型而变化。
    • Touch event 第一响应者是触摸事件产生的 view;
    • Press event 第一响应者是焦点响应者;
    • Shake-motion events,Remote-control events,Editing menu messages 第一响应者是你或者 UIKit 指定的对象。
  • 注意:运动事件相关的加速度计陀螺仪磁强计都不属于响应者链,而是由 CoreMotion 传递事件给你指定的对象。
  • 控件直接与它相关的 target 对象使用 action 消息通信。
    • 当用户与控件交互时,控件调用 target 对象的 action 方法。换句话说,控件发送 action 消息到目标对象。
    • Action 消息不是事件,但是它仍然可以利用响应链。
    • 当控件的 target 对象为 nil,UIKit 从 target 对象和响应链走,直到找到一个对象实现了合适的 action 方法。
  • 如果视图有添加手势识别器,手势识别器接收 touch 和 press 事件在视图接收事件之前。
    • 如果所有的视图的手势识别器都不能识别它们的手势,这些事件会传递到视图处理。
    • 如果视图不能处理它们,UIKit 传递事件到响应链。