iOS自定义控件开发梳理总结

在日常iOS开发中,系统提供的控件常常无法满足业务功能,这个时候需要我们实现一些自定义控件。自定义控件能让我们完全控制视图的展示内容以及交互操作。本篇将介绍一些自定义控件的相关概念,探讨自定义控件开发的基本过程及技巧。

UIView

在开始之前我们先介绍一个类UIVew,它在iOS APP中占有绝对重要的地位,因为几乎所有的控件都是继承自UIView类。
UIView表示屏幕上的一个矩形区域,负责渲染区域内的内容,并且响应区域内发生的触摸事件。

在UIView的内部有一个CALayer,提供内容的绘制和显示,包括UIView的尺寸样式。UIView的frame实际上返回的CALayer的frame。

UIView继承自UIResponder类,它能接收并处理从系统传来的事件,CALayer继承自NSObject,它无法响应事件。所以UIView与CALayer的最大区别在于:UIView能响应事件,而CALayer不能。
更详细的资料:https://developer.apple.com/reference/uikit/uiview

两种实现方式
在创建自定义控件时,主要有两种实现方式,分别是纯代码以及xib。接下来我们用这两种方式分别演示一下创建自定义控件的步骤。
我们实现一个简单的demo ,效果如下,封装一个圆形的imageView。

使用代码创建自定义控件
使用代码创建自定义控件,首先创建一个继承自UIView的类

实现initWithFrame:方法。在该方法中,设置自定义控件的属性,并创建、添加子视图:

-(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;
}

如果需要对子视图重新布局,需要调用layoutSubViews方法:

-(void)layoutSubviews {
 [super layoutSubviews];
 _imageView.frame = self.frame;
 _imageView.layer.cornerRadius = self.frame.size.width/2;

}

layoutSubviews是调整子视图布局的方法,官方文档如下:

You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want.

意思是当你需要调整subview的大小的时候,重写layoutSubviews方法。

layoutSubviews在以下情况下会被调用:

  • init初始化不会触发layoutSubviews
  • addSubview会触发layoutSubviews
  • 设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化
  • 滚动一个UIScrollView会触发layoutSubviews
  • 旋转Screen会触发父UIView上的layoutSubviews事件
  • 改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件

这个自定义控件提供对外接口方法,为自定义的控件赋值

- (void)configeWithImage:(UIImage *)image {
 _imageView.image = image;
}

最后,添加自定义控件到页面上

 _circleImageView = [[CircleImageView alloc] initWithFrame:CGRectMake(0, 80, 150, 150)];
 [_circleImageView configeWithImage:[UIImage imageNamed:@"tree"]];
 [self.view addSubview:_circleImageView];

通过xib创建自定义控件
首先创建一个自定义控件XibCircleImageView,继承自UIView
创建xib文件,与XibCircleImageView类同名
配置xib中imageView的属性,并将XibCircleImageView 类与对应的xib文件进行绑定
代码如下

- (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;

}

在页面中调用方式有点不同,通过loadNibNamed方法创建xib对象

 //使用xib创建自定义控件
 _xibCircleImageView = [[[NSBundle mainBundle] loadNibNamed:@"XibCircleImageView" owner:nil options:nil] lastObject];
 _xibCircleImageView.frame = CGRectMake(0, 500, 100, 100);
 [_xibCircleImageView configeWithImage:image];
 [self.view addSubview:_xibCircleImageView];

当使用xib创建自定义控件时,初始化不会调用initWithFrame:方法,只会调用initWithCoder:方法,初始化完毕后才调用awakeFromNib方法,注意要在awakeFromNib中初始化子控件。因为initWithCoder:方法表示对象是从文件解析来的,就会调用,而awakeFromNib方法是从xib或者storyboard加载完毕后才会调用。

小结

这两种创建自定义控件的方式各有优劣,纯代码方式比较灵活,维护和扩展都比较方便,但写起来比较麻烦。xib方式开发效率高,但不易扩展和维护,适合功能样式比较稳定的自定义控件。

事件传递机制

在自定义控件中,可能需要动态响应事件,如按钮太小,不易点击,需要扩大按钮的点击范围,接下来我们谈谈iOS的事件传递机制。

事件响应链

UIResponder类能够响应触摸、手势以及远程控制等事件。它是所有可响应事件的基类,其中包括很常见的UIView、UIViewController以及UIApplication。

UIResponder的属性和方法如下图,其中nextResponder表示指向一个UIResponder对象。

