View编程指南(三)

《View Programming Guide for iOS》文档翻译

Posted by Ted on January 15, 2018

苹果官方文档View Programming Guide for iOS

四、Views

由于View对象是应用程序与用户交互的主要方式,因此它们有许多责任。 这里仅仅是少数:

布局和subview管理

  • view定义了与其父view相关的默认调整大小行为。
  • 一个view可以管理subview列表。
  • view可以根据需要重写subview的大小和位置。
  • view可以将其坐标系中的点转换为其他view或window的坐标系。

绘画和动画

  • view在其矩形区域绘制内容。
  • 一些view属性可以动画变成新的值。

事件处理

  • view可以接收触摸事件。
  • view参与响应者链。

创建View

创建View最简单的方法是使用Interface Builder以图形方式进行组装。从Interface Builder中,您可以将View添加到界面,将这些view排列到层次结构中,配置每个view的设置,并将与view相关的行为连接到您的代码。由于Interface Builder使用实时view对象(即view类的实际实例),因此您在设计时看到的是运行时获得的内容。然后将这些活动对象保存在一个nib文件中,这是一个资源文件,用于保留对象的状态和配置。

您通常会创建nib文件,以便为应用程序的一个view controller存储整个view层次结构。 nib文件的顶层通常包含一个表示view controllerview的view对象。(view controller本身通常由文件的所有者对象来表示。)顶层view的大小应该适合目标设备,并且包含所有要呈现的其他view。使用nib文件仅存储view controllerview层次结构的一部分是很少见的。

在view controller中使用nib文件时,只需使用nib文件信息初始化view controller即可。view controller在适当的时候处理view的加载和卸载。但是,如果您的nib文件未与view controller关联,则可以使用NSBundle或UINib对象手动加载nib文件内容,该对象使用nib文件中的数据来重构view对象。

或者用代码创建

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

设置View的属性

UIView类有几个声明的属性来控制View的外观和行为。 这些属性用于操纵View的大小和位置,View的透明度,背景颜色和渲染行为。 所有这些属性都具有适当的默认值,您可以根据需要稍后进行更改。您还可以使用“Inspector”窗口在Interface Builder中配置其中的许多属性。

属性 用处
alpha, hidden, opaque 这些属性影响view的不透明度。 alpha和hidden属性直接改变view的不透明度。opaque属性告诉系统如何合成view。 如果view的内容完全不透明,则将此属性设置为YES,因此不会显示任何底层view的内容。 将此属性设置为YES可消除不必要的合成操作,从而提高性能。
bounds, frame, center, transform 这些属性会影响view的大小和位置。 center和frame属性表示view相对于superview的位置。 该frame还包括view的大小。 bounds属性定义了view在其自己的坐标系中的可见内容区域。transform属性用于以复杂的方式动画或移动整个view。 例如,您将使用变换来旋转或缩放view。 如果当前变换不是身份变换,则frame属性是不确定的,应该忽略。
autoresizingMask, autoresizesSubviews 这些属性会影响view及其子view的自动调整大小行为。 autoresizingMask属性控制view如何响应其父view bounds中的更改。 autoresizesSubviews属性控制是否调整当前view的subview的大小。
contentMode, contentStretch, contentScaleFactor 这些属性影响view内部内容的呈现行为。 contentMode和contentStretch属性确定在view的宽度或高度更改时如何处理内容。 contentScaleFactor属性仅在需要为高分辨率屏幕自定义view的绘制行为时使用。
gestureRecognizers, userInteractionEnabled, multipleTouchEnabled, exclusiveTouch 这些属性会影响view处理触摸事件的方式。 gestureRecognizers属性包含附加到view的手势识别器。 其他属性控制view支持的触摸事件。
backgroundColor, subviews, drawRect:method, layer, (layerClass method) 这些属性和方法可以帮助您管理view的实际内容。 对于简单的view,您可以设置背景颜色并添加一个或多个subviews。 subviews属性本身包含subview的只读列表,但有几种添加和重新排列subview的方法。 对于具有自定义绘图行为的view,您必须重写drawRect:方法。

