《View & Window》

View & Window 架构

架构基础知识

我们在视觉上做的大多数事情都是通过 View 对象 – UIView 类的实例完成的。View 对象在屏幕上定义了一个矩形区域,并处理该区域的绘图和触摸事件。View 也可以作为其他 View 的父对象,并协调这些View的位置和大小。

UIKit 中的每个 View 都有一个 layer 属性(通常是CALayer类的一个实例)来支持,它管理View的支持存储,并处理与View相关的动画。我们执行的大多数操作应该是通过UIView接口。然而,有的时候我们需要对View的渲染或动画行为进行更多控制,在这种情况下,我们可以通过其 layer 来执行操作。

图1-1显示了 View 和 layer之间的关系以及与底层核心动画层的关系。

img

View 层级 & subView 管理

除了提供自己的内容外,一个 View 还可以作为其他 View 的一个容器。当一个 View 包含另一个 View 时,这两个 View 之间会产生父子关系。关系中的子 View 被称为subView,父 View 被称为 superView。

在视觉上,subView 的内容会掩盖其 superView 的全部或部分内容。如果 subView 是完全不透明的,那么子 View 所占据的区域就完全遮挡了 superView的相应区域。如果 subView是部分透明的,那么在屏幕上显示之前, subView & superView 的内容会混合在一起。

为了方便管理,每个 spuerView 将其 subView 存储在一个有序的数组中(每个 View 都有一个 subViews 属性,用于保存该 View 的所有subView ),该数组中的顺序也会影响每个 subView 的可见性。如果两个兄弟姐妹的 subView 相互重叠,最后添加的那个(或被移到subView数组的末尾)会出现在另一个上面,即数组中位置越靠后的元素所对应的View位置越靠上。

View层次结构中的View排列也决定了应用程序对事件的响应方式。当触摸发生在一个特定的 View 内时,系统会将带有触摸信息的事件对象直接发送到该 View 进行处理。然而,如果该 View 不处理特定的触摸事件,它可以将事件对象传递给它的 superView。如果superView不处理该事件,它就会把事件对象传递给它的超View,如此循环往复,直到响应者链结束。

关于响应链机制,之前零零散散看过很多篇文章,觉得都没有讲清楚。看了苹果的官方文档后,自己有了一点新的理解:

其实,当Touch Event 发生时,UIKit 会先找到触摸事件发生的 View,怎么找到的呢? 就是通过 hitTest 方法。

然后判断该 View 能不能响应这个事件,如果可以,那么就调用对应的method 进行响应,如果不能,则顺着响应链层层向上传递,大概过程是: View -> superView -> … -> View controller -> app delegate -> drop

特定的View也可以将事件对象传递给中间的响应者对象,例如 View Controller。如果没有对象处理该事件,它最终会到达应用对象,而应用对象通常会丢弃它。

注:关于如何正确处理事件,苹果官方文档中有一篇

View 绘制周期(这部分还没有完全理解,尤其是 setNeedsDisplay的调用时机)

UIView 类使用一个按需绘制的模型来呈现内容。当一个View第一次出现在屏幕上时,系统会要求它绘制其内容。系统会捕捉该内容的快照,并使用该快照作为View的视觉呈现。如果我们从未改变过View的内容,那么View的绘制代码可能就不会再被调用。该快照图像可用于涉及该View的大多数操作。如果我们确实改变了内容,我们会通知系统该View已经改变。然后,View会重复绘制View和捕获新结果的快照的过程。

当View的内容发生变化时,可以使用 setNeedsDisplay 或 setNeedsDisplayInRect: 方法使View失效。这些方法告诉系统,View的内容发生了变化,需要在下一次机会重新绘制。系统会等到当前的运行循环结束后再启动任何绘图操作。这种延迟给了我们一个机会,让我们一次就能使多个View失效,从层次结构中添加或删除View,隐藏View,调整View的大小,以及重新定位View。这样我们所做的所有改变都会在同一时间反映出来。

注意:改变View的几何形状并不会自动导致系统重新绘制View的内容。View的 contentMode 属性决定了如何解释对View的几何形状的改变。大多数内容模式在View的边界内拉伸或重新定位现有的快照,而不创建新的快照。

内容模式

每个 View 都有一个内容模式,它控制着 View 在响应 View 的几何形状变化时如何循环其内容以及是否循环其内容。当一个 View 第一次显示时,它像往常一样渲染其内容,结果被捕获在底层位图中。此后,View的几何形状的变化并不总是导致位图的重新创建。

相反, contentMode 属性中的值决定了位图是否应该被缩放以适应新的边界,或者仅仅被固定在View的一个角落或边缘。

有两种方式可以应用View的内容模式:

  1. 改变View的框架或边界矩形的宽度或高度。
  2. 为View的transform属性指定一个包含缩放因子的变换。

