加深理解UIView,UIResponder,UIController

读完这篇文章后 以为本身对UIView UIResponder 和UIController的理解瞬间增长了一个层次,记下笔记,留给我这忘事精随时查看html

视图层次概览

若是你观察一下 UIView 的子类,能够发现 3 个基类: reponders (响应者),views (视图)和 controls (控件)。咱们快速重温一下它们之间发生了什么。ios

UIResponder

UIResponder 是 UIView 的父类。responder 可以处理触摸、手势、远程控制等事件。之因此它是一个单独的类而没有合并到 UIView 中,是由于 UIResponder 有更多的子类,最明显的就是 UIApplication 和 UIViewController。经过重写 UIResponder的方法,能够决定一个类是否能够成为第一响应者 (first responder),例如当前输入焦点元素。git

当 touches (触摸) 或 motion (指一系列运动传感器) 等交互行为发生时,它们被发送给第一响应者 (一般是一个视图)。若是第一响应者没有处理,则该行为沿着响应链到达视图控制器,若是行为仍然没有被处理,则继续传递给应用。若是想监测晃动手势,能够根据须要在这3层中的任意位置处理。github

UIResponder 还容许自定义输入方法,从 inputAccessoryView 向键盘添加辅助视图到使用 inputView 提供一个彻底自定义的键盘。设计模式

UIView

UIView 子类处理全部跟内容绘制有关的事情以及触摸时间。只要写过 "Hello, World" 应用的人都知道视图,但咱们重申一些技巧点:api

一个广泛错误的概念:视图的区域是由它的 frame 定义的。实际上 frame 是一个派生属性,是由 center 和 bounds 合成而来。不使用 Auto Layout 时,大多数人使用 frame 来改变视图的位置和大小。当心些,官方文档特别详细说明了一个注意事项:数组

若是 transform 属性不是 identity transform 的话,那么这个属性的值是未定义的,所以应该将其忽略缓存

另外一个容许向视图添加交互的方法是使用手势识别。注意它们对 responders 并不起做用,而只对视图及其子类奏效。安全

UIControl

UIControl 创建在视图上,增长了更多的交互支持。最重要的是,它增长了 target / action 模式。看一下具体的子类,咱们能够看一下按钮,日期选择器 (Date pickers),文本框等等。建立交互控件时,你一般想要子类化一个 UIControl。一些常见的像 bar buttons (虽然也支持 target / action) 和 text view (这里须要你使用代理来得到通知) 的类其实并非 UIControlruby

渲染

如今,咱们转向可见部分:自定义渲染。正如 Daniel 在他的文章中提到的,你可能想避免在 CPU 上作渲染而将其丢给 GPU。这里有一条经验:尽可能避免 drawRect:,使用现有的视图构建自定义视图。

一般最快速的渲染方法是使用图片视图。例如,假设你想画一个带有边框的圆形头像,像下面图片中这样:

Rounded image view

为了实现这个,咱们用如下的代码建立了一个图片视图的子类:

// 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 自动生成截图,这在你有多个多国语言的应用时会特别有用)。

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