UIView类包含一个tag属性,您可以使用它来为各个view对象添加一个整数值。 您可以使用tag唯一地标识view层次结构中的view,并在运行时执行对这些view的搜索。(基于tag的搜索比自己迭代view层次更快。)tag属性的默认值为0。

要搜索带tag的view,请使用UIView的viewWithTag:方法。 此方法执行接收器及其subview的深度优先搜索。 它不搜索superview或view层次结构的其他部分。 因此,从层次结构的root view调用此方法会搜索层次结构中的所有view,但是从特定的subview调用它只会搜索view的子集。

创建和管理一个View Hierarchy

管理view层次结构是开发应用程序用户界面的关键部分。 您的view的组织影响您的应用程序的外观,以及您的应用程序如何响应更改和事件。 例如,view层次结构中的父子关系确定哪些对象可能处理特定的触摸事件。 同样,父子关系定义每个view如何响应界面方向的变化。

img

添加和删除子view

Interface Builder是构建view层次结构最方便的方式,因为您可以用图形方式组装view,查看view之间的关系,并确切了解在运行时将如何显示这些view。使用Interface Builder时,将结果view层次结构保存在一个nib文件中,在运行时加载,因为需要相应的view。

如果您希望以编程方式创建view,请创建并初始化它们,然后使用以下方法将它们排列为层次结构:

要将subview添加到superview,请调用superview的addSubview:方法。此方法将subview添加到父级View的subviews列表的末尾。 要在superview的subviews中间插入subview,请调用superview的任何insertSubview:...方法。在list中间插入一个subview可视化地将该view放置在列表中稍后的任何view的后面。 要对其superview的现有subviews进行重新排序,请调用superview的bringSubviewToFront:sendSubviewToBack:exchangeSubviewAtIndex:,withSubviewAtIndex:方法。使用这些方法比删除subview并重新插入它们要快。 要从其superview移除subview,请调用subview的removeFromSuperview方法(而不是superview)。 当subview添加到其父项时,subview的当前frame矩形表示它在superview内的初始位置。frame位于其superview的可见边界之外的subview在默认情况下不会被剪切。如果您希望将subview剪裁到superview的边界,则必须明确地将superview的clipsToBounds属性设置为YES。

一个地方你可以添加subview到view层次结构是在VC的loadView或viewDidLoad方法。如果以编程方式构建view,则将view创建代码放置在view controller的loadView方法中。无论是以编程方式创建view还是从nib文件加载view,都可以在viewDidLoad方法中包含其他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"];
}

重要提示:Superviews自动retain subviews,所以在嵌入subview之后,释放该subview是安全的。 实际上,建议这样做是因为它会阻止您的应用程序保留一次太多的view,并在稍后导致内存泄漏。 请记住,如果您从其supview中删除subview并打算重用它,则必须再次保留该subview。 removeFromSuperview方法在移除之前autorelease一个subview。 如果在下一个事件循环周期之前不retain view,view将被released。

将subview添加到另一个View时,UIKit会通知superview和subview。 如果实现自定义view,则可以通过重写willMoveToSuperview:willMoveToWindow:willRemoveSubview:didAddSubview:didMoveToSuperviewdidMoveToWindow方法中的一个或多个拦截这些通知。 您可以使用这些通知来更新与您的view层次结构相关的任何状态信息或执行其他任务。

创建view层次结构后,可以使用superivew和subview属性以编程方式导航它。 每个view的window属性包含当前显示view的window(如果有的话)。 由于view层次结构中的root view没有父view,因此其superview属性设置为nil。 对于当前在屏幕上的view,window对象是view层次结构的root view。

隐藏View

要以可视方式隐藏view,可以将其hidden属性设置为YES,也可以将其alpha属性更改为0.0。隐藏的view不会从系统接收触摸事件。但是,隐藏的view会参与与view层次关联的自动调整和其他布局操作。因此,隐藏view通常是从view层次结构中删除view的一种方便的替代方法,特别是如果您计划在不久的将来再次显示view时。

重要提示:如果您隐藏当前是第一响应者的view,则该view不会自动退出其第一响应者状态。针对第一响应者的事件仍然传递到隐藏的view。为了防止这种情况发生,当您隐藏它时,您应该强制您的view退出第一个响应者状态。