默认情况下,大多数View的 contentMode 属性被设置为 UIViewContentModeScaleToFill,这导致View的内容被缩放以适应新的框架尺寸。从图 1-2 中可以看出,并不是所有的内容模式都会导致View的边界被完全填满,而那些填满的模式可能会使View的内容变形。

Content mode comparisons

Build-In Animation support

在每个View后面有一个 layer 对象的好处之一是,我们可以很容易地将许多与View有关的变化做成动画。UIView类的许多属性都是可动画的,要对这些可动画的属性之一执行动画,我们所要做的就是:

  1. 告诉UIKit我们想执行一个动画。
  2. 改变该属性的值。

可以在UIView对象上做动画的属性有以下几种:

property meaning
frame 用它来为View的位置和大小变化制作动画。
bounds 用它来为View的大小变化制作动画。
center 改变View的位置
transform 用来旋转或者缩放View
alpha 改变View的透明度
backgroundColor 改变View的背景颜色
contentStretch 改变View的伸展方式

动画非常重要的一个地方是在从一组 View 过渡到另一组 View 时。例如,我们通常使用一个导航控制器(Navigation controller)来管理显示每一个连续的数据层的View之间的过渡。然而,我们也可以使用动画而不是View控制器在两组View之间创建过渡。我们可以在标准的 View 控制器动画不能产生我们想要的结果的地方这样做 (比如可以自定义专场的速度、方向之类的) 。

View 几何 & 坐标系

UIKit的默认坐标系统的原点在左上角,其坐标轴从原点向下和向右延伸。除了屏幕坐标系之外,窗口和View还定义了它们自己的本地坐标系,允许我们指定相对于View或窗口原点的坐标,而不是相对于屏幕的坐标。

View 坐标系

除了屏幕坐标系外,因为每个View和窗口都定义了自己的本地坐标系。

每次我们在View中绘图或改变其几何形状时,都是相对于某个坐标系进行的。

在绘图的情况下,我们指定相对于View自身坐标系的坐标。在改变几何形状的情况下,我们指定相对于 superView 的坐标系统的坐标。

UIWindow和UIView类可以帮助我们从一个坐标系转换到另一个坐标系。

Frame、Bounds 和 Center的关系

View 对象使用其frame、bounds 和 center 来确定大小和位置:

  1. frame 属性包含框架矩形,它指定了 View 在其 superView 坐标系中的大小和位置。
  2. bounds 属性包含 bounds 矩形,它指定了 View 在View自己的本地坐标系中的尺寸。
  3. center 属性包含了 View 在 superView 的坐标系中的已知中心点。

我们主要使用 center 和 frame 属性来操纵当前 View 的几何形状。

我们主要在绘图时使用 bounds 属性,边界矩形是用 View 自己的本地坐标系表示的。该矩形的默认原点是(0,0),其大小与框架矩形的大小一致。我们在这个矩形内画的任何东西都是 View 可见内容的一部分。如果我们改变边界矩形的原点,我们在新矩形内画的任何东西都会成为 View 可见内容的一部分。

图1-3显示了图像 View 的框架和边界矩形之间的关系。在图中,图像 View 的左上角位于其 superView 坐标系中的点(40,40),矩形的大小为240 × 380点。对于边界矩形,原点是(0,0),矩形的大小同样是240乘380点。

Relationship between a view's frame and bounds

尽管可以独立于其他属性来改变 frame、bounds 和 center 属性中的某一个,但对一个属性的改变会以下列方式影响其他属性:

  1. 当你设置框架属性时,边界属性中的大小值会改变以匹配框架矩形的新大小。中心属性中的值同样也会改变,以匹配框架矩形的新中心点。
  2. 当你设置中心属性时,框架中的原点值也会相应改变。
  3. 当你设置边界属性的大小时,框架属性中的大小值也会改变,以匹配边界矩形的新大小。

默认情况下,一个View的框架不会被剪切到其上层View的框架中。因此,任何位于其超View框架之外的子View都会被完整地呈现出来。不过你可以通过设置超View的 clipsToBounds 属性为 YES 来改变这种行为。不管子View是否在视觉上被剪切,触摸事件总是尊重目标View的超View的边界矩形。换句话说,发生在超View边界矩形之外的部分View的触摸事件不会被传递到该View。

Point & Pixels

在iOS中,所有的坐标值和距离都是以点为单位的浮点值来指定的。点的可测量大小因设备而异,也就是说,在程序中使用的衡量长度的度量单位是点,同样的程序在不用设备上产生的效果却是不同的。

表1-1列出了不同类型的基于iOS的设备在纵向上的屏幕尺寸(用点来衡量)。

