加深理解UIView,UIResponder,UIController

Posted mfmdaoyou

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了加深理解UIView,UIResponder,UIController相关的知识,希望对你有一定的参考价值。

读完这篇文章后 认为自己对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 模式。看一下详细的子类,我们能够看一下button。日期选择器 (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 团队的project师 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 中。还须要移除观察者。对同一个对象设置多个观察者非常easy造成混乱。

有一些技术能够解决问题。比方 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,让标签控件自己主动调整为内容的尺寸,并向依赖元素加入一些其它的限制以确保又一次设置尺寸。使这项工作变得非常easy。

苹果为此提供了一个非常好的 介绍。另外。对于类似希伯来语这样的顺序从右到左的语言。假设你使用了 leading 和 trailing 属性。整个视图会自己主动依照从右到左的顺序展示,而不是硬编码的从左至右。

測试

最后。让我们考虑測试视图的问题。对于单元測试,你能够使用 Xcode 自带的工具或者其他第三方框架。另外。能够使用 UIAutomation 或者其他基于它的工具。为此,你的视图全然支持辅助功能是必要的。UIAutomation 并未充分得到利用的一个功能是截图;你能够用它自己主动对照视图和设计以确保两者每个像素都分毫不差。

(插一个无关的小提示:你还能够使用它来为应用上架 App Store 自己主动生成截图。这在你有多个多国语言的应用时会特别实用)。

原文查看地址:http://objccn.io/issue-3-4/

以上是关于加深理解UIView,UIResponder,UIController的主要内容,如果未能解决你的问题,请参考以下文章

UIResponder笔记

常见UI继承关系

UI控件

需要 UIResponder 链说明

iOS控件之UIResponder类

UIResponder相关