43

如何写好一个自定义View

 5 years ago
source link: http://www.cocoachina.com/ios/20181108/25411.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

对于iOS开发来说,写一个自定义view,或者恰当地使用tableview基本上可以算的上是“行活”。但是看过一些同学写的自定义控件后,有时感觉似乎写的不够好,虽然可以正常工作,但是在可拓展性、易用性、以及稳定性上都有所欠缺。所以我打算写一个系列,就叫做如何写好xxx,就总结下我认为的好的写法应该是什么样的,这篇便是这系列的第一篇。

当然受视野和水平所限,文章中提到的一些东西并不一定是最优解,非常欢迎大家提出不同的意见,讨论后共同成长!

目标

使用方式多样

  • 纯代码中使用

  • Xib/storyboard中使用

使用的易用性

  • 尽量简单的接口设计

  • 尽量少的暴露实现

  • 对异常情况的处理

实现

初始化方法

这里我们大可借鉴一下UIKit中系统的UI组件是如何设计自己的初始化方法的。

UIKit中初始化方法大概分为两类,

1.继承自父类的Designated initializer

  • initWithFrame

  • initWithCoder(不是所有的UI类都继承UIView,例如继承NSObject的UIBarItem,这些就没有initWithFrame方法)

2.Convenience Initializer,例如UITabBarItem中的

- (instancetype)initWithTitle:(nullable NSString *)title image:(nullable UIImage *)image tag:(NSInteger)tag;
- (instancetype)initWithTitle:(nullable NSString *)title image:(nullable UIImage *)image selectedImage:(nullable UIImage *)selectedImage NS_AVAILABLE_IOS(7_0);
- (instancetype)initWithTabBarSystemItem:(UITabBarSystemItem)systemItem tag:(NSInteger)tag;

等方法

首先,我们要搞清楚什么是Designated initializer和Convenience Initializer。

  • Designated initializer,初始化类必须有的属性

  • Convenience Initializer,提供便利的初始化方法,根据需要为某些属性提供默认值,方法内部实现最终还是会调用Designated initializer;

其次,为什么UIView的子类都会有两个Designated initializer呢?这里就是我们之前提到的,View的两种使用方法,Xib/storyboard,和纯代码。

实现Designated initializer

为了既能满足纯代码的方式,又能使用Xib的方式,我们需要实现CustomView的两个Designated initializer

而且在swift中,initWithCoder已经被标记为required,所以必须要实现啦

在实现这两个方式时,主要做的就是添加子view,以及提供默认值

提供Convenience Initializer

例如UIImageView,他就提供了initWithImage 这个Convenience Initializer。

使用Convenience Initializer的好处也是显而易见的,能让类的使用者很清楚的知道我应该如何正确的初始化这个类。而且会对必需的属性提供默认值,既能极大的避免了调用者只调用init,导致该实例并不能正常工作,又能在很多属性时,提供一个简单的初始化方法。

内部子view布局的实现

frame or autoLayout?

如果使用frame,我们需要保证custom view自己的size发生变化的时候,subviews能够自动变化,而不是保持原有的frame。(autoresizingMask,autoresizesSubviews)

如果使用autoLayout,就不存在上面的问题,唯一一个需要考虑的问题便是性能了。通过WWDC也可以知道,虽然苹果对于autolayout一再优化,仍然在多视图情境下,性能远不如frame

我的观点 :如果页面层级不复杂,性能差别也不大,我还是倾向使用AutoLayout,毕竟算frame也是比较麻烦,而且代码可读性也要比AutoLayout差很多

构建视图

这时需要解释几个很重要的方法,以及什么时候需要使用

-(void)drawRect:(CGRect)rect

使用场景:需要使用Core Graphics或者UIKit绘制页面时,如果是使用已有UI控件addsubview组合自己的view,则不需要重写这个方法

被调用时机:view首次显示的时候,或者某个事件导致了view需要更新,不要直接手动调用。如果需要重绘,调用 setNeedsDisplay 或者 setNeedsDisplayInRect:

参数说明:view需要更新的范围,如果是连续的绘制,那么rect可能只是view的一部分

-(void)layoutSubviews

使用场景:只有当autoresizing和约束不能满足你的需求时,才重写layoutSubviews来提供更精确的布局

被调用时机:不要直接手动调用,调用setNeedsLayout来更新约束,如果需要立即更新约束,那么调用 layoutIfNeeded

-(void)updateConstraints;

使用场景:为了优化约束的变化,需要提早改变约束,或者产生大量冗余修改时。

被调用时机:不要直接手动调用,在view需要修改约束的时候,调用setNeedsUpdateConstraints

注意事项:

在实现的最后,调用[Super updateConstraints]

不要在方法实现中调用setNeedsUpdateConstraints,会产生循环

接口的设计

接口设计尽量遵循Effective Objective-C 2.0中的建议,比如必须暴露的属性尽量为readonly,内部实现的私有方法不必暴露出去。在设计接口的时候,时刻要想着,这个方法,这个属性真的有必要让别人知道吗?这个方法真正的目的是什么?总之,尽量遵循Keep It Simple, Stupid就对了。

线程管理

所有UI的操作都应该在主线程进行,这需要我们在涉及到UI变动的方法中,确保是主线程,而不依赖使用者

一个简单的例子

TCZoomingImageView是基于UIScrollView和UIImageView做一个可缩放的View,实现非常的简单,仅作为一个简单的例子,抛砖引玉。

GitHub地址: TCZoomingImageView

作者:王天池

链接:https://juejin.im/post/5bb1e7df5188255c9c754182


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK