iOS自定义控件开发梳理
66151718
8年前
<p>在日常iOS开发中,系统提供的控件常常无法满足业务功能,这个时候需要我们实现一些自定义控件。自定义控件能让我们完全控制视图的展示内容以及交互操作。本篇将介绍一些自定义控件的相关概念,探讨自定义控件开发的基本过程及技巧。</p> <h3><strong>UIView</strong></h3> <p>在开始之前我们先介绍一个类UIVew,它在iOS APP中占有绝对重要的地位,因为几乎所有的控件都是继承自UIView类。</p> <p>UIView表示屏幕上的一个矩形区域,负责渲染区域内的内容,并且响应区域内发生的触摸事件。</p> <p>在UIView的内部有一个CALayer,提供内容的绘制和显示,包括UIView的尺寸样式。UIView的frame实际上返回的CALayer的frame。</p> <p>UIView继承自UIResponder类,它能接收并处理从系统传来的事件,CALayer继承自NSObject,它无法响应事件。所以UIView与CALayer的最大区别在于:UIView能响应事件,而CALayer不能。</p> <h3><strong>两种实现方式</strong></h3> <p>在创建自定义控件时,主要有两种实现方式,分别是纯代码以及xib。接下来我们用这两种方式分别演示一下创建自定义控件的步骤。</p> <p>我们实现一个简单的demo ,效果如下,封装一个圆形的imageView。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/5952dee97715c11dcc6b31faa54833f6.png"></p> <h3><strong>使用代码创建自定义控件</strong></h3> <p>使用代码创建自定义控件,首先创建一个继承自UIView的类</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/1072230a75c69b300a0ae7779bb08ffd.png"></p> <p>实现initWithFrame:方法。在该方法中,设置自定义控件的属性,并创建、添加子视图:</p> <pre> <code class="language-json">-(instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; _imageView.contentMode = UIViewContentModeScaleAspectFill; _imageView.layer.masksToBounds = YES; _imageView.layer.cornerRadius = frame.size.width/2; [self addSubview:_imageView]; } return self; }</code></pre> <p>如果需要对子视图重新布局,需要调用layoutSubViews方法:</p> <p>-(void)layoutSubviews {</p> <p>[super layoutSubviews];</p> <p>_imageView.frame = self.frame;</p> <p>_imageView.layer.cornerRadius = self.frame.size.width/2;</p> <p>}</p> <p>layoutSubviews是调整子视图布局的方法,官方文档如下</p> <p>You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want.</p> <p>意思是当你需要调整subview的大小的时候,重写layoutSubviews方法。</p> <p>layoutSubviews在以下情况下会被调用:</p> <p>1、init初始化不会触发layoutSubviews</p> <p>2、addSubview会触发layoutSubviews</p> <p>3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化</p> <p>4、滚动一个UIScrollView会触发layoutSubviews</p> <p>5、旋转Screen会触发父UIView上的layoutSubviews事件</p> <p>6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件</p> <p>这个自定义控件提供对外接口方法,为自定义的控件赋值</p> <pre> <code class="language-json">- (void)configeWithImage:(UIImage *)image { _imageView.image = image; }</code></pre> <p>最后,添加自定义控件到页面上</p> <pre> <code class="language-json"> _circleImageView = [[CircleImageView alloc] initWithFrame:CGRectMake(0, 80, 150, 150)]; [_circleImageView configeWithImage:[UIImage imageNamed:@"tree"]]; [self.view addSubview:_circleImageView];</code></pre> <p>运行效果</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/572790d7d34d5e4b33d2d99b006ea322.png"></p> <h3><strong>通过xib创建自定义控件</strong></h3> <p style="text-align: center;"><img src="https://simg.open-open.com/show/f05a1271efa972e502890b5fcdcbb1c7.png"></p> <p>首先创建一个自定义控件XibCircleImageView,继承自UIView</p> <p>创建xib文件,与XibCircleImageView类同名</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/cbcaba0791d0c286ab62d33e0703bf31.png"></p> <p>配置xib中imageView的属性,并将XibCircleImageView 类与对应的xib文件进行绑定</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/e3c550b49e2bb1f45af22551bfa4ae93.png"></p> <p>代码如下</p> <pre> <code class="language-json">- (void)awakeFromNib { [super awakeFromNib]; _imageView.layer.masksToBounds = YES; _imageView.layer.cornerRadius = self.frame.size.width/2; [self addSubview:_imageView]; } - (void)configeWithImage:(UIImage *)image { _imageView.image = image; } -(void)layoutSubviews { [super layoutSubviews]; _imageView.layer.cornerRadius = self.frame.size.width/2; }</code></pre> <p>在页面中调用方式有点不同,通过loadNibNamed方法创建xib对象</p> <p>//使用xib创建自定义控件</p> <pre> <code class="language-json">_xibCircleImageView = [[[NSBundle mainBundle] loadNibNamed:@"XibCircleImageView" owner:nil options:nil] lastObject]; _xibCircleImageView.frame = CGRectMake(0, 500, 100, 100); [_xibCircleImageView configeWithImage:image]; [self.view addSubview:_xibCircleImageView];</code></pre> <p>当使用xib创建自定义控件时,初始化不会调用initWithFrame:方法,只会调用initWithCoder:方法,初始化完毕后才调用awakeFromNib方法,注意要在awakeFromNib中初始化子控件。因为initWithCoder:方法表示对象是从文件解析来的,就会调用,而awakeFromNib方法是从xib或者storyboard加载完毕后才会调用。</p> <h3><strong>小结</strong></h3> <p>这两种创建自定义控件的方式各有优劣,纯代码方式比较灵活,维护和扩展都比较方便,但写起来比较麻烦。xib方式开发效率高,但不易扩展和维护,适合功能样式比较稳定的自定义控件。</p> <h3><strong>事件传递机制</strong></h3> <p>在自定义控件中,可能需要动态响应事件,如按钮太小,不易点击,需要扩大按钮的点击范围,接下来我们谈谈iOS的事件传递机制。</p> <h3><strong>事件响应链</strong></h3> <p>UIResponder类能够响应触摸、手势以及远程控制等事件。它是所有可响应事件的基类,其中包括很常见的UIView、UIViewController以及UIApplication。</p> <p>UIResponder的属性和方法如下图,其中nextResponder表示指向一个UIResponder对象。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/66175bce742db678068474da613bdb75.png"></p> <p>那么事件响应链与UIResponder有什么关系呢?应用内的视图按一定的结构组织起来,即树状层次结构,一个视图可以有多个子视图,而子视图只能有一个父视图。当一个视图被添加到父视图上时。每一个视图的nextResponder属性就指向它的父视图,这样,整个应用就通过nextResponder串成了一条链,即响应链。响应链是一个虚拟链,并不是真实存在的,它借助UIResponder的nextResponder串连起来。如下图</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/a9b2a753208a1b5538803b88a30fd90e.png"></p> <h3><strong>Hit-Test View</strong></h3> <p>有了事件响应链,接下来就是寻找具体响应对象了,我们称之为:Hit-Testing View,寻找这个View的过程称为Hit-Test。</p> <p>什么是Hit-Test?我们可以把它理解为一个探测器,通过这个探测器,我们可以找到并判断手指是否触摸在某个视图上。</p> <p>Hit-Test是如何工作的?Hit-Test采用递归方式从视图的根节点开始遍历,直到找到某个点击的视图。</p> <p>首先从UIWindow发送hitTest:withEvent:消息开始,判断该视图是否能响应触摸事件,如果不能响应返回nil,表示该视图不能响应触摸事件。然后再调用pointInside:withEvent:方法,该方法用于判断触摸事件点击的位置是否处理该视图范围内,如果pointInside:withEvent:返回no,那么hitTest:withEvent:也直接返回nil。</p> <p>如果pointInside:withEvent: 方法返回yes,那么该视图向所有子视图发送hitTest:withEvent:消息,所有子视图的调用顺序是从最顶层视图一直到最底层视图,即从subViews的数组的末尾向前遍历。直到有子视图返回非空对象或全部遍历完毕。若有子视图返回非空对象,则hitTest:withEvent:方法返回该对象,处理结束;若所有子视图都返回nil,则hitTest:withEvent:方法返回该视图自身。</p> <h3><strong>事件传递机制的应用</strong></h3> <p>举几个例子,说明一下事件传递机制在自定义控件中的应用。</p> <p>一、扩大view的点击区域。假设一个button的大小为20px 20px,太小难以点击。我们通过重写这个button子类的hitTest:withEvent:方法,判断点击处point是否在button周围20px以内,如果是则返回自身,实现扩大点击范围的功能,代码如下:</p> <pre> <code class="language-json">-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (!self.isUserInteractionEnabled || self.hidden || self.alpha<=0.01) { return nil; } CGRect touchRect = CGRectInset(self.bounds, -20, -20); if (CGRectContainsPoint(touchRect, point)) { for (UIView *subView in [self.subviews reverseObjectEnumerator]) { CGPoint convertedPoint = [subView convertPoint:point toView:self]; UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event]; if (hitTestView) { return hitTestView; } } return self; } return nil; }</code></pre> <p>二、穿透传递事件。</p> <p>假设有两个view,viewA和viewB,viewB完全覆盖viewA,我们希望点击viewB时能响应viewA的事件。我们重写这个viewA的hitTest:withEvent:方法,不继续遍历它的子视图,直接返回自身。代码如下:</p> <pre> <code class="language-json">-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (!self.isUserInteractionEnabled || self.hidden || self.alpha<=0.01) { return nil; } if ([self pointInside:point withEvent:event]) { NSLog(@"in view A"); return self; } return nil; }</code></pre> <h3><strong>回调机制</strong></h3> <p>在自定义控件开发中,需要向它的父类回传返回值。比如一个存放按钮的自定义控件,需要在上层接收按钮点击事件。我们可以使用多种方式回调消息,比如target action模式、代理、block、通知等。</p> <p>Target-Action</p> <p>Target-Action是一种设计模式,当事件触发时,它让一个对象向另一个对象发送消息。这个模式我们接触的比较多,如为按钮绑定点击事件,为view添加手势事件等。UIControl及其子类都支持这个机制。Target-Action 在消息的发送者和接收者之间建立了一个松散的关系。消息的接收者不知道发送者,甚至消息的发送者也不知道消息的接收者会是什么。</p> <p>基于 target-action 传递机制的一个局限是,发送的消息不能携带自定义的信息。iOS 中,可以选择性的把发送者和触发 action 的事件作为参数。除此之外就没有别的控制 action 消息内容的方法了。</p> <p>举个例子,我们使用Target-Action为控件添加一个单击手势。</p> <pre> <code class="language-json"> UITapGestureRecognizer *tapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(refresh)]; [_imageView addGestureRecognizer:tapGR]; - (void)refresh{ NSLog(@"Touch imageView"); }</code></pre> <h3><strong>代理</strong></h3> <p>代理是一种我们常用的回调方式,也是苹果推荐的方式,在系统框架UIKit中大量使用,如UITableView、UITextField。</p> <p>优点:</p> <ol> <li> <p>代理语法清晰,可读性高,易于维护 ;</p> </li> <li> <p>它减少了代码耦合性,使事件监听与事件处理分离;</p> </li> <li> <p>一个控制器可以实现多个代理,满足自定义开发需求,灵活性较高;</p> </li> </ol> <p>缺点:</p> <ol> <li> <p>实现代理的过程较繁琐;</p> </li> <li> <p>跨层传值时加大代码的耦合性,并且程序的层次结构也变得混乱;</p> </li> <li> <p>当多个对象同时传值时不易区分,导致代理易用性大大降低;</p> </li> </ol> <p>Block</p> <p>Block封装一段代码,并当做变量进行传递,它十分方便地将不同地方的代码组织在一起,可读性很高。</p> <p>优点:1,语法简洁,代码可读性和可维护性较高。2,配合GCD优秀的解决多纯程问题。</p> <p>缺点:1,Block中得代码将自动进行一次retain操作,容易造成内存泄露。 2.Block内默认引用为强引用,容易造成循环引用。</p> <p>通知</p> <p>代理是一对一的关系,通知是一对多的关系,通知相比代理可以实现更大跨度的通信机制。但接收对象多了,就难以控制,有时不希望的对象也接收处理了消息。</p> <p>优点:</p> <ol> <li> <p>使用简单,代码精简。</p> </li> <li> <p>支持一对多,解决了同时向多个对象监听的问题。</p> </li> <li> <p>传值方便快捷,Context自身携带相应的内容。</p> </li> </ol> <p>缺点:</p> <ol> <li> <p>通知使用完毕后需要注销,否则会造成意外崩溃。</p> </li> <li> <p>key不够安全,编译器不会检测到是否被通知中心正确处理。</p> </li> <li> <p>调试时难以跟踪。</p> </li> <li> <p>当使用者向通知中心发送通知的时候,并不能获得任何反馈信息。</p> </li> <li> <p>需要一个第三方的对象来做监听者与被监听者的中介。</p> </li> </ol> <p>至此,开发自定义控件的相关知识梳理了一遍,希望能帮助大家更好地理解自定义控件开发。</p> <p> </p> <p>来自:http://www.cocoachina.com/ios/20161103/17946.html</p> <p> </p>