那么事件响应链与UIResponder有什么关系呢?应用内的视图按一定的结构组织起来,即树状层次结构,一个视图可以有多个子视图,而子视图只能有一个父视图。当一个视图被添加到父视图上时。每一个视图的nextResponder属性就指向它的父视图,这样,整个应用就通过nextResponder串成了一条链,即响应链。响应链是一个虚拟链,并不是真实存在的,它借助UIResponder的nextResponder串连起来。如下图

Hit-Test View

有了事件响应链,接下来就是寻找具体响应对象了,我们称之为:Hit-Testing View,寻找这个View的过程称为Hit-Test。
什么是Hit-Test?我们可以把它理解为一个探测器,通过这个探测器,我们可以找到并判断手指是否触摸在某个视图上。
Hit-Test是如何工作的?Hit-Test采用递归方式从视图的根节点开始遍历,直到找到某个点击的视图。

首先从UIWindow发送hitTest:withEvent:消息开始,判断该视图是否能响应触摸事件,如果不能响应返回nil,表示该视图不能响应触摸事件。然后再调用pointInside:withEvent:方法,该方法用于判断触摸事件点击的位置是否处理该视图范围内,如果pointInside:withEvent:返回no,那么hitTest:withEvent:也直接返回nil。

如果pointInside:withEvent: 方法返回yes,那么该视图向所有子视图发送hitTest:withEvent:消息,所有子视图的调用顺序是从最顶层视图一直到最底层视图,即从subViews的数组的末尾向前遍历。直到有子视图返回非空对象或全部遍历完毕。若有子视图返回非空对象,则hitTest:withEvent:方法返回该对象,处理结束;若所有子视图都返回nil,则hitTest:withEvent:方法返回该视图自身。

事件传递机制的应用

举几个例子,说明一下事件传递机制在自定义控件中的应用。

一、扩大view的点击区域。假设一个button的大小为20px 20px,太小难以点击。我们通过重写这个button子类的hitTest:withEvent:方法,判断点击处point是否在button周围20px以内,如果是则返回自身,实现扩大点击范围的功能,代码如下:

-(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;
}

二、穿透传递事件。

假设有两个view,viewA和viewB,viewB完全覆盖viewA,我们希望点击viewB时能响应viewA的事件。我们重写这个viewA的hitTest:withEvent:方法,不继续遍历它的子视图,直接返回自身。代码如下:

-(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;
}

回调机制

在自定义控件开发中,需要向它的父类回传返回值。比如一个存放按钮的自定义控件,需要在上层接收按钮点击事件。我们可以使用多种方式回调消息,比如target action模式、代理、block、通知等。

Target-Action

Target-Action是一种设计模式,当事件触发时,它让一个对象向另一个对象发送消息。这个模式我们接触的比较多,如为按钮绑定点击事件,为view添加手势事件等。UIControl及其子类都支持这个机制。Target-Action 在消息的发送者和接收者之间建立了一个松散的关系。消息的接收者不知道发送者,甚至消息的发送者也不知道消息的接收者会是什么。

基于 target-action 传递机制的一个局限是,发送的消息不能携带自定义的信息。iOS 中,可以选择性的把发送者和触发 action 的事件作为参数。除此之外就没有别的控制 action 消息内容的方法了。

举个例子,我们使用Target-Action为控件添加一个单击手势。

  UITapGestureRecognizer *tapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(refresh)];
  [_imageView addGestureRecognizer:tapGR];

- (void)refresh{
 NSLog(@"Touch imageView");
}

代理

代理是一种我们常用的回调方式,也是苹果推荐的方式,在系统框架UIKit中大量使用,如UITableView、UITextField。

优点:1,代理语法清晰,可读性高,易于维护 ;2,它减少了代码耦合性,使事件监听与事件处理分离;3,一个控制器可以实现多个代理,满足自定义开发需求,灵活性较高;

缺点:1,实现代理的过程较繁琐;2,跨层传值时加大代码的耦合性,并且程序的层次结构也变得混乱;3,当多个对象同时传值时不易区分,导致代理易用性大大降低;

Block

Block封装一段代码,并当做变量进行传递,它十分方便地将不同地方的代码组织在一起,可读性很高。

优点:1,语法简洁,代码可读性和可维护性较高。2,配合GCD优秀的解决多线程问题。

缺点:1,Block中得代码将自动进行一次retain操作,容易造成内存泄露。 2.Block内默认引用为强引用,容易造成循环引用。

通知

代理是一对一的关系,通知是一对多的关系,通知相比代理可以实现更大跨度的通信机制。但接收对象多了,就难以控制,有时不希望的对象也接收处理了消息。