如果要为view的可视化转换为隐藏(或相反),您必须使用view的alpha属性进行动画处理。隐藏的属性不是一个动画属性,所以你对它做出的任何改变立即生效。

定位View Hierarchy中的view

在view层次结构中定位view有两种方法:

  • 将指针存储在适当位置的任何相关view中,例如拥有view的VC中。

  • 为每个view的tag属性分配一个唯一的整数,并使用viewWithTag:方法来定位它。

存储对相关view的引用是定位view的最常见方法,并使访问这些view非常方便。如果使用Interface Builder创建view,则可以使用outlet将nib文件中的对象连接到另一个对象。对于以编程方式创建的view,可以在私有成员变量中存储对这些view的引用。无论您使用outlets还是私有成员变量,您都有责任根据需要保留view,然后将其释放。确保对象被保留和正确释放的最好方法是使用声明的属性。

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

移动、缩放、旋转View

每个view都有一个关联的affine transform,可以用来移动,缩放或旋转view的内容。 view transform会改变view的最终渲染外观,通常用于实现滚动,动画或其他视觉效果。

UIView的transform属性包含一个应用了转换的CGAffineTransform结构。 默认情况下,此属性设置为标记转换,不会修改view的外观。 您可以随时为此属性分配一个新的转换。 例如,要将view旋转45度,可以使用以下代码:

CGAffineTransform xform = CGAffineTransformMakeRotation(M_PI/4.0);
self.view.transform = xform;

将上述代码中的变换应用于view将围绕其中心点顺时针旋转。

img

将多个transform应用于view时,将这些transform添加到CGAffineTransform结构的顺序非常重要。 旋转view然后移动与先移动后旋转是不一样的。 即使在每种情况下旋转和平移的数量是相同的,但是变换的顺序影响最终的结果。 此外,您添加的任何转换都将应用于相对于view的center。 因此,应用旋转因子围绕其中心点旋转view。 缩放view会更改view的宽度和高度,但不会更改其center.

View Hierarchy中转换坐标

在许多情况下,特别是在处理事件时,应用程序可能需要将坐标值从一个参照系转换为另一个参照系。 例如,触摸事件报告每个触摸在windows坐标系中的位置,但view对象通常需要view的本地坐标系中的信息。 UIView类定义了以下用于将坐标转换为view本地坐标系的方法:

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

convert ...:fromView:方法将坐标从其他view的坐标系转换为当前view的局部坐标系(bounds矩形)。 相反,convert ...:toView:方法将坐标从当前view的本地坐标系(bounds矩形)转换为指定view的坐标系。 如果您将nil指定为任何方法的参考view,则将转换到包含view的window的坐标系并从该view的坐标系转换。

除了UIView转换方法之外,UIWindow类还定义了几种转换方法。 这些方法类似于UIView版本,除了不是从view的局部坐标系转换而是从window的坐标系转换而来。

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

在旋转view中转换坐标时,UIKit会在假定要返回的矩形反映源矩形所覆盖的屏幕区域的情况下转换矩形。

图显示了一个转换过程中如何导致矩形大小改变的例子。 在图中,外部父view包含旋转的subview。 将subview坐标系中的矩形转换为父坐标系,得到一个物理上较大的矩形。 这个较大的矩形实际上是outerView bounds中最小的矩形,它完全包围了旋转的矩形。

img

运行时调整View的大小和位置

每当view的大小发生变化时,其subview的大小和位置都必须相应地改变。 UIView类支持view hierarchy中的view的自动和手动布局。 通过自动布局,您可以设置每个view在其父view调整大小时应遵循的规则,然后完全忽略调整大小的操作。 通过手动布局,您可以根据需要手动调整view的大小和位置。

在view中发生以下任何事件时,可能会发生布局更改:

  • view bounds矩形的大小发生变化。
  • 发生界面方向更改,通常会触发root view bounds矩形中的更改。
  • 与view图层相关联的核心动画sublayers发生更改并需要布局。
  • 您的应用程序通过调用view的setNeedsLayoutlayoutIfNeeded方法来强制执行布局。
  • 您的应用程序通过调用view 底层layer的setNeedsLayout方法来强制布局。

