以前写过一个文章《如何用Swift创建自定义iOS控件》,文章主要以Swift语言讲解,图个新鲜,其原理跟使用OC语言是一致的。这次再写一个进阶的文章,重点讲一下如何制作可以和Xcode IDE交互的控件,以及如何与AutoLayout系统协作。
我们以一个自定义按钮为出发点讲解,创建方法和《如何用Swift创建自定义iOS控件》 类似,也是基于Nib来创建我们的控件。Demo项目在Github上这里, Demo还是有一定使用价值的,虽然还有很多需要完善的地方。
使用效果图如下:
因为是进阶教程,就不再一步一步手把手教了,重点阐述一些疑惑和踩坑的地方。
-
自定义控件如何与Xcode交互?
- 属性前用IBInspectable修饰(Swift的话是@IBInspectable)
- 类声明用IB_DESIGNABLE修饰(Swift的话是用@IBDesignable)
-initWithFrame:
,-initWithCoder:
以及-awakeFromNib
分别在什么时候调用。
-initWithFrame:
是使用代码创建控件的时候调用,-init:
方法初始化也会调用,相当于调用[self initWithFrame:CGZeroRect]
。-initWithCoder:
是在Nib加载的时候调用的,其中包括在独立的Xib文件,或者Storyboard文件,或者通过UINib类手写代码加载。-awakeFromNib
是Nib加载完毕,并且设置好所有的属性之后调用。
因此,你看代码就会看到在这三个方法里分别做三个不同的事情。
-initWithFrame:
需要做完所有的工作,包括创建视图createViews,初始化默认值setupDefaults(吐槽一下OC的属性不能设置初始值,Swift就舒服很多很多),设置视图表现setupViews。
而-initWithCoder:
则需要调用createViews和setupDefaults,如果在这里尝试做setupViews的话,那么那些设计过程中修改了的属性值就不会生效了,因为这时候的属性值都还没有初始化。
因此,只有在-awakeFromNib
才做setupViews的工作。
-
Xcode IDE怎么更新自定义控件的视图表现的呢?
最开始,我刚写demo的时候用的办法是为每个IBInspectable属性编写自定义set方法,每次设置属性就重新执行一下setupViews方法。这个做法在设计阶段不考虑执行效率的话还行。像我们的自定义按钮,有10多个自定义属性,如果运行时候每次设置属性都执行一次setupViews,这样的效率肯定是不能接受的。那么, 什么才是正道呢?答案是
-prepareForInterfaceBuilder
方法。Xcode需要更新设计UI的时候就会调用
-prepareForInterfaceBuilder
方法,并且,这个方法在运行时是不会被调用的,完美!另外,一个特别的宏TARGET_INTERFACE_BUILDER
,这个宏只有在IDE执行代码的时候才会被定义,利用这个宏,可以做一些针对设计阶段执行的代码,例如本例就针对设计初始化一个Title的默认值。 -
如何与AutoLayout系统对接?
我们自定义控件内部的子视图是在自己的Nib文件中做了AutoLayout的约束的。但是,自定义按钮作为一个整体被外部时候的时候,外部也会有AutoLayout去约束它的大小和位置。这时候,AutoLayout系统需要知道一个重要的信息:你的控件内容有多大?
当AutoLayout需要询问控件的内容大小的时候,它会调用
-intrinsicContentSize
,我们的自定义控件根据各个子视图(都是一些UILabel和UIImageView)的内容大小和间隔信息返回一个值就行了。注意:这个值必须是不依赖frame的。想想UILabel和UIButton的行为你大概就会明白这点了,这些控件你只需要定义位置约束,而不需要定义视图大小约束就能完成约束了。当然,你也可以强制定义视图大小约束,这时候就有内容大小和视图大小约束优先级的问题,需要结合Hugging和Compression Resistance的优先级来确定最终内容大小和视图大小了。
布局完成之后的位置调整
最初版本的Demo有个BUG,如果Title左右位置的图标大小如果一样,那么整体内容并没有居中。出现这个BUG的原因是,我们把Title约束在整个内容视图的中间。当左右两个icon大小一致,或者Title左右的Span值不相同的情况下,需要把Title的中心位置偏移一下才能正确的表现整体内容居中的效果来。那么问题了,在哪里调整才合适呢?(怎么调整就只是算法问题,看代码就能明白)。答案是-layoutSubviews
这个方法会在AutoLayout完成视图布局之后调用,正式我们需要调整便宜的地方,因为,只有在视图布局完成之后,我们才能知道各个子视图正确的Frame大小。
-
layoutSubviews里再修改约束不会导致无限循环吗?
这个问题,我没有找到答案,只有调试能说明,答案是:不会?至于原理,没有找到合适的官方文档说明。有读者知道的,请不吝赐教。
-
其他的一些小细节
IconFont看上去真是个挺不错的东西,资源小,矢量拉伸,可更改颜色,图标资源丰富。再结合一个制作工具,把自己项目用到的图标单独抽取出来,只带一个小字体文件就能搞定APP的所有图标,非常的赞。