优点:1,使用简单,代码精简。2,支持一对多,解决了同时向多个对象监听的问题。3,传值方便快捷,Context自身携带相应的内容。

缺点:1,通知使用完毕后需要注销,否则会造成意外崩溃。2,key不够安全,编译器不会检测到是否被通知中心正确处理。3,调试时难以跟踪。 4,当使用者向通知中心发送通知的时候,并不能获得任何反馈信息。 5.需要一个第三方的对象来做监听者与被监听者的中介。

总结

至此,开发自定义控件的相关知识梳理了一遍,希望能帮助大家更好地理解自定义控件开发。

(0)

相关推荐

  • 详解iOS中UIView的layoutSubviews子视图布局方法使用

    概念 在UIView里面有一个方法layoutSubviews: 复制代码 代码如下: - (void)layoutSubviews;    // override point. called by layoutIfNeeded automatically. As of iOS 6.0, when constraints-based layout is used the base implementation applies the constraints-based layout, other

  • iOS为UIView设置阴影效果

    UIView的阴影设置主要通过UIView的layer的相关属性来设置 阴影的颜色 imgView.layer.shadowColor = [UIColor blackColor].CGColor; 阴影的透明度 imgView.layer.shadowOpacity = 0.8f; 阴影的圆角 imgView.layer.shadowRadius = 4.f; 阴影偏移量 imgView.layer.shadowOffset = CGSizeMake(4,4); imgView.layer.s

  • iOS应用开发中UIView添加边框颜色及设置圆角边框的方法

    UIView加边框及边框颜色 引用库: 复制代码 代码如下: #import <QuartzCore/QuartzCore.h> 使用: 复制代码 代码如下: //添加边框和提示         CGRect frameRect = CGRectMake(20, 90, self.window.frame.size.width-40, self.window.frame.size.height-180);         UIView   *frameView = [[UIView alloc

  • iOS App开发中UIViewController类的使用教程

    一.引言 作为MVC设计模式中的C,Controller一直扮演着项目开发中最重要的角色,它是视图和数据的桥梁,通过它的管理,将数据有条有理的展示在我们的View层上.iOS中的UIViewController是UIKit框架中最基本的一个类.从第一个UI视图到复杂完整项目,都离不开UIViewController作为基础.基于UIViewController的封装和扩展,也能够出色的完成各种复杂界面逻辑.这里旨在讨论UIViewController的生命周期和属性方法,在最基础的东西上,往往会

  • IOS代码笔记UIView的placeholder的效果

    本文实例为大家分享了IOS占位符效果,供大家参考,具体内容如下 一.效果图 二.工程图 三.代码 RootViewController.h #import <UIKit/UIKit.h> @interface RootViewController : UIViewController <UITextViewDelegate> { UITextView *psTextView; UILabel *pslabel; } @end RootViewController.m #import

  • 详解iOS App开发中UIViewController的loadView方法使用

    当你访问一个ViewController的view属性时,如果此时view的值是nil,那么,ViewController就会自动调用loadView这个方法.这个方法就会加载或者创建一个view对象,赋值给view属性. loadView默认做的事情是:如果此ViewController存在一个对应的nib文件,那么就加载这个nib.否则,就创建一个UIView对象. 如果你用Interface Builder来创建界面,那么不应该重载这个方法. 控制器的loadView方法以及view属性

  • 深入讲解iOS开发中的UIViewController

    UIViewController顾名思义:视图控制器.应该在MVC设计模式中扮演控制层的角色.一些初学者在最开始的时候一直不理解为何有了UIView还要UIViewController做什么用,不都是向视图中增加view.在此我要声明一下 UIViewController和Uiview是两个不同的类UIViewController是视图控制器 而UIView是视图也就是说,UIViewController是控制UIView的.你也可以认为UIViewController就是一个相框,而UIVie

  • iOS自定义控件开发梳理总结

    在日常iOS开发中,系统提供的控件常常无法满足业务功能,这个时候需要我们实现一些自定义控件.自定义控件能让我们完全控制视图的展示内容以及交互操作.本篇将介绍一些自定义控件的相关概念,探讨自定义控件开发的基本过程及技巧. UIView 在开始之前我们先介绍一个类UIVew,它在iOS APP中占有绝对重要的地位,因为几乎所有的控件都是继承自UIView类. UIView表示屏幕上的一个矩形区域,负责渲染区域内的内容,并且响应区域内发生的触摸事件. 在UIView的内部有一个CALayer,提供内容

  • IOS多线程开发之线程的状态

    大家都知道,在开发过程中应该尽可能减少用户等待时间,让程序尽可能快的完成运算.可是无论是哪种语言开发的程序最终往往转换成汇编语言进而解释成机器码来执行.但是机器码是按顺序执行的,一个复杂的多步操作只能一步步按顺序逐个执行.改变这种状况可以从两个角度出发:对于单核处理器,可以将多个步骤放到不同的线程,这样一来用户完成UI操作后其他后续任务在其他线程中,当CPU空闲时会继续执行,而此时对于用户而言可以继续进行其他操作:对于多核处理器,如果用户在UI线程中完成某个操作之后,其他后续操作在别的线程中继续

  • iOS Swift开发之日历插件开发示例

    本文介绍了iOS Swift开发之日历插件开发示例,分享给大家,具体如下: 效果图 0x01 如何获取目前日期 关于日期,苹果给出了 Date 类,初始化一个 Date 类 let date = Date() 打印出来就是当前系统的日期和时间 那么如何单独获得当前年份,月份呢? var date: [Int] = [] let calendar: Calendar = Calendar(identifier: .gregorian) var comps: DateComponents = Dat

  • iOS程序开发之使用PlaceholderImageView实现优雅的图片加载效果

    说明 1. PlaceHolderImageView基于SDWebImage编写 2. 给定一个图片的urlString,以及一个placeholderImage就可以优雅的显示图片加载效果 效果 源码 PlaceholderImageView.h/.m // // PlaceholderImageView.h // SDWebImageViewPlaceHorder // // Created by YouXianMing on 16/9/14. // Copyright © 2016年 Yo

  • IOS游戏开发之五子棋OC版

    先上效果图 - 功能展示 - 初高级棋盘切换效果 实现思路及主要代码详解 1.绘制棋盘 利用Quartz2D绘制棋盘.代码如下 - (void)drawBackground:(CGSize)size{ self.gridWidth = (size.width - 2 * kBoardSpace) / self.gridCount; //1.开启图像上下文 UIGraphicsBeginImageContext(size); //2.获取上下文 CGContextRef ctx = UIGraph

  • IOS程序开发之跳转短信发送界面实现发送短信功能

    项目需求:在程序开发中,我们需要在某个程序里面发送一些短信验证(不是接收短信验证,关于短信验证,传送门:http://www.cnblogs.com/wolfhous/p/5096774.html 项目实现: 新建demo,直接看我源码标志. 源码截图 真机截图 就是如此简单,如您有任何问题/建议或者更好的实现方法,联系本人. 可以看我折叠的源码 /** 点击发送短信按钮*/ - (IBAction)sendMessageBut:(id)sender { /** 如果可以发送文本消息(不在模拟器

  • iOS逆向开发之微信自动添加好友功能

    这一次,小程演示怎么让一个APP自动地运行,从而代替手工的操作.同样以"微信"以例,实现在一个微信群里面,对所有的成员,自动地一个一个地发出添加好友的请求. 知识点还是之前介绍的东西,流程方面还是跟踪与最终注入.因为这是一个系列的文章讲解(微信公众号"广州小程" -> 逆向开发),所以读者可以联系前面的文章来理解,用自己的话"翻译"成自己的知识与经验. 本文解决一个问题:如何让第三方程序自动化地运行. (一)批量添加好友的效果 小程使用&q

  • iOS蓝牙开发数据实时传输

    随着iOS项目开发  很多app需要通过蓝牙与设备连接 蓝牙开发注意: 先定义中心设备和外围设备以及遵守蓝牙协议 @interface ViewController()<CBCentralManagerDelegate,CBPeripheralDelegate> @property (strong, nonatomic) CBCentralManager *manager; @property (nonatomic, strong) CBPeripheral *peripheral; @pro

  • iOS如何开发简单的手绘应用实例详解

    开发一款简单的 iOS 手绘应用, 收集点,绘制形状,给形状着色,呈现给用户,好像就完了 框架是 Quartz2D 1, 收集点 首先需要有一个界面 UIView, 用这个界面监听用户的手势,收集点 用户按下手指 location(in, 从触摸事件中,获得在画板中的坐标 var lastPoint = CGPoint.zero override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { guard l

  • iOS应用开发中UITabBarController标签栏控制器使用进阶

    做了这么长时间的ios开发了,最基本的UITabBarController和UINavigationController都用了好长时间了,总是改现成的代码,或者各种自定义控件的修改,用的都有些混乱了,呵呵.还是自己做个demo再复习一下吧,记录下来以备后续翻查. 一.UITabBarController和UINavigationController的联合使用 这种方法最常见,好像一般有tabbar都会有naviBar.一般使用, 1. 在appDelegate里面创建UITabBarContro

随机推荐