当您更改view的大小时,通常需要更改嵌入的子view的位置和大小,以考虑其父级的新大小。 superview的autoresizesSubviews属性决定子view是否调整大小。如果此属性设置为YES,则该view使用每个子view的autoresizingMask属性来确定如何调整和定位该子view。对任何子view的大小更改会触发嵌入式子view的类似布局调整。

对于view层次结构中的每个view,将该view的autoresizingMask属性设置为适当的值是处理自动布局更改的重要部分。

img

配置自动调整规则的最简单方法是使用Interface Builder的“Size”检查器中的“AutoSizing”控件。 上图中灵活的宽度和高度常数与“AutoSizing”控件图中的宽度和大小指示器具有相同的行为。 但是,保证的行为和使用是有效的逆转。 在界面构建器中,边缘指示符的存在意味着边距具有固定大小,并且缺少指示符意味着边距具有灵活的大小。 幸运的是,Interface Builder提供了一个动画来展示自动修改行为对你的view的影响。

重要提示:如果view的transform属性不包含标识转换,则该view的frame是未定义的,其自动调整行为的结果也是如此。

手动调整view的布局

只要view的大小发生变化,UIKit就会应用该view的subview的自动调整行为,然后调用view的layoutSubviews方法以使其进行手动更改。您可以在自定义view中实现layoutSubviews方法,当自动执行行为本身不会产生所需的结果时。此方法的实现可以执行以下任何操作:

  • 调整任何直接subview的大小和位置。

  • 添加或删除subview或核心动画layer。
  • 通过调用setNeedsDisplay或setNeedsDisplayInRect:方法强制subview重绘。

应用程序经常手动布置root view的一个地方是在实现大的可滚动区域时。由于对其可滚动内容拥有一个大view是不切实际的,因此应用程序通常会实现一个root view,其中包含许多较小的view。每个图块代表可滚动内容的一部分。当滚动事件发生时,根view调用其setNeedsLayout方法来启动布局更改。其layoutSubviews方法然后根据发生的滚动量重新定位平铺view。当tile从view的可见区域滚出时,layoutSubviews方法将tile移动到传入边缘,替换进程中的内容。

在编写布局代码时,请务必以下列方式测试您的代码:

  • 更改view的方向以确保布局在所有支持的接口方向上正确。
  • 确保你的代码正确响应状态栏高度的变化。当打电话时,状态栏高度会增加,当用户结束通话时,状态栏的大小会减小。

在运行时修改view

随着应用程序从用户接收输入,他们调整其用户界面以响应该输入。应用程序可能会通过重新排列view,更改其大小或位置,隐藏或显示view或加载全新的view来修改view。在iOS应用程序中,有几种地方和方法可以执行这些操作:

在VC中:

  • view controller必须在显示它们之前创建其view。它可以从一个nib文件加载view或以编程方式创建它们。当这些views不再需要时,就把它们处理掉。

  • 当设备改变方向时,view controller可能会调整view的大小和位置以匹配。作为调整新方向的一部分,可能会隐藏一些views,并显示其他views。
  • 当view controller管理可编辑的内容时,它可能会调整其view层次结构时,编辑模式。例如,它可能会添加额外的按钮和其他控件来方便编辑其内容的各个方面。这可能还需要调整任何现有的view以适应额外的控制。

在动画块中:

  • 当您想要在用户界面的不同view集之间切换时,您可以隐藏一些view并在动画块中显示其他view。
  • 实现特殊效果时,可以使用动画块来修改view的各种属性。例如,要动画改变view的大小,你可以改变它的frame矩形的大小。

其他方法:

  • 触摸事件或手势发生时,您的界面可能会通过加载一组新的view或更改当前的view来作出响应。有关处理事件的信息,请参阅iOS事件处理指南。
  • 当用户与滚动view交互时,大的可滚动区域可能会隐藏并显示切片子view。有关支持可滚动内容的更多信息,请参阅Scroll View Programming Guide for iOS
  • 当键盘出现时,您可以重新定位或调整view的大小,使其不会位于键盘下方。有关如何与键盘交互的信息,请参阅Text Programming Guide for iOS