Device Screen dimensions (in points)
iPhone and iPod touch devices with 4-inch Retina display 320 x 568
Other iPhone and iPod touch devices 320 x 480
iPad 768 x 1024

每种类型的设备所使用的基于点的测量系统定义了所谓的用户坐标空间。永远记住:一个点不一定对应于屏幕上的一个像素。

View的运行时交互模型

任何时候用户与你的用户界面互动,UIKit 内部都会发生一连串复杂的事件来处理这种互动。在这个序列中的特定点上,UIKit 会呼出View类,让它们有机会代表你的应用程序做出反应。

图 1-4 显示了基本的事件序列,从用户触摸屏幕开始,到图形系统更新屏幕内容作为回应结束。

UIKit interactions with your view objects

图 1-4 的具体响应过程如下:

  1. 用户点击屏幕。
  2. 硬件向 UIKit 框架报告一个 Touch Event。
  3. UIKit 将触摸打包成一个 UITouch 对象,并将该对象分发至相应的 View(Event Handling Guide for iOS.)
  4. 你的View的事件处理代码会对事件做出反应。例如,你的代码可能会。
    1. 改变View或其子View的属性(框架、边界、alpha等)。
    2. 调用setNeedsLayout方法,将View(或其子View)标记为需要布局更新。
    3. 调用setNeedsDisplay或setNeedsDisplayInRect:方法来标记View(或其子View)需要被重新绘制。
    4. 通知控制器关于某些数据的变化。

在前面的一组步骤中,你自己的自定义View的主要集成点是。

事件处理方法:

  1. touchesBegan:withEvent:
  2. touchesMoved:withEvent:
  3. touchesEnded:withEvent:
  4. touchesCancelled:withEvent:
  5. layoutSubViews方法
  6. drawRect:方法

这些是View最常用的重写方法,但你可能不需要重写所有的方法。如果你使用手势识别器来处理事件,则不需要覆盖任何事件处理方法。同样地,如果 View 不包含子View,或者它的大小不发生变化,就没有理由覆盖layoutSubViews方法。最后,drawRect:方法只有在View的内容可以在运行时改变。

Tips:

  1. 在优化你的绘图代码之前,你应该总是收集关于你的View的当前性能的数据。测量当前的性能可以让你确认是否真的存在问题,如果存在问题,可以给你一个基线测量,你可以对照它来比较未来的优化。
  2. View并不总是有一个对应的View Controller 在你的应用程序中,单个View和View Controller之间很少是一对一的关系。
  3. 尽量减少自定义绘图 尽管自定义绘图有时是必要的,但也是你应该尽可能避免的。只有当现有的系统View类不能提供你所需要的外观或功能时,你才应该真正做任何自定义绘图。任何时候你的内容都可以用现有View的组合来组装,你最好的选择是将这些View对象组合成一个自定义的View层次结构。
  4. 利用内容模式的优势 内容模式可以最大限度地减少重新绘制View的时间。默认情况下,View使用UIViewContentModeScaleToFill内容模式,该模式会缩放View的现有内容以适应View的框架矩形。你可以根据需要改变这种模式,以不同的方式调整你的内容,但如果可以,你应该避免使用UIViewContentModeRedraw内容模式。无论哪种内容模式生效,你都可以通过调用setNeedsDisplay或setNeedsDisplayInRect:来强制你的View重新绘制其内容。
  5. 尽可能地将View声明为不透明的 UIKit使用每个View的不透明属性来决定该View是否可以优化合成操作。将自定义View的该属性值设置为YES,告诉UIKit它不需要在你的View后面渲染任何内容。更少的渲染可以为您的绘图代码带来更多的性能,一般来说,我们鼓励这样做。当然,如果您将不透明属性设置为YES,您的View必须用完全不透明的内容填满其边界矩形。
  6. 调整你的View在滚动时的绘图行为 滚动会在很短的时间内产生大量的View更新。如果你的View的绘图代码没有得到适当的调整,你的View的滚动性能可能会很慢。与其努力确保View的内容在任何时候都是纯净的,不如考虑在滚动操作开始时改变View的行为。例如,你可以暂时降低渲染内容的质量,或者在滚动过程中改变内容模式。当滚动停止时,你可以将View恢复到之前的状态,并根据需要更新内容。
  7. 不要通过嵌入子View来定制控件 尽管在技术上可以向标准的系统控件–继承自UIControl的对象–添加子View,但你绝对不应该以这种方式定制它们。支持自定义的控件是通过控件类本身的明确的、有据可查的接口来实现的。例如,UIButton类包含了为按钮设置标题和背景图像的方法。使用已定义的自定义点意味着你的代码将始终正确工作。绕过这些方法,在按钮内嵌入一个自定义的图像View或标签,可能会导致你的应用程序现在或将来某个时候的行为不正确,如果按钮的实现发生变化的话。

