加深理解UIView,UIResponder,UIController
转载出处:http://www.th7.cn/Program/IOS/201503/406514.shtml
原文地址==>自定义控件:http://objccn.io/issue-3-4/
读完这篇文章后 觉得自己对UIView UIResponder 和UIController的理解瞬间增加了一个层次,记下笔记,留给我这忘事精随时查看
视图层次概览
如果你观察一下 UIView 的子类,可以发现 3 个基类: reponders
(响应者),views
(视图)和 controls
(控件)。我们快速重温一下它们之间发生了什么。
UIResponder
UIResponder
是 UIView
的父类。responder
能够处理触摸、手势、远程控制等事件。之所以它是一个单独的类而没有合并到 UIView
中,是因为 UIResponder
有更多的子类,最明显的就是 UIApplication
和 UIViewController
。通过重写 UIResponder
的方法,可以决定一个类是否可以成为第一响应者 (first responder),即当前输入焦点元素。
当 touches (触摸) 或 motion (指一系列运动传感器) 等交互行为发生时,它们被发送给第一响应者 (通常是一个视图)。如果第一响应者没有处理,则该行为沿着响应链到达视图控制器,如果行为仍然没有被处理,则继续传递给应用。如果想监测晃动手势,可以根据需要在这3层中的任意位置处理。
UIResponder
还允许自定义输入方法,从 inputAccessoryView
向键盘添加辅助视图到使用 inputView
提供一个完全自定义的键盘。
UIView
UIView
子类处理所有跟内容绘制有关的事情以及触摸时间。只要写过 "Hello, World" 应用的人都知道视图,但我们重申一些技巧点:
一个普遍错误的概念:视图的区域是由它的 frame 定义的。实际上 frame 是一个派生属性,是由 center
和 bounds
合成而来。不使用 Auto Layout 时,大多数人使用 frame 来改变视图的位置和大小。小心些,官方文档特别详细说明了一个注意事项:
如果 transform 属性不是 identity transform 的话,那么这个属性的值是未定义的,因此应该将其忽略
另一个允许向视图添加交互的方法是使用手势识别。注意它们对 responders 并不起作用,而只对视图及其子类奏效。
UIControl
UIControl
建立在视图上,增加了更多的交互支持。最重要的是,它增加了 target / action 模式。看一下具体的子类,我们可以看一下按钮,日期选择器 (Date pickers),文本框等等。创建交互控件时,你通常想要子类化一个 UIControl
。一些常见的像 bar buttons (虽然也支持 target / action) 和 text view (这里需要你使用代理来获得通知) 的类其实并不是 UIControl
。
渲染
现在,我们转向可见部分:自定义渲染。正如 Daniel 在他的文章中提到的,你可能想避免在 CPU 上做渲染而将其丢给 GPU。这里有一条经验:尽量避免 drawRect:
,使用现有的视图构建自定义视图。
通常最快速的渲染方法是使用图片视图。例如,假设你想画一个带有边框的圆形头像,像下面图片中这样:
为了实现这个,我们用以下的代码创建了一个图片视图的子类:
// called from initializer
- (void)setupView
{
self.clipsToBounds = YES;
self.layer.cornerRadius = self.bounds.size.width / 2;
self.layer.borderWidth = 3;
self.layer.borderColor = [UIColor darkGrayColor].CGColor;
}
我鼓励各位读者深入了解 CALayer
及其属性,因为你用它能实现的大多数事情会比用 Core Graphics 自己画要快。然而一如既往,监测自己的代码的性能是十分重要的。
把可拉伸的图片和图片视图一起使用也可以极大的提高效率。在 Taming UIButton 这个帖子中,Reda Lemeden 探索了几种不同的绘图方法。在文章结尾处有一个很有价值的来自 UIKit 团队的工程师 Andy Matuschak 的回复,解释了可拉伸图片是这些技术中最快的。原因是可拉伸图片在 CPU 和 GPU 之间的数据转移量最小,并且这些图片的绘制是经过高度优化的。
处理图片时,你也可以让 GPU 为你工作来代替使用 Core Graphics。使用 Core Image,你不必用 CPU 做任何的工作就可以在图片上建立复杂的效果。你可以直接在 OpenGL 上下文上直接渲染,所有的工作都在 GPU 上完成。
自定义绘制
如果决定了采用自定义绘制,有几种不同的选项可供选择。如果可能的话,看看是否可以生成一张图片并在内存和磁盘上缓存起来。如果内容是动态的,也许你可以使用 Core Animation,如果还是行不通,使用 Core Graphics。如果你真的想要接近底层,使用 GLKit 和原生 OpenGL 也不是那么难,但是需要做很多工作。
如果你真的选择了重写 drawRect:
,确保检查内容模式。默认的模式是将内容缩放以填充视图的范围,这在当视图的 frame 改变时并不会重新绘制。
自定义交互
正如之前所说的,自定义控件的时候,你几乎一定会扩展一个 UIControl 的子类。在你的子类里,可以使用 target action 机制触发事件,如下面的例子:
[self sendActionsForControlEvents:UIControlEventValueChanged];
为了响应触摸,你可能更倾向于使用手势识别。然而如果想要更接近底层,仍然可以重写 touchesBegan
, touchesMoved
和 touchesEnded
方法来访问原始的触摸行为。但虽说如此,创建一个手势识别的子类来把手势处理相关的逻辑从你的视图或者视图控制器中分离出来,在很多情况下都是一种更合适的方式。
创建自定义控件时所面对的一个普遍的设计问题是向拥有它们的类中回传返回值。比如,假设你创建了一个绘制交互饼状图的自定义控件,想知道用户何时选择了其中一个部分。你可以用很多种不同的方法来解决这个问题,比如通过 target action 模式,代理,block 或者 KVO,甚至通知。
使用 Target-Action
经典学院派的,通常也是最方便的做法是使用 target-action。在用户选择后你可以在自定义的视图中做类似这样的事情:
[self sendActionsForControlEvents:UIControlEventValueChanged];
如果有一个视图控制器在管理这个视图,需要这样做:
- (void)setupPieChart
{
[self.pieChart addTarget:self
action:@selector(updateSelection:)
forControlEvents:UIControlEventValueChanged];
}
- (void)updateSelection:(id)sender
{
NSLog(@"%@", self.pieChart.selectedSector);
}
这么做的好处是在自定义视图子类中需要做的事情很少,并且自动获得多目标支持。
使用代理
如果你需要更多的控制从视图发送到视图控制器的消息,通常使用代理模式。在我们的饼状图中,代码看起来大概是这样:
[self.delegate pieChart:self didSelectSector:self.selectedSector];
在视图控制器中,你要写如下代码:
@interface MyViewController <PieChartDelegate>
...
- (void)setupPieChart
{
self.pieChart.delegate = self;
}
- (void)pieChart:(PieChart*)pieChart didSelectSector:(PieChartSector*)sector
{
// 处理区块
}
当你想要做更多复杂的工作而不仅仅是通知所有者值发生了变化时,这么做显然更合适。不过虽然大多数开发人员可以非常快速的实现自定义代理,但这种方式仍然有一些缺点:你必须检查代理是否实现了你想要调用的方法 (使用 respondsToSelector:
),最重要的,通常你只有一个代理 (或者需要创建一个代理数组)。也就是说,一旦视图所有者和视图之间的通信变得稍微复杂,我们几乎总是会采取这种模式。
使用 Block
另一个选择是使用 block。再一次用饼状图举例,代码看起来大概是这样:
@interface PieChart : UIControl
@property (nonatomic,copy) void(^selectionHandler)(PieChartSection* selectedSection);
@end
在选取行为的代码中,你只需要执行它。在此之前检查一下block是否被赋值非常重要,因为执行一个未被赋值的 block 会使程序崩溃。
if (self.selectionHandler != NULL) {
self.selectionHandler(self.selectedSection);
}
这种方法的好处是可以把相关的代码整合在视图控制器中:
- (void)setupPieChart
{
self.pieChart.selectionHandler = ^(PieChartSection* section) {
// 处理区块
}
}
就像代理,每个动作通常只有一个 block。另一个重要的限制是不要形成引用循环。如果你的视图控制器持有饼状图的强引用,饼状图持有 block,block 又持有视图控制器,就形成了一个引用循环。只要在 block 中引用 self 就会造成这个错误。所以通常代码会写成这个样子:
__weak id weakSelf = self;
self.pieChart.selectionHandler = ^(PieChartSection* section) {
MyViewController* strongSelf = weakSelf;
[strongSelf handleSectionChange:section];
}
一旦 block 中的代码要失去控制 (比如 block 中要处理的事情太多,导致 block 中的代码过多),你还应该将它们抽离成独立的方法,这种情况的话可能用代理会更好一些。
使用 KVO
如果喜欢 KVO,你也可以用它来观察。这有一点神奇而且没那么直接,但当应用中已经使用,它是很好的解耦设计模式。在饼状图类中,编写代码:
self.selectedSegment = theNewSelectedSegment;
当使用合成属性,KVO 会拦截到该变化并发出通知。在视图控制器中,编写类似的代码:
- (void)setupPieChart
{
[self.pieChart addObserver:self forKeyPath:@"selectedSegment" options:0 context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if(object == self.pieChart && [keyPath isEqualToString:@"selectedSegment"]) {
// 处理改变
}
}
根据你的需要,在 viewWillDisappear:
或 dealloc
中,还需要移除观察者。对同一个对象设置多个观察者很容易造成混乱。有一些技术可以解决这个问题,比如 ReactiveCocoa 或者更轻量级的 THObserversAndBinders
。
使用通知
作为最后一个选择,如果你想要一个非常松散的耦合,可以使用通知来使其他对象得知变化。对于饼状图来说你几乎肯定不想这样,不过为了讲解的完整,这里介绍如何去做。在饼状图的的头文件中:
extern NSString* const SelectedSegmentChangedNotification;
在实现文件中:
NSString* const SelectedSegmentChangedNotification = @"selectedSegmentChangedNotification";
...
- (void)notifyAboutChanges
{
[[NSNotificationCenter defaultCenter] postNotificationName:SelectedSegmentChangedNotification object:self];
}
现在订阅通知,在视图控制器中:
- (void)setupPieChart
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(segmentChanged:)
name:SelectedSegmentChangedNotification
object:self.pieChart];
}
...
- (void)segmentChanged:(NSNotification*)note
{
}
当添加了观察者,你可以不将饼状图作为参数 object
,而是传递 nil
,以接收所有饼状图对象发出的通知。就像 KVO 通知,你也需要在恰当的地方退订这些通知。
这项技术的好处是完全的解耦。另一方面,你失去了类型安全,因为在回调中你得到的是一个通知对象,而不像代理,编译器无法检查通知发送者和接受者之间的类型是否匹配。
辅助功能 (Accessibility)
苹果官方提供的标准 iOS 控件均有辅助功能。这也是推荐用标准控件创建自定义控件的另一个原因。
这或许可以作为一整期的主题,但是如果你想编写自定义视图,Accessibility Programming Guide 说明了如何创建辅助控制器。最为值得注意的是,如果有一个视图中有多个需要辅助功能的元素,但它们并不是该视图的子视图,你可以让视图实现 UIAccessibilityContainer
协议。对于每一个元素,返回一个描述它的 UIAccessibilityElement
对象。
本地化
创建自定义视图时,本地化也同样重要。像辅助功能一样,这个可以作为一整期的话题。本地化自定义视图的最直接工作就是字符串内容。如果使用 NSString
,你不必担心编码问题。如果在自定义视图中展示日期或数字,使用日期和数字格式化类来展示它们。使用 NSLocalizedString
本地化字符串。
另一个本地化过程中很有用的工具是 Auto Layout。例如,有在英文中很短的词在德语中可能会很长。如果根据英文单词的长度对视图的尺寸做硬编码,那么当翻译成德文的时候几乎一定会遇上麻烦。通过使用 Auto Layout,让标签控件自动调整为内容的尺寸,并向依赖元素添加一些其他的限制以确保重新设置尺寸,使这项工作变得非常简单。苹果为此提供了一个很好的 介绍。另外,对于类似希伯来语这种顺序从右到左的语言,如果你使用了 leading 和 trailing 属性,整个视图会自动按照从右到左的顺序展示,而不是硬编码的从左至右。
测试
最后,让我们考虑测试视图的问题。对于单元测试,你可以使用 Xcode 自带的工具或者其它第三方框架。另外,可以使用 UIAutomation 或者其它基于它的工具。为此,你的视图完全支持辅助功能是必要的。UIAutomation 并未充分得到利用的一个功能是截图;你可以用它自动对比视图和设计以确保两者每一个像素都分毫不差。(插一个无关的小提示:你还可以使用它来为应用上架 App Store 自动生成截图,这在你有多个多国语言的应用时会特别有用)。
加深理解UIView,UIResponder,UIController的更多相关文章
- 理解UIView的绘制-孙亚洲
前言 最近研究OpenGL ES相关和 GPU 相关 发现这篇文章很具有参考的入门价值. 理解 UIView 的绘制, UIView 是如何显示到 Screen 上的? 首先要从Runloop开始说, ...
- 一篇RPO漏洞挖掘文章翻译加深理解。
这是我第一次尝试翻译一篇漏洞挖掘文章,翻译它也是为了加深理解它.这是一篇很有意思的漏洞挖掘文章. 前几天在看fd的博客,偶然看到了这篇文章,虽然有点老了.但是思路真的牛皮.我决定花费时间和精力研究它们 ...
- 通过自己实现接口来加深理解SpringMVC的执行流程
功能介绍 上篇文章[从源码角度了解SpringMVC的执行流程]通过接口源码向大家介绍了SpringMVC的执行流程,主要偏重于源码.这篇文件我们来自己实现那几个关键接口,来真实体验下SpringMV ...
- 加深理解Java异常概念并熟记5个最常见的运行时异常
加深理解Java异常概念并熟记5个最常见的运行时异常 说明Error与Exception的联系和区别有哪些? 列举最常见的5个运用时异常. 1.Error和Exception的联系和区别: Error ...
- 理解UIView的绘制
界面的绘制和渲染 UIView是如何到显示的屏幕上的. 这件事要从RunLoop开始,RunLoop是一个60fps的回调,也就是说每16.7ms绘制一次屏幕,也就是我们需要在这个时间内完成view的 ...
- [ZigBee] 13、ZigBee基础阶段性回顾与加深理解——用定时器1产生PWM来控制LED亮度(七色灯)
引言:PWM对于很多软件工程师可能又熟悉又陌生,以PWM调节LED亮度为例,其本质是在每个周期都偷工减料一些,整体表现出LED欠压亮度不同的效果.像大家看到的七色彩灯其原理也类似,只是用3路PWM分别 ...
- Sql Server之旅——第六站 使用winHex利器加深理解数据页
这篇我来介绍一个winhex利器,这个工具网上有介绍,用途大着呢,可以用来玩数据修复,恢复删除文件等等....它能够将一个file解析成 hex形式,这样你就可以对hex进行修改,然后你就可以看到修复 ...
- typedef (还需经常看看加深理解)
看了 c++primer 1,typedef名字 typedef定义以关键字typedef开始,后面是 数据类型+标示符. 并未引入新的类型,只是现有数据类型的同义词 例: typedef doubl ...
- java是通过值传递,也就是通过拷贝传递——通过方法操作不同类型的变量加深理解(勿删)
head first java里写到“java是通过值传递的,也就是通过拷贝传递”,由此得出结论,方法无法改变调用方传入的参数.该怎么理解呢? 看例子: public class Test1 { pu ...
随机推荐
- 如何对n个数进行排序,要求时间复杂度O(n),空间复杂度O(1)
看上去似乎任何已知的算法都无法做到,如果谁做到了,那么所有的排序方法:QuickSort,ShellSort,HeapSort,BubbleSort等等等等,都可以扔掉了,还要这些算法干吗阿,呵呵.不 ...
- ContextMenuStrip 添加在窗体。点击右键不能显示问题解答
ContextMenuStrip控件,系统默认为点击菜单,使用时.需要与你的窗体绑定.如图所示,,form窗体上包括一个contextMenustrip信息.默认状态是无.所以在使用contextMe ...
- oracle 命令创建用户 、授权、数据库导入、导出
最近在使用oracle,经常要导入导出数据,命令很简单,却经常忘记,所以记下来.. drop user yfplss cascade;--登录system用户删除已存在的用户名,该用户下的所有东西都被 ...
- easyui的验证
加一个JS来重写验证,文件名为:validator.js 内容为: //扩展easyui表单的验证 $.extend($.fn.validatebox.defaults.rules, { //验证汉子 ...
- 配置Nutch模拟浏览器以绕过反爬虫限制
原文链接:http://yangshangchuan.iteye.com/blog/2030741 当我们配置Nutch抓取 http://yangshangchuan.iteye.com 的时候,抓 ...
- ORACLE EBS R12 FOR LINUX 开机后如何启动数据库、应用脚本[Z]
在Linux中安裝EBS R12後, EBS關閉與啟動的程序為: 1. 關閉EBS - 先關閉Applications Server $ cd /d01/oracle/VIS/inst/apps/V ...
- windows下的mongodb下载安装
1.首先从官网https://www.mongodb.org/下载mongodb的安装包,本人下载的是mongoDB3.2版本的.msi的文件,然后双击安装即可 2.点击next 3.compelte ...
- linux创建用户和组
linux下创建用户(一) Linux 系统是一个多用户多任务的分时操作系统,任何一个要使用系统资源的用户,都必须首先向系统管理员申请一个账号,然后以这个账号的身份进入系统.用户的账号一方面可以帮助系 ...
- 6.828 lab1 bootload
MIT6.828 lab1地址:http://pdos.csail.mit.edu/6.828/2014/labs/lab1/ 第一个练习,主要是让我们熟悉汇编,嗯,没什么好说的. Part 1: P ...
- javaScript的2种变量范围有什么不同
1.javascript怎样选中一个checkbox,怎样设置它无效? document.all.cb1[0].disabled = true; 2.js中的3种弹出式消息提醒(警告窗口,确认窗口 ...