前言:对于接触业务开发的童鞋,自定义View的开发是进行最频繁的工做了。但发现一些童鞋仍是没有以一个好的规范甚至以一种错误的方式来搭建UI控件。由此,本文将以如下目录来进行讲叙,详细描述关于自定义View的一些书写注意事项。bash
1、关于自定义View的初始化方法markdown
一般咱们会建立私有方法createUI方法来建立当前自定义View所须要的子View。那上述所说的createUI应该放在自定义View的哪一个方法中呢?网络
一、init?oop
二、initWithFrame?布局
三、仍是为了考虑外部建立自定义View的方式不一样,在init与initWithFrame方法中均调用createUI方法?测试
咱们来一一验证,首先在CustomView的init方法中调用createUI方法。动画
- (instancetype)init { if (self = [super init]) { [self createUI]; } return self; } - (void)createUI { [self addSubview:self.testView]; } - (UIView *)testView { if (!_testView) { _testView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; _testView.backgroundColor = [UIColor redColor]; } return _testView; } 复制代码
外部以init形式建立CustomViewthis
CustomView *customView = [[CustomView alloc] init];
customView.frame = CGRectMake(100, 100, 200, 200);
customView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:customView];
复制代码
可验证,CustomView与其子视图都可正常显示。但有个问题是,若是外部以initWithFrame形式建立,没法调用createUI方法,所以子视图没法显示。spa
第二种初始化形式,单独在initWithFrame方法中调用createUI方法调试
- (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self createUI]; } return self; } 复制代码
可验证结果是,不管外部以init或者initWithFrame方法初始化CustomView,都可以正常显示CustomView与其子视图。
最后咱们作个实验,在init与initWithFrame方法中均调用createUI方法。调试createUI方法调用次数。
- (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self createUI]; } return self; } - (instancetype)init { if (self = [super init]) { [self createUI]; } return self; } - (void)createUI { NSLog(@"SubViews Add"); [self addSubview:self.testView]; } - (UIView *)testView { if (!_testView) { _testView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; _testView.backgroundColor = [UIColor redColor]; } return _testView; } @end 复制代码
外部建立CustomView仍使用init形式 经过打印结果或断点可验证createUI方法被执行了两次!
2019-06-26 17:17:51.961744+0800 TestAddSubview[72346:1989647] SubViews Add
2019-06-26 17:17:51.961917+0800 TestAddSubview[72346:1989647] SubViews Add
复制代码
其实,上述三种假设均和一个问题相关,即自定义View的init方法是否会默认调用initWithFrame方法。
答案是确定的,经过上述的代码调试流程,咱们能够获得以下结论,关于代码的调用过程(之外部初始化init为例):
一、动态查找到CustomView的init方法
二、调用[super init]方法
三、super init方法内部执行的的是[super initWithFrame:CGRectZero]
四、若super发现CustomView实现了initWithFrame方法
五、转而执行self(CustomView)的initWithFrame方法
六、最后在执行init的其他部分
这里也能够验证一个结论:OC中的super其实是让某个类去调用父类的方法,而不是父类去调用某个方法,方法动态调用过程顺序是由下而上的(这也是为何只在init方法中进行createUI不会执行屡次的缘由,由于父类的initWithFrame没作createUI操做)。
结论: createUI方法最好在initWithFrame中调用,外部使用init或initWithFrame都可以正常执行createUI方法。不要在自定义View中同时重写init与initWithFrame并执行相同视图布局代码。会致使布局代码(createUI)执行屡次。
2、关于addSubview
咱们接着问题一自定义View的初始化方法来讲,若是同时在init与initWithFrame中同时调用了createUI方法,会有什么影响呢?
显而易见的是createUI方法执行了屡次,也就是说重复屡次添加了self.testView。那是否会重复添加多个View层呢?
并不会,重复屡次添加同一个View并不会产生多层级的状况。 咱们看下addSubview的文档描述
This method establishes a strong reference to view and sets its next responder to the receiver, which is its new superview. Views can have only one superview. If view already has a superview and that view is not the receiver, this method removes the previous superview before making the receiver its new superview.
大概阐述的意思是,View有且仅有一个父视图,若是新的父视图与原父视图不同,会将View在原视图中移除,添加到新视图上。
所以同一父视图重复添加同一个View并不会产生多层级。 能够简单经过代码验证,咱们在createUI中循环添加self.testView,最终打印当前视图的子视图个数
- (void)createUI { for (NSInteger i = 0; i < 100; i++) { [self addSubview:self.testView]; } NSLog(@"subviewsCount = 【%ld】",self.subviews.count); for (UIView *view in self.subviews) { NSLog(@"subView 【%@】",view); } } 复制代码
运行可见,视图的子视图个数始终为1
2019-06-28 16:02:50.420144+0800 TestAddSubview[78991:832644] subviewsCount = 【1】
2019-06-28 16:02:50.422151+0800 TestAddSubview[78991:832644] subView 【<UIView: 0x7f80a9c09590; frame = (0 0; 100 100); layer = <CALayer: 0x600003ff0a40>>】
复制代码
根据打印结果可验证,CustomView始终只存在一个子视图(testView)。
新旧父视图一致,咱们能够假设苹果作了以下处理:
一、在旧父视图中移除子视图,再从新将子视图添加到父视图上
二、判断新旧父视图是否一致,若一致,不作任何操做。
由于没法看到addSubview的源码,猜想可能会有这两种状况,我的更偏向第二种处理。(可重写子视图layoutSubviews方法,由于addSubviews会调用layoutSubviews方法,咱们能够调试layoutSubviews的调用次数,测试后可验证addSubviews作了上述二的处理)
结论:若父视图重复添加同一子视图,并不会产生多层级状况。由于此例中testView是以懒加载的形式建立,因此self每次添加的均为同一个View,但若是在createUI中以UIView *testView = [UIView alloc] initWithFrame的形式建立,那就会建立出多层级的View。
总结:自定义View的子视图最好以懒加载形式建立,可避免因其余书写不当致使的异常
3、关于layoutSubviews
关于这一点,主要想聊一聊layoutSubviews的调用时机
一、setNeedsLayout\ layoutIfNeeded
二、addSubview
三、View的大小发生变化,未变不调用
四、UIScrollView滑动
五、旋转Screen会触发父UIView上的layoutSubviews事件
所以对于layoutSubviews的使用咱们须要注意如下几点:
一、自定义视图的init方法并不会调用layoutSubviews
二、苹果声明不要直接调用layoutSubviews方法,若是须要更新,应该调用setNeedsLayout方法,视图会在下一次绘制后更新。若是须要当即更新视图,须要执行layoutIfNeeded方法
三、由于layoutSubviews调用比较频繁,所以若无特殊需求(文档所述为执行精确的子视图布局时可以使用),不用重写layoutSubviews方法。
4、关于frame与bounds
众所周知,在iOS UI控件中有两个关于位置大小的很是重要的属性,frame与bounds
UI控件的frame意为相对于该控件父视图的位置,bounds意为相对于控件自己的位置。 frame、bounds均为结构体CGRect,由CGPoint与CGSize组成,咱们能够经过 frame.origin/bounds.origin 与frame.size/bounds.size来进行返回控件左上角位置与大小。
一般给View添加动画,能够直接操做Frame或者获得Layer设置隐式动画。
那若是咱们直接操做View的bounds会有什么状况出现呢?
有以下例子,有三个View,分别为RedView、BlueView、GreenView,RedView添加在当前视图控制器上,BlueView为RedView的子视图,GreenView为BlueView的子视图,坐标分别为(10,10,200,200)、(10,10,150,150)、(10,10,100,100),其坐标位置以下图所示:
若修改BlueView的bounds为(0,10,150,150),那么会有什么状况出现呢?
可能咱们一般移动View不会经过bounds而是frame,而且也知道bounds是相对于自身的坐标,修改其origin不会对其自己产生什么影响,但这就大错特错了,咱们来看此状况的结果,三个View的展现状况变成了下图所示:
BlueView位置并无什么变化,GreenView却由于BlueView的修改,其位置上移了10坐标点!
咱们来看下缘由,由于调整里BlueView的bounds,致使BlueView相对于本身的坐标上移了10坐标点,GreenView相对于其父视图的位置也一样上移了10坐标点。对于GreenView,他的父视图BlueView的左上角已经不是(0,0),而是(0,10),所以会有上图的结果。
总结:在CustomView中尽可能使用frame来作某些操做,不出于特殊需求,不要修改bounds的origin属性,会形成难以预期的Bug。(不会影响当前视图,可是会间接影响其子视图)
好了,暂时写到这里,无规矩不成方圆,对于开发者一个好的代码规范每每能够事半功倍,共勉!