Windows

每个iOS应用程序至少需要一个窗口 (UIWindow类的实例),有些可能包括多个窗口。窗口有以下几个职责:

  1. 它包含你的应用程序的可见内容。
  2. 它在向你的View和其他应用程序对象传递触摸事件方面起着关键作用。
  3. 它与你的应用程序的View Controller一起工作,以促进方向变化。

在iOS中,窗口没有标题栏、关闭框或任何其他视觉装饰物。窗口始终只是一个或多个View的空白容器另外,应用程序不会通过显示新的窗口来改变其内容。当你想改变显示的内容时,你会改变窗口的最前面的View。

大多数iOS应用程序在其生命周期内只创建和使用一个窗口。

窗口可以参与坐标系的转换。

创建窗口

在应用程序中加入didFinishLaunchingWithOptions: 方法的应用程序委托:

self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease]。

在前面的例子中,self.window被认为是应用程序委托的一个声明属性,它被配置为保留窗口对象。

在创建窗口时,你应该总是将窗口的大小设置为屏幕的全部边界。

在窗口中添加内容

每个窗口通常有一个单一的 root View 对象(由相应的 root controller 管理),它包含代表你的内容的所有其他View。

使用一个根View可以简化改变界面的过程;要显示新的内容,只需要替换 root View 。可以使用 addSubView 向窗口中添加一个View。

[window addSubView:ViewController.View]

你可以使用任何你想要的View作为窗口的根View。根据你的界面设计,根View可以是一个通用的 UIView 对象,作为一个或多个子View的容器,根View可以是一个标准的系统View,或者根View可以是一个你定义的自定义View。一些通常被用作根View的标准系统View包括滚动View、表格View和图像View。

重要:窗口的根View可以是一个容器View Controller(如Tabbar Controller 、导航控制器或分割View Controller)

改变窗口级别

每个 UIWindow 对象都有一个可配置的 windowLevel 属性,它决定了该窗口的层级。尽管你可以自己给窗口分配这些级别,但当你使用特定的界面时,系统通常会帮你做这些。例如,当你显示或隐藏状态栏或显示警报View时,系统会自动创建需要的窗口来显示这些项目,并将它的层级置顶。

监视窗口变化

如果你想在你的应用程序中跟踪窗口的出现或消失,可以使用这些与窗口相关的通知来做到这一点:

  1. UIWindowDidBecomeVisibleNotification
  2. UIWindowDidBecomeHiddenNotification
  3. UIWindowDidBecomeKeyNotification
  4. UIWindowDidResignKeyNotification

这些通知是为了响应你的应用程序的窗口的程序性变化而交付的。因此,当你的应用程序显示或隐藏一个窗口时,UIWindowDidBecomeVisibleNotification和UIWindowDidBecomeHiddenNotification的通知将相应地被传递。当你的应用程序进入后台执行状态时,这些通知不会被传递。即使当你的应用程序在后台时,你的窗口没有显示在屏幕上,它仍然被认为是在你的应用程序的上下文中是可见的。

UIWindowDidBecomeKeyNotification和UIWindowDidResignKeyNotification通知帮助你的应用程序跟踪哪个窗口是关键窗口,也就是说,哪个窗口目前正在接收键盘事件和其他与触摸无关的事件。触摸事件被传递到发生触摸的窗口,而没有相关坐标值的事件被传递到你的应用程序的关键窗口。一次只能有一个窗口是按键的。

View

因为View对象是应用程序与用户交互的主要方式,它们有很多责任:

  1. 布局和子View管理
  2. View定义了它自己的默认调整行为,与它的父View有关。
  3. View可以管理一个子View的列表。
  4. View可以根据需要覆盖其子View的大小和位置。
  5. View可以将其坐标系中的点转换成其他View或窗口的坐标系。
  6. 绘图和动画
  7. View在其矩形区域内绘制内容。
  8. 一些View的属性可以被动画化为新的值。
  9. 事件处理
  10. View可以接收触摸事件。
  11. View参与响应者链。

创建 View 对象

View的默认初始化方法是initWithFrame:方法,它设置了View相对于其(即将建立的)父View的初始尺寸和位置。例如,要创建一个新的通用UIView对象,你可以使用类似以下的代码。

CGRect ViewRect = CGRectMake(0, 0, 100, 100)。
UIView* myView = [[UIView alloc] initWithFrame:ViewRect]。

注意:虽然所有的View都支持initWithFrame:方法,但有些View可能有一个首选的初始化方法。

Properties of View

