自定义View的书写注意事项

  • 关于自定义View的初始化方法
  • 关于addSubview
  • 关于layoutSubviews
  • 关于frame与bounds
一、关于自定义View的初始化方法

通常我们会创建私有方法createUI方法来创建当前自定义View所需要的子View。那上述所说的createUI应该放在自定义View的哪个方法中呢?

1、init?
2、initWithFrame?
3、还是为了考虑外部创建自定义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形式创建CustomView

CustomView *customView = [[CustomView alloc] init];
customView.frame = CGRectMake(100, 100, 200, 200);
customView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:customView];

可验证,CustomView与其子视图均可正常显示。但有个问题是,如果外部以initWithFrame形式创建,无法调用createUI方法,因此子视图无法显示

第二种初始化形式,单独在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为例):
1、动态查找到CustomView的init方法
2、调用[super init]方法
3、super init方法内部执行的的是[super initWithFrame:CGRectZero]
4、若super发现CustomView实现了initWithFrame方法
5、转而执行self(CustomView)的initWithFrame方法
6、最后在执行init的其余部分

这里也可以验证一个结论:OC中的super实际上是让某个类去调用父类的方法,而不是父类去调用某个方法,方法动态调用过程顺序是由下而上的(这也是为什么在init方法中进行createUI不会执行多次的原因,因为父类的initWithFrame没做createUI操作)。

结论:
createUI方法最好在initWithFrame中调用,外部使用init或initWithFrame均可以正常执行createUI方法。不要在自定义View中同时重写init与initWithFrame并执行相同视图布局代码。会导致布局代码(createUI)执行多次。

二、关于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)。

新旧父视图一致,我们可以假设苹果做了如下处理:
1、在旧父视图中移除子视图,再重新将子视图添加到父视图上
2、判断新旧父视图是否一致,若一致,不做任何操作。

因为无法看到addSubview的源码,猜测可能会有这两种情况,个人更偏向第二种处理。(可重写子视图layoutSubviews方法调试调用次数也可验证addSubviews做了上述二的处理)

结论:若父视图重复添加同一子视图,并不会产生多层级情况。因为此例中testView是以懒加载的形式创建,因此self每次添加的均为同一个View,但如果在createUI中以UIView *testView = [UIView alloc] initWithFrame的形式创建,那就会创建出多层级的View。

总结:自定义View的子视图最好以懒加载形式创建,可避免因其他书写不当导致的异常

三、关于layoutSubviews

关于这一点,主要想聊一聊layoutSubviews的调用时机
1、setNeedsLayout\ layoutIfNeeded
2、addSubview
3、View的大小发生变化,未变不调用
4、UIScrollView滑动
5、旋转Screen会触发父UIView上的layoutSubviews事件

因此对于layoutSubviews的使用我们需要注意以下几点:
1、自定义视图的init方法并不会调用layoutSubviews
2、苹果声明不要直接调用layoutSubviews方法,如果需要更新,应该调用setNeedsLayout方法,视图会在下一次绘制后更新。如果需要立即更新视图,需要执行layoutIfNeeded方法
3、因为layoutSubviews调用比较频繁,因此若无特殊需求(文档所述为执行精确的子视图布局时可使用),不用重写layoutSubviews方法。

四、关于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),其坐标位置如下图所示:

View1

若修改BlueView的bounds为(0,10,150,150),那么会有什么情况出现呢?

可能我们通常移动View不会通过bounds而是frame,并且也知道bounds是相对于自身的坐标,修改其origin不会对其本身产生什么影响,但这就大错特错了,我们来看此情况的结果,三个View的展示情况变成了下图所示:

View2

BlueView位置并没有什么变化,GreenView却因为BlueView的修改,其位置上移了10坐标点!

我们来看下原因,因为调整里BlueView的bounds,导致BlueView相对于自己的坐标上移了10坐标点,GreenView相对于其父视图的位置也同样上移了10坐标点。对于GreenView,他的父视图BlueView的左上角已经不是(0,0),而是(0,10),因此会有上图的结果。

总结:在CustomView中尽量使用frame来做某些操作,不出于特殊需求,不要修改bounds的origin属性,会造成难以预期的Bug。(不会影响当前视图,但是会间接影响其子视图)


推荐文集

收录:原文地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容