前言
本篇文章紧接着上一篇文章中图形处理框架-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
部分
- 响应系统事件:负责响应系统事件,包括:
设备方向变化
内存警告
视图控制器切换
等
- 其它事件:
手势事件
等
- UIResponder中的几种事件:
- 管理
- 负责 管理
UIKit
应用程序的视图层次结构
的对象- 通过VC的
生命周期方法
,负责处理UI的加载
、显示
、布局
、交互
、旋转
、隐藏
和卸载
等任务 - 实现容器视图控制器(在接下来的篇幅介绍相关API)
- 通过VC的
- 页面切换:
- 通过
容器控制器
(UINavigationController、UITabBarController)来管理界面之间的切换和导航 - 通过 URLRouter(URL+OpenURL+容器控制器 配合) 进行页面切换
- 通过
- 页面传值:
可以通过属性
、委托
、通知
、Block回调(闭包回调)
、路由跳转传参
等方式进行数据传递和通信 - ...
2. UIViewController|生命周期相关 API
我们先介绍
UIViewController生命周期
相关的API:
loadView()
:- 简介:用于创建或加载视图控制器的视图层次结构。
- 说明:如果视图控制器通过storyboard创建,通常不需要重写这个方法。
viewDidLoad()
:- 简介:视图已经加载完成,此时可以进行一些初始化操作,如添加子视图、设置视图的初始状态等。
viewWillAppear(_ animated: Bool)
:- 简介:视图即将显示在屏幕上,此时视图控制器可以做一些在界面显示之前需要准备的工作,比如更新数据。
- 参数:animated表示视图是否以动画形式显示。
viewDidAppear(_ animated: Bool)
:- 简介:视图已经显示在屏幕上,此时可以执行一些需要在界面显示完成后立即执行的操作,比如启动定时器。
- 参数:animated表示视图是否以动画形式显示。
viewWillDisappear(_ animated: Bool)
:- 简介:视图即将从屏幕上消失,此时可以做一些在界面消失之前需要处理的工作,比如保存数据。
- 参数:animated表示视图是否以动画形式消失。
viewDidDisappear(_ animated: Bool)
:- 简介:视图已经从屏幕上消失,此时可以执行一些需要在界面消失后立即执行的操作,比如停止定时器。
- 参数:animated表示视图是否以动画形式消失。
viewWillLayoutSubviews()
:- 简介:视图将要布局子视图时调用,可以在此方法中更新子视图的布局。
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(
FirstViewController
、SecondViewController
) - 将重写
UIViewController生命周期
相关方法 和 类初始化
和反初始化
相关的方法 分别插入FirstViewController
、SecondViewController
- 编写代码,由
FirstViewController
push打开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
- —> FirstViewController
- —>
viewWillAppear
- —>
viewWillLayoutSubviews
- —>
viewDidLayoutSubviews
- —> FirstViewController
viewDidDisappear
- —>
- —>
viewDidAppear
- (pop页面阶段开始)—>
viewWillDisappear
- —> FirstViewController
viewWillAppear
- —> FirstViewController
- —>
viewDidDisappear
- —> FirstViewController
viewDidAppear
- —> FirstViewController
- —>
- —>
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|几种初始化方式
这一块相对简单,我们列举一下就好:
-
纯代码创建
:
在代码中使用UIViewController的init(nibName:bundle:)或者init()方法创建视图控制器,并设置相应的属性。
let viewController = MyViewController()
-
Storyboard创建
:
在Storyboard中创建UIViewController,并设置对应的类名。
let storyboard = UIStoryboard(name: "Main", bundle: nil) let viewController = storyboard.instantiateViewController(withIdentifier: "MyViewController") as! MyViewController
-
XIB文件创建
:
在XIB文件中创建UIViewController,并设置对应的类名。
let viewController = MyViewController(nibName: "MyViewController", bundle: nil)
-
- 使用UIStoryboard的instantiateInitialViewController方法:
用于从Storyboard中实例化初始化的视图控制器。
let storyboard = UIStoryboard(name: "Main", bundle: nil) let initialViewController = storyboard.instantiateInitialViewController() as! MyViewController
- 使用UIStoryboard的instantiateInitialViewController方法:
-
自定义初始化方法
:
有时候视图控制器可能有一些自定义的初始化方法,可以根据需要进行调用。
let viewController = MyViewController(customParameter: parameter)
5. UIViewController|几种页面传值方式
这一块相对简单,我们列举一下就好:
正向传值:
初始化方法
传值属性
传值
逆向传值:
- Delegate
- 回调闭包Block/Closure
可逆向传值也可正向传值的几种方式
全局单例
传值通知广播
传值- 模块管理工具模块间通讯
EventBus
,事件管理派发
传值 跳转路由
传值(本质还是属性传值)
6. UIViewController|几种页面跳转方式
这一块相对简单,我们列举一下就好:
- 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 // 设置目标视图控制器的属性等 } }
- Modal方式跳转:
以模态形式显示目标视图控制器,覆盖在当前视图控制器之上。// 以模态形式显示目标视图控制器 present(destinationVC, animated: true, completion: nil) // 关闭模态视图控制器返回到上一个视图控制器 dismiss(animated: true, completion: nil)
- 通过容器控制器管理:
- 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()
- a. Navigation Controller:
7. UIViewController|自定义转场动画
本文先介绍一下自定义转场动画的核心要点,对具体动画的实现等,在后面介绍动画相关章节的时候,会有更详尽的分享。
7.1 核心要点
切换页面转场
的几种方式:- 通过
UIViewController
Modal
出一个新VC的页面 - 通过容器控制器 切换 页面
- 通过
UINavigationController
进行Push
或Pop
操作,作VC间的页面切换 - 通过
UITabBarController
对selectIndex
重新赋值,,进行选中VC的切换
- 通过
- 通过
- 转场方式:
- 默认转场动画: 系统的
Modal
、Push
或Pop
、selectVC
切换 - 自定义转场动画:
- 交互性(实现动画的实例+手势交互)
- 非交互形(实现动画的实例)
- 默认转场动画: 系统的
- 注意:
- 系统默认转场动画,是系统提供了
默认实现动画实例
- 因此,我们要自定义转场动画,也要
- 提供
自定义的实现动画实例
- 在页面转场的时机,将
自定义的实现动画实例
提交 给系统API- 系统 通过
Delegate
回调方法 把 页面切换的时机告诉我们
- 系统 通过
- 提供
- 系统默认转场动画,是系统提供了
因此,接下来我们就要 重点介绍 转场动画 相关的 几个协议(OC、Swift版本的API基本一样.这里用OCAPI介绍)
7.2 实现自定义动画对象|UIViewControllerAnimatedTransitioning
实现自定义动画步骤:
-
- 自定义动画对象:
自定义Class,遵守UIViewControllerAnimatedTransitioning
协议
- 自定义动画对象:
-
- 实现协议中的核心API:
动画执行时间
:
- transitionDuration:transitionContext
动画具体实现
- animateTransition:
动画执行结束的回调
- animationEnded:
-
- 在页面转场的时机回调方法中,返回给系统
该自定义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:
- Modal推出:
- 非交互型(一般添加pan手势进行交互)
- Modal推出:
- interactionControllerForPresentation:
- dismiss返回:
- interactionControllerForDismissal:
- Modal推出:
@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手势添加交互逻辑。编写交互逻辑要点如下:
-
- 在回调方法中,获取
开始交互的时机
- 在回调方法中,获取
-
- 给vc的view添加交互逻辑
-
- 根据交互逻辑 计算出 转场 动画 的 百分比,把百分比值percent 提交给 VC页面切换的
上下文对象
。以达到,通过交互控制转场动画的效果
- 根据交互逻辑 计算出 转场 动画 的 百分比,把百分比值percent 提交给 VC页面切换的
-
- 这依然依赖我们实现的自定义转场动画
-
- 我们可以用 继承系统的
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核心要点
这样的介绍显然太过简洁难懂,我们结合多年的项目实施经验给予更详细的诠释:
- 作用:
- 每个应用程序都 至少有一个
UIWindow
对象,它是应用程序中的主窗口。 UIWindow
是应用程序中视图层次结构的顶层容器
,它负责管理应用程序中所有视图的显示和布局。
- 每个应用程序都 至少有一个
- 层级关系:
UIWindow
对象位于视图层次结构的最顶层
,所有其他视图都是它的子视图
或子视图的子视图
。
- 创建方式:
- 可以通过
UIWindow
类的init(frame:)
方法或init(windowScene:)
方法来创建一个窗口对象。 - 通常情况下,
UIWindow
对象是由系统自动创建并管理的,开发者无需手动创建。
- 可以通过
- 关键属性:
rootViewController
:窗口的根视图控制器,决定了窗口中显示的内容。windowScene
:窗口所属的场景对象,用于多窗口管理。
- 常用方法:
makeKeyAndVisible()
:将窗口设置为主窗口
,并显示在屏幕上
。resignKey()
:将窗口从主窗口中移除
。
- 事件处理:
UIWindow
对象是响应者链中的一部分
,可以处理触摸事件、运动事件等。- 通常情况下,
UIWindow
对象会将触摸事件传递给其子视图
或根视图控制器
进行处理。
- 窗口管理:
- iOS应用程序可以包含多个窗口,每个窗口可以显示不同的内容。
多窗口
管理通常用于支持多任务处理
、多屏幕显示
等功能。
- 使用场景:
UIWindow
通常用于显示应用程序的主界面
、弹出窗口
、警告框
等。- 也可以用于实现一些特殊效果,如
悬浮按钮
、悬浮窗口
等。
- 其它:
- iOS程序启动完毕后,创建的第一个视图控件就是UIWindow
- 接着创建控制器的View
- 最后将控制器的View添加到UIWindow上,于是控制器的View就显示在屏幕上了
状态栏
和键盘
是特殊的UIWindow
- iOS程序启动完毕后,创建的第一个视图控件就是UIWindow
那么UIWindow是如何将View显示到屏幕上的呢?
- 这里有三个重要的对象
UIScreen
,UIWindow
,UIView
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
函数了解程序启动的过程
- 根据传递的类名创建UIApplication对象,这是第一个对象
- 创建UIApplication代理对象,并给UIApplicaiton对象设置代理
- 开启
主运行循环
main events loop处理事件,保持程序一直运行- 加载info.plist,判断是否指定mian(xib 或者 storyboard)如果指定就去加载
当我们把指定的Main Interface
中mian给删除的时候,重新运行程序,就会发现我们之前设置的view没有办法显示了。
此时我们基本可以想到,UIWindow应该是在加载storyboard的时候系统创建的,那么系统是如何加载storyboard的呢?
系统在加载storyboard的时候会做以下三件事情:
-
- 创建窗口
-
- 加载mian.storyboard 并实例化view controller
-
- 分配新视图控制器到窗口root viewcontroller,然后使窗口显在示屏幕上。
因此,当系统加载完info.plist,判断后发现没有main,就不会加载storyboard,也就不会帮我们创建UIWindow,那么我们需要自己在程序启动完成的时候也就是在didFinishLaunchingWithOptions
方法中创建。
2.2 如何创建UIWindow?
首先根据系统加载storyboard时做的三件事情,我们可以总结出UIWindow创建步骤
-
- 创建窗口对象
-
- 创建窗口的根控制器,并且赋值
-
- 显示窗口
并且我们在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;
}
窗口显示注意点:
-
- 我们看到系统为我们创建的window属性是
strong强引用
,是为了不让窗口销毁,所以需要强引用
- 我们看到系统为我们创建的window属性是
-
- 窗口的尺寸必须设置,一般设置为屏幕大小。
-
[self.window addSubview:rootVc.view];
- 可直接将控制器的view添加到UIWindow中,并不理会它对应的控制器
- 但是这种方法违背了MVC原则,当我们需要处理一些业务逻辑的时候就很麻烦了。
- 当发生屏幕旋转事件的时候
UIApplication
对象会将旋转事件传递给 UIWindow- UIWindow又会将旋转事件传递给它的根控制器,由根控制器决定是否需要旋转
UIApplication
对象 -> UIWindow -> 根控制器。
([self.window addSubview:rootVc.view];
没有设置根控制器,所以不能跟着旋转)。
-
- 设置根控制器可以将对应界面的事情交给对应的控制器去管理。
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];
的底层实现就很明显了。
- 可以显示窗口
self.window.hidden = NO;
- 成为应用程序的主窗口
application.keyWindow = self.window
,这个会报错,因为application.keyWindow
是readonly,所以我们没有办法直接赋值。
2.3 通过storyboard加载控制器
刚才我们提到过系统在加载storyboard的时候会做以下三件事情
- 创建窗口
- 加载mian.storyboard 并实例化ViewController
- 分配新视图控制器到窗口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 对象新的信息
- 如果 Touch 位置超过视图边界,hitTest:withEvent 方法将忽略这个视图和它的所有子视图。
案例说明1
- 把父视图的 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
- 视图层级从后往前依次是 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 像素有效,其他地方都不响应呢?
- 扩大响应范围:
- (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 传递事件到响应链。