Properties Usage
alpha, hidden, opaque These properties affect the opacity of the View. The alpha and hidden properties change the View’s opacity directly. The opaque property tells the system how it should composite your View. Set this property to YES if your View’s content is fully opaque and therefore does not reveal any of the underlying View’s content. Setting this property to YES improves performance by eliminating unnecessary compositing operations.
bounds, frame, center, transform These properties affect the size and position of the View. The center and frame properties represent the position of the View relative to its parent View. The frame also includes the size of the View. The bounds property defines the View’s visible content area in its own coordinate system. The transform property is used to animate or move the entire View in complex ways. For example, you would use a transform to rotate or scale the View. If the current transform is not the identity transform, the frame property is undefined and should be ignored.For information about the relationship between the bounds, frame, and center properties, see The Relationship of the Frame, Bounds, and Center Properties. For information about how transforms affect a View, see Coordinate System Transformations.
autoresizingMask, autoresizesSubViews These properties affect the automatic resizing behavior of the View and its subViews. The autoresizingMask property controls how a View responds to changes in its parent View’s bounds. The autoresizesSubViews property controls whether the current View’s subViews are resized at all.
contentMode, contentStretch, contentScaleFactor These properties affect the rendering behavior of content inside the View. The contentMode and contentStretch properties determine how the content is treated when the View’s width or height changes. The contentScaleFactor property is used only when you need to customize the drawing behavior of your View for high-resolution screens. For more information on how the content mode affects your View, see Content Modes. For information about how the content stretch rectangle affects your View, see Stretchable Views. For information about how to handle scale factors, see Supporting High-Resolution Screens In Views in Drawing and Printing Guide for iOS.
gestureRecognizers, userInteractionEnabled, multipleTouchEnabled, exclusiveTouch These properties affect how your View processes touch events. The gestureRecognizers property contains gesture recognizers attached to the View. The other properties control what touch events the View supports.For information about how to respond to events in your Views, see Event Handling Guide for iOS.
backgroundColor, subViews, drawRect:method, layer, (layerClass method) These properties and methods help you manage the actual content of your View. For simple Views, you can set a background color and add one or more subViews. The subViewsproperty itself contains a read-only list of subViews, but there are several methods for adding and rearranging subViews. For Views with custom drawing behavior, you must override the drawRect: method. For more advanced content, you can work directly with the View’s Core Animation layer. To specify an entirely different type of layer for the View, you must override the layerClass method.

Tag

UIView类包含一个tag属性,可以用一个整数值来标记单个View对象。标签唯一地识别View层次结构中的View,并在运行时对这些View进行搜索(基于标签的搜索比自己遍历View层次要快)。标签属性的默认值是0。

要搜索一个有标签的View,应该使用 UIView 的 ViewWithTag: 方法。这个方法对接收器和它的子View进行深度优先搜索。

创建和管理View层次结构

管理View层次是开发应用程序用户界面的一个关键部分。View的组织既影响应用程序的视觉外观,也影响应用程序对变化和事件的响应。例如,View层次结构中的父子关系决定了哪些对象可以处理特定的触摸事件。同样地,父子关系定义了每个View如何响应界面方向的变化。

添加和删除子View

如果你喜欢以编程方式创建View,你可以创建并初始化它们,然后使用以下方法将它们排列成层次结构。

  1. 要将子View添加到父View中,请调用父View的 addSubView: 方法。该方法将子View添加到父View的子View列表的最后(即 subViews 数组的最后,这样说来,越是后面添加的 View,越在上层)。
  2. 要在父View的子View列表的中间插入一个子View,应调用父View的 insertSubView: 方法。在列表的中间插入一个子View,在视觉上会把该View放在列表中后面的任何View后面。
  3. 要在其父View内重新排序现有的子View,应调用父View的 bringSubViewToFront:sendSubViewToBack:, 或 exchangeSubViewAtIndex:withSubViewAtIndex:方法。

要从它的父View中移除一个子View,请调用子View(而不是父View)的removeFromSuperView方法。

当添加一个子View到它的父View时,子View的当前框架矩形表示它在父View内的初始位置。一个子View的框架位于其上级View的可见边界之外,默认情况下不会被剪切。如果想让子View被剪切到超View的边界上,必须明确地将超View的clipsToBounds属性设置为YES

可以在View层次结构中添加子View的一个地方是在View controller的 loadViewViewDidLoad 方法中。如果以编程方式建立View,你可以把创建View的代码放在View Controller的 loadView 方法中。