view controller是对view进行更改的常用位置。由于view controller管理与正在显示的内容相关联的view分层结构,因此它最终负责这些view发生的所有事情。当加载其view或处理方向更改时,view controller可以添加新view,隐藏或替换现有view,并进行任意数量的更改以使view准备好显示。如果您实现了对编辑view内容的支持,则UIViewController中的setEditing:animated:方法为您提供了将view转换为可编辑版本的地方。

动画块是启动view相关更改的另一个常见位置。内置到UIView类中的动画支持可以轻松地将更改设置为查看属性。您也可以使用transitionWithView:duration:options:animations:completion:transitionFromView:toView:duration:options:completion:将新整个view换出的方法

与Core Animation Layers交互

每个view对象都有一个专用的Core Animation Layer,用于管理屏幕上view内容的显示和动画。 虽然您可以使用view对象做很多事情,但您也可以根据需要直接使用相应的layer对象。 view的layer对象存储在view的layer属性中。

与view关联的layer类型在创建view后无法更改。 因此,每个view使用layerClass类方法来指定其layer对象的类。 此方法的默认实现返回CALayer类,更改此值的唯一方法是子类,重写该方法,并返回一个不同的值。 您可以更改此值以使用不同类型的图层。 例如,如果view使用平铺来显示大的可滚动区域,则可能需要使用CATiledLayer类来支持view。

实现layerClass方法应该简单地创建所需的Class对象并返回它。 例如,使用平铺的view将为此方法提供以下实现:

+ (Class)layerClass
{
    return [CATiledLayer class];
}

每个view在其初始化过程中尽早调用其layerClass方法,并使用返回的类来创建其图层对象。 另外,view总是将自己指定为其图层对象的delegate。 在这一点上,view拥有它的层,view和层之间的关系不能改变。 您还必须不分配与任何其他图层对象的delegate相同的view。 更改view的所有权或delegate关系会导致绘图问题和应用程序中的潜在崩溃

如果您主要使用图层对象而不是view,则可以根据需要将自定义图层对象合并到view层次结构中。 自定义图层对象是不属于view的CALayer的任何实例。 您通常以编程方式创建自定义图层,并使用Core Animation例程将其合并。 自定义图层不接收事件或参与响应者链,但根据核心动画规则绘制自己的图形并响应其父view或图层中的大小更改

- (void)viewDidLoad {
    [super viewDidLoad];
 
    // Create the layer.
    CALayer* myLayer = [[CALayer alloc] init];
 
    // Set the contents of the layer to a fixed image. And set
    // the size of the layer to match the image size.
    UIImage layerContents = [[UIImage imageNamed:@"myImage"] retain];
    CGSize imageSize = layerContents.size;
 
    myLayer.bounds = CGRectMake(0, 0, imageSize.width, imageSize.height);
    myLayer = layerContents.CGImage;
 
    // Add the layer to the view.
    CALayer*    viewLayer = self.view.layer;
    [viewLayer addSublayer:myLayer];
 
    // Center the layer in the view.
    CGRect        viewBounds = backingView.bounds;
    myLayer.position = CGPointMake(CGRectGetMidX(viewBounds), CGRectGetMidY(viewBounds));
 
    // Release the layer, since it is retained by the view's layer
    [myLayer release];
}

响应事件

view对象是响应者对象(UIResponder类的实例),因此能够接收触摸事件。当触摸事件发生时,window将相应的事件对象分派到发生触摸的view。如果你的view对一个事件不感兴趣,它可以忽略它,或者把它传递给响应者链,由另一个对象处理。

除了直接处理触摸事件之外,view还可以使用手势识别器来检测轻敲,滑动,捏,以及其他类型的常见触摸相关的手势。手势识别器在追踪触摸事件方面付出了艰辛的努力,并确保他们按照正确的标准将其定位为目标手势。您可以创建手势识别器,为其分配合适的目标对象和操作方法,而不必使用跟踪触摸事件的应用程序,并使用addGestureRecognizer:方法将其安装在view上。手势识别器然后在相应手势发生时调用您的操作方法。

如果您希望直接处理触摸事件,则可以针对您的view实现以下方法,这些方法在iOS事件处理指南中有更详细的描述:

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处理触摸事件。