清单3-1显示了UIKit目录(iOS)中 TransitionsViewController 类的 ViewDidLoad方法。创建和定制UIKit控件示例应用程序。 TransitionsViewController 类管理着与两个View之间的过渡相关的动画。

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    self.title = NSLocalizedString(@"TransitionsTitle", @"");
 
    // create the container view which we will use for transition animation (centered horizontally)
    CGRect frame = CGRectMake(round((self.view.bounds.size.width - kImageWidth) / 2.0),
                                                        kTopPlacement, kImageWidth, kImageHeight);
    self.containerView = [[[UIView alloc] initWithFrame:frame] autorelease];
    [self.view addSubview:self.containerView];
 
    // The container view can represent the images for accessibility.
    [self.containerView setIsAccessibilityElement:YES];
    [self.containerView setAccessibilityLabel:NSLocalizedString(@"ImagesTitle", @"")];
 
    // create the initial image view
    frame = CGRectMake(0.0, 0.0, kImageWidth, kImageHeight);
    self.mainView = [[[UIImageView alloc] initWithFrame:frame] autorelease];
    self.mainView.image = [UIImage imageNamed:@"scene1.jpg"];
    [self.containerView addSubview:self.mainView];
 
    // create the alternate image view (to transition between)
    CGRect imageFrame = CGRectMake(0.0, 0.0, kImageWidth, kImageHeight);
    self.flipToView = [[[UIImageView alloc] initWithFrame:imageFrame] autorelease];
    self.flipToView.image = [UIImage imageNamed:@"scene2.jpg"];
}

重要提示: Superview 会自动保留它们的子View,所以在嵌入一个子View后,释放该子View是安全的。事实上,这样做是值得推荐的,因为它可以防止你的应用程序过多地保留该View而导致以后的内存泄漏。请记住,如果你把一个子View从它的超View中移除并打算重新使用它,你必须再次保留该子View。removeFromSuperview方法在将子View从其超View中移除之前会自动释放该View。如果你在下一个事件循环周期前没有保留该View,该View将被释放。

当向另一个View添加一个子View时,UIKit会通知父View和子View的变化。如果实现了自定义View,可以通过覆盖 willMoveToSuperview:, willMoveToWindow:, willRemoveSubview:, didAddSubview:, didMoveToSuperview, 或 didMoveToWindow 方法中的一个或多个来拦截这些通知。可以使用这些通知来更新与你的View层次结构相关的任何状态信息或执行其他任务。

隐藏View

要在视觉上隐藏一个View,可以将其隐藏属性设置为YES或将其alpha属性改为0.0。隐藏的View不会接收来自系统的触摸事件。然而,隐藏的View会参与自动调整大小和其他与View层次结构相关的布局操作。因此,隐藏View通常是一种方便的选择,可以从View层次结构中移除View,特别是如果打算在不久后再次显示这些View。

重要提示:如果你隐藏了一个目前是第一响应者的View,该View不会自动放弃其第一响应者的地位。针对第一响应者的事件仍然会被传递到隐藏的View。为了防止这种情况发生,你应该在隐藏View时强制其放弃第一响应者的地位。

如果想对View从可见到隐藏(或相反)的过渡制作动画,必须使用View的alpha属性来实现。隐藏属性不是一个可动画的属性,所以你对它做的任何改变都会立即生效。

在View层次结构中定位View

有两种方法可以在View层次结构中定位View:

  1. 在一个适当的位置存储指向任何相关View的指针,例如在拥有这些View的View Controller中。
  2. 为每个View的tag属性分配一个唯一的整数,并使用viewWithTag:方法来定位它。

存储对相关View的引用是定位View的最常用方法,并使访问这些View变得非常方便。可以在私有成员变量中存储对这些View的引用。无论你使用outlet还是私有成员变量,你都要负责在需要时保留View,然后再释放它们。确保对象被正确保留和释放的最好方法是使用声明的属性。

标签是一种有用的方法,可以减少硬编码的依赖性,支持更多的动态和灵活的解决方案。与其存储一个指向View的指针,不如使用它的标签来定位它。标签也是引用View的一种更持久的方式。例如,如果你想保存当前在你的应用程序中可见的View列表,你可以把每个可见View的标签写到一个文件中。这比归档实际的View对象更简单,尤其是在你只追踪当前可见的View的情况下。当你的应用程序随后被加载时,你将重新创建你的View,并使用保存的标签列表来设置每个View的可见性,从而使你的View层次结构回到以前的状态。

View层次结构中的坐标转换

在不同的时候,特别是在处理事件时,应用程序可能需要将坐标值从一个参考框架转换到另一个框架。例如,触摸事件报告了每次触摸在窗口坐标系中的位置,但View对象经常需要在View的本地坐标系中的该信息。UIView类定义了以下方法,用于将坐标转换到View的本地坐标系统或从该系统转换。

convertPoint:fromView:
convertRect:fromView:
convertPoint:toView:
convertRect:toView:

convert...:fromView:方法将坐标从其他View的坐标系转换成当前View的本地坐标系(边界矩形)。反之,convert...:toView:方法将坐标从当前View的本地坐标系(边界矩形)转换到指定View的坐标系。

除了UIView的转换方法外,UIWindow类还定义了几个转换方法。这些方法与UIView版本类似,只是这些方法不是转换到View的本地坐标系,而是转换到窗口的坐标系。

convertPoint:fromWindow:
convertRect:fromWindow:
convertPoint:toWindow:
convertRect:toWindow:

在旋转View中转换坐标时,UIKit转换矩形的前提是你希望返回的矩形能反映源矩形所覆盖的屏幕区域。图3-3显示了一个例子,说明在转换过程中,旋转会导致矩形的大小发生变化。在图中,一个外部父View包含一个旋转的子View。将子View坐标系中的矩形转换到父View的坐标系中,会产生一个物理上较大的矩形。这个更大的矩形实际上是 outerView 范围内完全包围旋转矩形的最小的矩形。

Converting values in a rotated view

在运行时修改View

当应用程序收到来自用户的输入时,它们会根据输入调整其用户界面。一个应用程序可以通过重新安排View、改变它们的大小或位置、隐藏或显示它们,或者加载一组全新的View来修改其View。在iOS应用程序中,有几个地方和方式可以执行这些类型的操作。

在一个View Controller中:

  • View Controller必须在显示View之前创建其View。它可以从nib文件中加载View或以编程方式创建它们。当这些View不再需要时,它就会将它们处理掉。
  • 当一个设备改变方向时,View Controller可能会调整View的大小和位置以适应。作为对新方向调整的一部分,它可能会隐藏一些View并显示其他的。
  • 当一个View Controller管理可编辑的内容时,它可能会在进入和离开编辑模式时调整其View层次。例如,它可能会添加额外的按钮和其他控件,以方便编辑其内容的各个方面。这可能还需要调整任何现有View的大小以适应额外的控件。

在动画块中:

  • 当你想在用户界面的不同View集之间进行转换时,你可以在动画块中隐藏一些View并显示其他View。
  • 在实现特殊效果时,你可能会使用一个动画块来修改View的各种属性。例如,为了使View的大小发生动画变化,你会改变其框架矩形的大小。

其他方式:

  • 当触摸事件或手势发生时,你的界面可能会通过加载新的View集或改变当前的View集来响应。
  • 当用户与滚动View交互时,一个大的可滚动区域可能会隐藏和显示瓦片子View。
  • 当键盘出现时,你可能会重新定位或调整View的大小,使它们不在键盘的下方。

View Controller是发起对View进行修改的常见场所。因为View Controller管理着与所显示的内容相关的View层次,它最终对发生在这些View上的一切负责。当加载其View或处理方向变化时,View Controller可以添加新的View,隐藏或替换现有的View,并进行任何数量的更改,使View准备好显示。

与核心动画层互动

每个View对象都有一个专门的核心动画层,管理View内容在屏幕上的呈现和动画。虽然你可以用View对象做很多事情,但你也可以根据需要直接用相应的层对象工作。View的图层对象存储在View的图层属性中。

改变与View相关的图层类型

与View相关的图层类型在View创建后是不能改变的。因此,每个View都使用layerClass类方法来指定其图层对象的类别。该方法的默认实现会返回CALayer类,改变该值的唯一方法是子类,覆盖该方法,并返回一个不同的值。你可以改变这个值来使用不同种类的层。例如,如果你的View使用平铺来显示一个大的可滚动区域,你可能想使用CATiledLayer类来支持你的View。

layerClass方法的实现应该简单地创建所需的Class对象并返回它。例如,一个使用平铺的View,这个方法的实现如下。

\- (Class)layerClass
{
  return [CATiledLayer class]
}

每个View在其初始化过程的早期都会调用其layerClass方法,并使用返回的类来创建其层对象。此外,View总是将自己指定为其层对象的委托人。在这一点上,View拥有它的层,View和层之间的关系不能改变。你也不能把同一个View指定为任何其他层对象的委托人。改变View的所有权或委托关系将导致绘图问题和应用程序的潜在崩溃。

定义一个自定义View

如果标准的系统View不能完全满足你的需要,可以定义一个自定义View。自定义View可以让你完全控制你的应用程序内容的外观以及如何处理与该内容的交互。

实现自定义View的检查表

自定义View的工作是展示内容并管理与该内容的交互。不过,成功实现一个自定义View不仅仅涉及到绘制和处理事件。下面的清单包括实现自定义View时可以覆盖的更重要的方法(以及可以提供的行为):

  1. 为你的View定义适当的初始化方法。
  • 对于你计划以编程方式创建的View,覆盖initWithFrame:方法或定义一个自定义的初始化方法。
  • 对于计划从nib文件加载的View,覆盖initWithCoder: 方法。使用这个方法来初始化你的View,并将其放入一个已知的状态。
  1. 实现一个dealloc方法来处理任何自定义数据的清理。

  2. 为了处理任何自定义的绘图,覆盖drawRect:方法并在那里进行绘图。

  3. 设置View的autoresizingMask属性以定义其自动调整行为。

  4. 如果你的View类管理着一个或多个完整的子View,请执行以下操作。

  • 在View的初始化序列中创建这些子View。
  • 在创建时设置每个子View的autoresizingMask属性。
  • 如果你的子View需要自定义布局,覆盖layoutSubviews方法并在那里实现你的布局代码。
  1. 为了处理基于触摸的事件,请做以下工作。
  • 通过使用addGestureRecognizer:方法将任何合适的手势识别器附加到View中。
  • 对于你想自己处理触摸的情况,覆盖touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:, and touchesCancelled:withEvent:方法。(记住,你应该总是覆盖touchesCancelled:withEvent:方法,不管你覆盖了哪些其他与触摸相关的方法。)

初始化自定义View

定义的每个新的View对象都应该包括一个自定义的initWithFrame:初始化方法。该方法负责在创建时初始化该类,并将你的View对象放入一个已知的状态。

下列代码显示了一个标准initWithFrame:方法的骨架实现。这个方法首先调用该方法的继承实现,然后在返回初始化对象前初始化类的实例变量和状态信息。

初始化一个View子类:

1
2
3
4
5
6
7
8
- (id)initWithFrame:(CGRect)aRect {
    self = [super initWithFrame:aRect];
    if (self) {
          // 设置视图的初始属性
          ...
       }
    return self;
}

对事件的响应

View对象是响应者对象 – UIResponder类的实例,因此能够接收触摸事件。当一个触摸事件发生时,窗口会将相应的事件对象派发给发生触摸的View。如果你的View对某个事件不感兴趣,它可以忽略它,或者将它传递到响应者链上,由不同的对象来处理。

除了直接处理触摸事件外,View还可以使用手势识别器来检测轻拍、轻扫、轻捏以及其他类型的常见触摸相关手势。手势识别器做的是跟踪触摸事件的艰苦工作,并确保它们遵循正确的标准,使其成为目标手势。你可以创建手势识别器,为其分配适当的目标对象和动作方法,并使用 addGestureRecognizer: 方法将其安装在View上,而不是让你的应用程序去跟踪触摸事件。当相应的手势发生时,手势识别器会调用你的动作方法。

如果喜欢直接处理触摸事件,可以为你的View实现以下方法,这些方法在Event Handling Guide for iOS中有更详细的描述:

1
2
3
4
touchesBegan:withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:

View的默认行为是一次只响应一个触摸。如果用户放下第二个手指,系统会忽略这个触摸事件,不向你的View报告。如果你打算从View的事件处理程序方法中跟踪多指手势,你需要通过将View的 multipleTouchEnabled 属性设置为 YES 来启用多点触摸事件。

一些View,如标签和图像,最初完全禁用事件处理。你可以通过改变View的 userInteractionEnabled 属性的值来控制该View是否能够接收触摸事件。你可以暂时将此属性设置为NO,以防止用户在长时间操作时操作你的View的内容。为了防止事件到达你的任何View,你也可以使用UIApplication对象的 beginIgnoringInteractionEventsendIgnoringInteractionEvents 方法。这些方法影响整个应用程序的事件传递,而不仅仅是单个View。

注意:UIView的动画方法通常在动画进行中禁用触摸事件。你可以通过适当地配置动画来覆盖这种行为。

在处理触摸事件时,UIKit 使用 UIView的 hitTest:withEvent:pointInside:withEvent: 方法来确定触摸事件是否发生在特定View的边界内。虽然你很少需要覆盖这些方法,但你可以这样做来为你的View实现自定义触摸行为。**例如,你可以覆盖这些方法来防止子View处理触摸事件。

清理你的View

如果你的View类分配了任何内存,存储了对任何自定义对象的引用,或持有必须在View释放时释放的资源,你必须实现一个dealloc方法。当你的View的保留计数达到0,并且是时候去分配View的时候,系统会调用dealloc方法。你对这个方法的实现应该释放View持有的任何对象或资源,然后调用继承的实现,如清单3-5所示。你不应该使用这个方法来执行任何其他类型的任务。

实现dealloc方法:

- (void)dealloc {
    // 释放一个保留的UIColor对象
    [color release];

    // 调用继承的实现
    [super dealloc];
}

动画

下表列出了可动画的属性 – 有内置动画支持的 UIView 类的属性。可动画化并不意味着动画会自动发生。改变这些属性的值通常只是立即更新该属性(和View),而没有动画。要使这种变化成为动画,你必须在一个动画块中改变属性的值,这在《View中的属性变化动画》中有所描述。

| | | —- |