Animations

动画提供了用户界面在不同状态之间切换的流畅视觉过渡方式,在iOS中,动画广泛用于重新定位视图,更改视图大小,从视图层次结构中删除它们以及隐藏它们。
我们可以使用动画将反馈传达给用户或实现有趣的视觉效果,
在iOS中,创建复杂的动画不需要您编写任何绘图代码。本章中描述的所有动画技术都使用Core Animation提供的内置支持。您所要做的就是触发动画并让Core Animation处理各个帧的渲染。这使得只需几行代码即可轻松创建复杂的动画。

哪些属性可以作为动画

UIKit 和 Core Animation都支持动画,但每种技术提供的支持程度各不相同。在UIKit中,使用UIView对象执行动画。View支持一组涵盖许多常见任务的基本动画。例如,我们可以对视图属性进行动画处理或使用过渡动画将一组视图替换为另一组视图。
表4-1列出了可动画的属性 。动画制作并不意味着动画会自动发生。更改这些属性的值通常只是在没有动画的情况下立即更新属性(和视图)。要为此类更改设置动画,必须从动画block内部更改属性的值,如下所述

frame
修改此属性以相对于其超视图的坐标系更改视图的大小和位置。 (如果transform属性不包含标识转换,请改为修改边界或中心属性。)
bounds
修改此属性以更改视图的大小。
center
修改此属性以更改视图相对于其父视图坐标系的位置。
transform
修改此属性以相对于其中心点缩放,旋转或平移视图。使用此属性的转换始终在2D空间中执行。 (要执行3D变换,必须使用Core Animation为视图的图层对象设置动画。)
alpha
修改此属性以逐渐更改视图的透明度。
backgroundColor
修改此属性以更改视图的背景颜色。

视图过渡动画是除了视图控制器提供的更改方式外的另外一种使视图层次结构更改的方式。虽然我们应该使用视图控制器来管理视图层次结构,但有时我们可能希望替换全部或部分视图层次结构。在这些情况下,我们可以使用基于视图的过渡的方式来为视图的添加和删除设置动画。
在我们需要执行更加高级,或者UIView没有提供的动画的时候,就需要考虑通过view的子layer来创建Core Animation。由于视图和图层对象错综复杂地链接在一起,因此对视图图层的更改会影响视图本身。使用Core Animation,我们可以为视图的图层设置以下类型的更改动画:

* 图层的大小和位置
* 执行转换时使用的中心点
* 在3D空间中转换为图层或其子图层
* 从图层层次结构中添加或删除图层
* 图层相对于其他同级图层的Z顺序
* 图层的阴影
* 图层的边框(包括图层的边角是否为圆角)
* 在调整大小操作期间拉伸的图层部分
* 图层的不透明度
* 位于图层边界之外的子图层的剪切行为
* 图层的当前内容
* 图层的光栅化行为

在View中使用动画的方式修改属性:

为了对UIView类的属性进行动画处理,必须将这些更改包装在动画块中。术语动画块在一般意义上用于指代指定可动画变化的任何代码。在iOS 4及更高版本中,我们可以使用块对象创建动画块。在早期版本的iOS中,使用UIView类的特殊类方法标记动画块的开头和结尾。这两种技术都支持相同的配置选项,并对动画执行提供相同的控制。但是,只要有可能,推荐使用基于块的方式。
以下部分重点介绍了为视图属性的更改设置动画所需的代码。有关如何在视图集之间创建动画过渡的信息,请参阅在视图之间创建动画过渡。

使用基于块的方式启动动画

在iOS 4及更高版本中,我们可以使用基于块的类方法来启动动画。有几种基于块的方法为动画块提供不同级别的配置。这些方法罗列如下:

animateWithDuration:animations:
animateWithDuration:animations:completion:
animateWithDuration:delay:options:animations:completion:

因为这些是类方法,所以使用它们创建的动画块不会绑定到单个视图。因此,我们可以使用这些方法创建涉及更改多个视图的单个动画。例如,清单4-1显示了在一秒内淡入一个视图的同时淡出另一个视图。执行此代码时,会立即在另一个线程上启动指定的动画,以避免阻塞当前线程或应用程序的主线程。

[UIView animateWithDuration:1.0 animations:^{
firstView.alpha = 0.0;
secondView.alpha = 1.0;
}];

前面示例中的动画仅使用ease-in, ease-out曲线运行一次。如果要更改默认动画参数,则必须使用animateWithDuration:delay:options:animations:completion:方法来执行动画。此方法允许您自定义以下动画参数:

  • 在开始动画之前使用的延迟
  • 动画期间使用的定时曲线的类型
  • 动画重复的次数
  • 动画到达结尾时是否应自动反转
  • 触摸事件是否在动画正在进行时传递给视图
  • 动画是否应该中断任何正在进行的动画,或者在开始之前等待它们完成
    另一个是animateWithDuration:animations:completion:和animateWithDuration:delay:options:animations:completion:支持完成动画处理程序块的能力。我们可以使用动画完成处理程序向应用程序发出特定动画已完成的信号。通过动画完成处理程序可以将单一的动画链接在一起。
    清单4-2显示了一个动画块的示例,该动画块在第一个动画完成后启动新动画。第一次调用animateWithDuration:delay:options:animations:completion:设置淡出动画并使用一些自定义选项对其进行配置。当该动画完成时,其完成处理程序将运行并设置动画的后半部分,这会在延迟后将视图淡入。
    使用动画完成处理程序块是链接多个动画的主要方式。
- (IBAction)showHideView:(id)sender
{
// Fade out the view right away
[UIView animateWithDuration:1.0
delay: 0.0
options: UIViewAnimationOptionCurveEaseIn
animations:^{
thirdView.alpha = 0.0;
}
completion:^(BOOL finished){
// Wait one second and then fade in the view
[UIView animateWithDuration:1.0
delay: 1.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
thirdView.alpha = 1.0;
}
completion:nil];
}];
}

要点:在涉及该属性的动画正在进行时更改属性的值不会停止当前动画。相反,当前动画将继续并动画显示您刚刚分配给属性的新值。

使用Begin/Commit 方法启动动画

如果您的应用程序在iOS 3.2及更早版本中运行,则必须使用UIView的beginAnimations:context:和commitAnimations类方法来定义动画块。这些方法标记动画块的开头和结尾。在调用commitAnimations方法后,在这些方法之间更改的任何可设置动画的属性都会设置为新值。动画的执行发生在辅助线程上,以避免阻塞当前线程或应用程序的主线程。
[由于这种方式只适用于iOS 4及之前的版本,所以在该翻译文档中不做介绍,请查看英文原版文档,但是推荐使用动画block方式来取代]

嵌套动画块

我们可以通过嵌套其他动画块为当前动画块的某些部分指定不同的时序和配置选项。顾名思义,嵌套动画块是在现有动画块内创建的新动画块。嵌套动画与任何父动画同时启动,但使用自己的配置选项运行。默认情况下,嵌套动画会继承父级的持续时间和动画曲线,但即使这些选项也可以根据需要进行覆盖。
清单4-5显示了如何使用嵌套动画来更改整个组中某些动画的时间,持续时间和行为的示例。在这种情况下,两个视图被淡化为完全透明,但是另一个视图的透明度在最终被隐藏之前来回变换几次。嵌套动画块中使用的UIViewAnimationOptionOverrideInheritedCurve和UIViewAnimationOptionOverrideInheritedDuration键允许为第二个动画修改第一个动画的曲线和持续时间值。如果这些键不存在,则将使用外部动画块的持续时间和曲线。

[UIView animateWithDuration:1.0
delay: 1.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
aView.alpha = 0.0;

// Create a nested animation that has a different
// duration, timing curve, and configuration.
[UIView animateWithDuration:0.2
delay:0.0
options: UIViewAnimationOptionOverrideInheritedCurve |
UIViewAnimationOptionCurveLinear |
UIViewAnimationOptionOverrideInheritedDuration |
UIViewAnimationOptionRepeat |
UIViewAnimationOptionAutoreverse
animations:^{
[UIView setAnimationRepeatCount:2.5];
anotherView.alpha = 0.0;
}
completion:nil];

}
completion:nil];

在结合重复计数创建可逆动画时,可以考虑为重复计数指定非整数值。对于自动反转动画,动画的每个完整周期都涉及从原始值到新值的动画,然后再返回。如果希望动画以新值结束,则向重复计数添加0.5会导致动画完成以新值结束所需的额外半周期。如果不包括此半步,则动画将设置原始值的动画,然后快速捕捉到新值,这可能不是我们想要的视觉效果。

在视图之间创建动画过渡

视图转换可帮助我们隐藏以及从视图层次结构中添加,删除,隐藏或显示视图相关的突然更改。我们可以使用视图过渡来实现以下类型的更改:

  • 更改现有视图的可见子视图。如果要对现有视图进行相对较小的更改,通常可以选择此选项。
  • 使用不同的视图替换视图层次结构中的一个视图。当您想要替换跨越全部或大部分屏幕的视图层次结构时,通常会选择此选项。
    重要提示:View过渡不应与视图控制器启动的过渡动画例如模态视图控制器的显示或将新视图控制器推送到导航堆栈混淆。视图过渡仅影响视图层次结构,而视图控制器过渡会更改活动视图控制器。因此,对于视图转换,
    在启动转换时处于活动状态的视图控制器在转换完成时保持活动状态。

有关如何使用视图控制器呈现新内容的详细信息,请参阅View Controller Programming Guide for iOS文档。

修改一个View的子View

更改View的子View允许我们对View进行适度更改。例如,我们可以添加或删除子View以在两个不同状态之间切换父View。虽然它的内容现在不同但是动画完成时,会显示相同的视图。
在iOS 4及更高版本中,我们使用transitionWithView:duration:options:animations:completion:方法来启动视图的过渡动画。在传递给此方法的动画块中,通常是与显示,隐藏,添加或删除子视图相关联的更改。将动画限制到此集允许创建视图前后版本的快照图像,并在两个图像之间设置动画。但是,如果需要为其他更改设置动画,则可以在调用方法时包含UIViewAnimationOptionAllowAnimatedContent选项。包含该选项可防止视图直接创建快照并为所有更改设置动画。

清单4-6将会教你如何使用过渡动画使其看起来好像添加了新的文本输入页面的示例。在该示例中,主视图包含两个嵌入的文本视图。文本视图配置相同,但一个始终可见,而另一个始终隐藏。当用户点击按钮创建新页面时,此方法切换两个视图的可见性,从而生成一个新的空白页面,其中包含准备接受文本的空文本视图。转换完成后,视图使用私有方法保存旧页面中的文本,并重置现在隐藏的文本视图,以便以后可以重复使用。然后视图会安排它们的指针,以便在用户请求另一个新页面时可以做同样的事情

- (IBAction)displayNewPage:(id)sender
{
[UIView transitionWithView:self.view
duration:1.0
options:UIViewAnimationOptionTransitionCurlUp
animations:^{
currentTextView.hidden = YES;
swapTextView.hidden = NO;
}
completion:^(BOOL finished){
// Save the old text and then swap the views.
[self saveNotes:temp];

UIView* temp = currentTextView;
currentTextView = swapTextView;
swapTextView = temp;
}];
}

用一个不同的视图替换另一个视图
当你想要你的界面动态改变的时候,可以使用替换视图,因为该技术只是交换视图(而不是视图控制器),所以我们负责适当地设计应用程序的控制器对象。这种技术是使用一些标准过渡快速呈现新视图的方法的简要方式。
在iOS 4及更高版本中,我们可以使用transitionFromView:toView:duration:options:completion:方法在两个视图之间进行转换。实际上,该方法会从层次结构中删除第一个视图并插入另一个视图,因此如果要保留第一个视图,则应确保引用第一个视图。如果要隐藏视图而不是从视图层次结构中删除视图,请将UIViewAnimationOptionShowHideTransitionViews键作为其中一个选项传递。
清单4-8显示了在单个视图控制器管理的两个主视图之间交换所需的代码。在此示例中,视图控制器的根视图始终显示两个子视图之一(primaryView或secondaryView)。每个视图都呈现相同的内容,但以不同的方式呈现。视图控制器使用displaysPrimary成员变量(布尔值)来跟踪在任何给定时间显示的视图。翻转方向根据正在显示的视图而改变。

- (IBAction)toggleMainViews:(id)sender {
[UIView transitionFromView:(displayingPrimary ? primaryView : secondaryView)
toView:(displayingPrimary ? secondaryView : primaryView)
duration:1.0
options:(displayingPrimary ? UIViewAnimationOptionTransitionFlipFromRight :
UIViewAnimationOptionTransitionFlipFromLeft)
completion:^(BOOL finished) {
if (finished) {
displayingPrimary = !displayingPrimary;
}
}];
}

将多个动画连接在一起

UIView动画接口支持将单独的动画块连接起来,使它们先后执行而不是同时执行。连接动画块的过程取决于我们使用的是基于块的动画方法还是使用begin/commit方法:

对于基于块的动画,需要使用animateWithDuration:animations:completion: 和animateWithDuration:delay:options:animations:completion:执行任何后续动画的方法。
对于begin/commit动画,将代理对象和动画停止selector与动画相关联。将动画连接在一起的替代方法是使用具有不同延迟因子的嵌套动画,以便在不同时间开始动画。有关如何嵌套动画的更多信息,请参阅嵌套动画块

View和图层一起动画

我们的应用可以根据需要自由混合基于视图和基于图层的动画代码,但配置动画参数的过程取决于谁拥有该图层。更改视图拥有的图层与更改视图本身相同,并且应用于图层属性的任何动画都会遵循当前基于视图的动画块的动画参数。对于我们自己创建的图层,情况也是如此。
自定义图层对象忽略基于视图的动画块参数,而是使用默认的Core Animation参数。
如果要为我们自己创建的图层自定义动画参数,则必须直接使用Core Animation。通常,使用Core Animation动画图层涉及创建CABasicAnimation对象或CAAnimation的其他一些具体子类。然后,将该动画添加到相应的图层。我们可以从基于视图的动画块内部或外部应用动画。

清单4-9显示了一个同时修改视图和自定义图层的动画。此示例中的视图在其边界的中心包含一个自定义CALayer对象。该动画顺时针旋转视图,同时顺时针旋转图层。由于旋转方向相反,因此该层保持其相对于屏幕的原始方向,并且看起来不会显着旋转。但是,该图层下方的视图会旋转360度并返回其原始方向。此示例主要用于演示如何混合视图和图层动画。这种混合不应该用于需要精确定时的情况。

[UIView animateWithDuration:1.0
delay:0.0
options: UIViewAnimationOptionCurveLinear
animations:^{
// Animate the first half of the view rotation.
CGAffineTransform xform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(-180));
backingView.transform = xform;

// Rotate the embedded CALayer in the opposite direction.
CABasicAnimation* layerAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
layerAnimation.duration = 2.0;
layerAnimation.beginTime = 0; //CACurrentMediaTime() + 1;
layerAnimation.valueFunction = [CAValueFunction functionWithName:kCAValueFunctionRotateZ];
layerAnimation.timingFunction = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionLinear];
layerAnimation.fromValue = [NSNumber numberWithFloat:0.0];
layerAnimation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(360.0)];
layerAnimation.byValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(180.0)];
[manLayer addAnimation:layerAnimation forKey:@"layerAnimation"];
}
completion:^(BOOL finished){
// Now do the second half of the view rotation.
[UIView animateWithDuration:1.0
delay: 0.0
options: UIViewAnimationOptionCurveLinear
animations:^{
CGAffineTransform xform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(-359));
backingView.transform = xform;
}
completion:^(BOOL finished){
backingView.transform = CGAffineTransformIdentity;
}];
}];

如果需要在视图和基于图层的动画之间进行精确计时,建议您使用Core Animation创建所有动画。 您可能会发现使用Core Animation更容易执行某些动画。 例如,清单4-9中的基于视图的旋转需要多步序列以进行超过180度的旋转,而Core Animation部分使用旋转值函数,该函数通过中间值从头到尾旋转。

Views

由于view是我们应用与用户交互的主要对象,它承担着很多任务,我们仅仅将其中的一小部分罗列在下面:

  1. 布局和子view管理
    view 根据它和父view的关系来定义它自己的尺寸。
    view 管理着一个子view的列表
    view 可以改变子view的尺寸以及位置
    view 可以将它坐标系统的坐标,转换为其他view或者window坐标系统的坐标值。

  2. 绘制和动画
    view 管理着自身矩形区域的内容绘制
    一些view的属性可以以动画形式切换到新的值

  3. 事件处理
    view 可以接收触摸事件
    view 可以参与事件的责任链

这个章节,集中于创建,管理,绘制以及处理布局以及管理view层级关系这些内容。对于如何处理触摸事件可以查看Event Handling Guide for iOS 文档。

创建和配置View对象:

我们可以通过代码或者Interface Builder来创建view对象,然后将它集成到view的层级上。

  • 使用Interface Builder创建view对象:
    [该部分不做介绍]

  • 手动创建view对象:
    如果你想要通过编程方式创建view,我们可以使用标准的分配初始化模式,view默认的初始化方法是initWithFrame:这个方法设置了相对于它父view的初始化尺寸和位置。比如要创建一个通用的UIView对象,我们可以使用如下的代码进行创建。

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

在创建view结束后,我们必须在它可见之前将它添加到window上(或者在window的另外的view上),关于如何添加view到view层级结构可以查看Adding and Removing Subviews.

设置View的属性:

UIView有许多用于控制自身外观和行为的属性,这些属性用于控制view的尺寸和位置,view的透明度,背景颜色以及渲染行为。
所有的这些属性有自己的默认值。

下面列出了,一些很常用的属性以及列出()[https://blog.csdn.net/wzzvictory/article/details/10076323]

  • alpha, hidden, opaque
    这些属性会影响视图的不透明度。 alpha和hidden属性直接更改视图的不透明度。
    opaque属性告诉系统它应该如何组合视图。如果视图的内容完全不透明,则将此属性设置为YES,因此不会显示任何基础视图的内容。
    将此属性设置为YES可通过消除不必要的合成操作来提高性能。

  • bounds, frame, center, transform
    这些属性将会影响view的尺寸以及位置。center 和 frame属性,代表相对与父view的位置。
    frame还包括了view的尺寸。bounds定义了在自身坐标系中view的可见区域。

transform 属性用于以复杂的动画形式或者移动整个view。比如你可能使用transform来旋转或者缩放view。
如果当前的transform是不确定的transform,frame属性是不确定的,可忽略的。

关于bounds,frame,center属性可以查看The Relationship of the Frame, Bounds, and Center Properties。
关于transforms如何影响一个view可以查看 Coordinate System Transformations.

  • autoresizingMask, autoresizesSubviews
    这些属性会影响视图及其子视图的自动调整大小行为。 autoresizingMask属性控制视图如何响应其父视图边界的更改。 autoresizesSubviews属性控制是否根本调整当前视图的子视图的大小。

  • contentMode, contentStretch, contentScaleFactor

这些属性会影响视图内容的呈现行为。 contentMode和contentStretch属性确定在视图的宽度或高度更改时如何处理内容。仅当我们需要为高分辨率屏幕自定义视图的绘制行为时,才使用contentScaleFactor属性。
有关内容模式如何影响视图的详细信息,请参阅 Stretchable Views.。有关内容拉伸矩形如何影响视图的信息,请参阅可伸展视图。有关如何处理比例因子的信息,请参阅“Drawing and Printing Guide for iOS”中的“Supporting High-Resolution Screens In Views”。

  • gestureRecognizers, userInteractionEnabled, multipleTouchEnabled, exclusiveTouch
    这些属性影响你的view如何处理触摸事件,gestureRecognizers属性添加到view的手势识别,其他属性控制view支持的触摸事件。
    关于在我们view中如何处理事件请参阅Event Handling Guide for iOS.

exclusiveTouch的意思是UIView会独占整个Touch事件,具体的来说,就是当设置了exclusiveTouch的 UIView是事件的第一响应者,那么到你的所有手指离开前,其他的视图UIview是不会响应任何触摸事件的,对于多点触摸事件,这个属性就非常重要,值得注意的是:手势识别(GestureRecognizers)会忽略此属性。
列举用途:我们知道ios是没有GridView视图的,通常做法是在UITableView的cell上加载几个子视图,来模拟实现 GridView视图,但对于每一个子视图来说,就需要使用exclusiveTouch,否则当同时点击多个子视图,那么会触发每个子视图的事件。当然 还有我们常说的模态对话框。

  • backgroundColor, subviews, drawRect:, layer, (layerClass method)
    这些属性和方法可帮助我们管理视图的实际内容。对于简单视图,我们可以设置背景颜色并添加一个或多个子视图。 subviews属性本身包含一个只读的子视图列表,但有几种方法可以添加和重新排列子视图。对于具有自定义绘图行为的视图,必须覆盖drawRect:方法。
    对于更高级的内容,您可以直接使用视图的核心动画层。要为视图指定完全不同类型的图层,必须覆盖layerClass方法。
为View打标签以便日后识别:

UIView 包含了一个tag的属性,我们可以使用这个属性在整个view 层级上单独得识别view。并且在运行过程中寻找这些view。(基于tag的搜索会比在view 层级树中搜索要快得多)

为了搜索一个打了标签的view,可以使用UIView的viewWithTag: 方法。这个方法在消息接收者以及它的子view执行一个深度优先的遍历。它不会搜索父view或者层级树的其他部分。
因此如果从一个层级树的顶部调用这个方法,将会搜索整个view层级树,但是如果在某个子view上调用,那么它只会搜索view的一个子集。

创建和管理View层级:

管理view的层级是开发用户界面十分关键的部分,view的组织不但影响到我们应用的外观,还影响到我们如何响应改变和事件。例如,视图层次结构中的父子关系确定哪些对象可能处理特定的触摸事件。同样,父子关系定义每个视图如何响应界面方向更改。

图3-1显示了视图分层如何为应用程序创建所需视觉效果的示例。在Clock应用程序的情况下,视图层次结构由从不同源派生的视图的混合组成。选项卡栏和导航视图是由选项卡栏和导航控制器对象提供的特殊视图层次结构,用于管理整个用户界面的各个部分。这些条之间的所有内容都属于Clock应用程序提供的自定义视图层次结构

添加和删除子view

Interface Builder 是建立view层级最方便的一种形式,因为我们可以以图形界面的形式很直观得组织我们的view层级。
同时还可以查看view之间的关系,以及查看在运行时整个界面显示成什么样子。当使用Interface Builder,我们可以以nib文件的形式保存整个层级树。这个文件将会在必要的时候加载所需的view。
如果我们更偏向于使用手动的方式创建我们的view。我们可以通过如下代码创建初始化它们,然后将它们添加到层级树中。

  • 为了将某个子view添加到一个父view,我们可以调用 addSubview:方法,这个方法会将子view添加到父view的子view列表的尾部。
  • 如果想插入一个子view到子view列表的中间,可以使用insertSubview 方法。这个方法在消息接收者以及它的子view执行一个深度优先的遍历。它不会搜索父view或者层级树的其他部分。
    在列表中间插入子视图可视地将该视图放在列表后面的任何视图后面。
  • 为了在父view中对已经存在的子view进行排序,可以调用bringSubviewToFront:sendSubviewToBack:或者exchangeSubviewAtIndex:withSubviewAtIndex:通过这些方法会比删除然后重新插入子view来得快。
  • 如果想将一个view从它的父view中删除,可以通过调用子view的removeFromSuperview

将子视图添加到其父视图时,子视图的当前frame矩形表示其在父视图中的初始位置。默认情况下,不会剪切其frame位于其父view可见边界之外的子视图。如果希望将子视图剪切到super view的边界,则必须将superview的clipsToBounds属性显式设置为YES。
我们添加子view的地方可以是view controller的loadView 或者viewDidLoad方法,如果你使用代码形式建立view。我们可以在view controller的loadView方法中,不论我们是从nil文件中加载,或者使用代码方式创建,我们可以在viewDidLoad方法中对view进行配置

Listing 3-1 将view添加到现有的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"];
}

当你添加一个subview到另一个view,UIKit会通知父view以及子view这些改变,如果我们自己实现了自定义view,我们可以可以复写
willMoveToSuperview:, willMoveToWindow:didMoveToSuperview, didMoveToWindow
didAddSubview:willRemoveSubview:这些方法。 我们可以使用这些通知来更新与view层级相关的事件或者执行额外的任务。

在view层级树被创建出来后,我们可以通过superview 以及subviews属性来进行定位,每个view的window属性包含了view所显示在的那个window。
因为view层级树中的root view 是没有parent的,所以superview属性将会设置为nil。对于正在显示在屏幕上的view来说,window是view树的root View

隐藏view

为了隐藏一个可见的view,我们既可以通过设置hide属性为YES或者修改它的透明属性为0.0
隐藏的view不会接收系统的触摸事件,但是隐藏的view会参与view层级相关的布局操作。因此,如果你打算在隐藏后不久又立刻显示,隐藏视图通常是一个更明智的选择。

如果我们隐藏当前获得第一焦点的view,这时候view并不会自动重分配它的焦点状态,事件的第一处理者仍然传递到这个隐藏的view中,为了避免这种情况发生,我们应该在隐藏某个view的时候强制view重新分配焦点状态,
如果你想让一个view从可见到隐藏或者隐藏到可见状态,我们应该使用alpha属性,hidden不是一个动画属性,所以任何的改变都只会让它立即生效。

在view层级树中定位某个view

有两种在view层级上定位view的方式:

  1. 将指向某个相关view的指针存储到合适的位置
  2. 为某个view分配一个唯一的整数给view的tag属性。并且使用 viewWithTag:方法来定位它。

存储指向相关view的引用是定位view最常用的方式,这种方式让查找view十分方便。如果你使用Interface Builder 来创建我们的view,我们可以通过outlet连接两个在nil文件中的view。
对于我们使用代码创建的view,我们可以将这些view存储到私有成员变量中。不论是使用outlets还是私有成员变量,我们的职责就是在需要的时候retained view,不需要的时候将它们释放掉。
减少硬编码依赖,以及支持更动态化和弹性解决方案非常有用的方式。这种方式我们通过tag来定位而不是直接存储一个指向view的指针。

Tag也是一种更持久的引用view的方式。例如,如果要保存应用程序中当前可见的view列表,则应将每个可见view的tag写出到文件中。这比归档实际view对象更简单,尤其是在我们仅跟踪当前可见的view的情况下。随后加载应用程序时,我们将重新创建view并使用保存的tag列表来设置每个视图的可见性,从而将view层次结构返回到先前的状态。

变换,缩放,旋转view

每个view都有相关联的旋转,缩放view内容的仿射动画,view的变换会改变view最终渲染的外观,常用于实现滚动动画或者其他可见的效果。
UIView的transform属性包含一个CGAffineTransform结构,其中包含要应用的转换。默认情况下,此属性设置为标识转换,不会修改视图的外观。您可以随时为此属性分配新变换。例如,要将视图旋转45度,可以使用以下代码:

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

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

在view层级中转换坐标

在不同的时间,特别是在处理事件时,应用程序可能需要将坐标值从一个frame转换为另一个frame。例如,触摸事件会报告相对窗口坐标系每次触摸的位置,但视图对象通常需要视图的局部坐标系中的信息。 UIView类定义了以下用于坐标转换:

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

convert …:fromView:方法将坐标从其他视图的坐标系转换为当前视图的局部坐标系。相反,convert …:toView:方法将坐标从当前视图的局部坐标系转换为指定视图的坐标系。如果将nil指定为任何方法的参考视图,则会在包含视图的窗口的坐标系中进行转换。
除了UIView转换方法之外,UIWindow类还定义了几种转换方法。这些方法与UIView版本类似,不同之处在于这些方法不是转换到视图的局部坐标系,而是从窗口的坐标系转换。

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

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

在运行时改变view的大小和位置:

不论什么时候view的尺寸一旦改变,它的子view的尺寸和位置必须跟着改变,UIView提供了在view层级上自动和手动布局view的能力。动态布局的时候,我们可以为每个view设置父view改变的时候,它们所需要遵守的规则。手动布局,我们可以在需要的时候改变view的尺寸和位置。

为布局变化做好准备

布局变化可以在如下任何事件发生的时候发生。

  1. view边界矩形变化的时候
  2. 界面方向改变
  3. view layer相关的Core Animation sublayers 发生变化的时候
  4. 我们应用调用了setNeedsLayout 或者 layoutIfNeeded来强制布局
  5. 我们应用调用了view layer的setNeedsLayout方法来强制布局
使用自动调整规则自动处理布局更改

当您更改视图的大小时,通常需要更改任何嵌入的子视图的位置和大小以考虑其父级的新大小。 superview的autoresizesSubviews属性确定子视图是否完全调整大小。如果此属性设置为YES,则视图使用每个子视图的autoresizingMask属性来确定如何调整子视图的大小和位置。对任何子视图的大小更改会触发其嵌入式子视图的类似布局调整。
对于视图层次结构中的每个视图,将该视图的autoresizingMask属性设置为适当的值是处理自动布局更改的重要部分。表3-2列出了可应用于给定视图的自动调整大小选项,并描述了它们在布局操作期间的效果。您可以使用OR运算符组合常量,或者在将它们分配给autoresizingMask属性之前将它们添加到一起。如果使用Interface Builder来组合视图,则可以使用“自动调整大小”检查器来设置这些属性。

UIViewAutoresizingNone
当前view不会自动布局
UIViewAutoresizingFlexibleHeight
在父view调整高度之后,view的高度会随之变化
UIViewAutoresizingFlexibleWidth
在父view调整宽度之后,view的宽度会随之变化
UIViewAutoresizingFlexibleLeftMargin
与父view的左边缘会随着变宽或者变窄,如果不加这个属性,那么当前view和父view的左边距将会保持一个固定的值
UIViewAutoresizingFlexibleRightMargin
与父view的右边缘会随着变宽或者变窄,如果不加这个属性,那么当前view和父view的右边距将会保持一个固定的值
UIViewAutoresizingFlexibleBottomMargin
与父view的底部边缘会随着变宽或者变窄,如果不加这个属性,那么当前view和父view的底部边距将会保持一个固定的值
UIViewAutoresizingFlexibleTopMargin
与父view的顶部边缘会随着变宽或者变窄,如果不加这个属性,那么当前view和父view的顶部边距将会保持一个固定的值

手动调整视图的布局

每当视图大小发生变化时,UIKit都会应用该视图子视图的自动调整行为,然后调用视图的layoutSubviews方法让它进行手动更改。当自动调整行为本身不能产生所需的结果时,您可以在自定义视图中实现layoutSubviews方法。您对此方法的实现可以执行以下任何操作:
调整任何直接子视图的大小和位置。
添加或删除子视图或核心动画图层。
通过调用setNeedsDisplay或setNeedsDisplayInRect:方法强制重绘子视图。

编写布局代码时,请务必通过以下方式测试代码:

更改视图的方向以确保布局在所有支持的界面方向中看起来都正确。
确保您的代码适当地响应状态栏高度的变化。当电话呼叫处于活动状态时,状态栏高度会增加,当用户结束呼叫时,状态栏的大小会减小。
有关自动调整行为如何影响视图大小和位置的信息,请参阅使用自动调整规则自动处理布局更改。有关如何实现平铺的示例,请参阅ScrollViewSuite示例

在运行时修改View

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

  • 在视图控制器中:
    视图控制器必须在显示它们之前创建其视图。它可以从nib文件加载视图或以编程方式创建它们。当不再需要这些视图时,它会处理它们。
    当设备更改方向时,视图控制器可能会调整视图的大小和位置以进行匹配。作为对新方向的调整的一部分,它可能隐藏一些观点并展示其他观点。
    当视图控制器管理可编辑内容时,它可能会在进入和退出编辑模式时调整其视图层次结构。例如,它可能会添加额外的按钮和其他控件,以便于编辑其内容的各个方面。这可能还需要调整任何现有视图的大小以适应额外的控件。
  • 在动画块中:
    如果要在用户界面中的不同视图集之间进行切换,则会隐藏一些视图并在动画块内显示其他视图。
    实现特殊效果时,可以使用动画块来修改视图的各种属性。例如,要更改视图大小的更改,您可以更改其框架矩形的大小。
  • 其他方法:
    触摸事件或手势发生时,您的界面可能会通过加载一组新视图或更改当前视图集来响应。有关处理事件的信息,请参阅iOS事件处理指南。
    当用户与滚动视图交互时,大的可滚动区域可能隐藏并显示切片子视图。有关支持可滚动内容的更多信息,请参阅适用于iOS的“滚动视图编程指南”。
    当键盘出现时,您可能会重新定位或调整视图大小,使它们不位于键盘下方。有关如何与键盘交互的信息,请参阅iOS文本编程指南。

View controllers 是启动视图更改的常见位置。因为view controller管理着与展示内容相关的view 层级,它是发生在这些view上面所有事件的最终响应者,在加载它的view或者处理方向改变的时候,view controller 可以添加新的view,隐藏或者替换已经存在的view,如果你的实现支持编辑view的内容,UIViewController 的setEditing:animated:方法提供了一个可以在可变和不可变之间进行切换的地方。
动画block 是另一个启动view相关更改的地方。UIView类中内置的动画支持使得对视图属性的更改进行动画处理变得容易。您还可以使用transitionWithView:duration:options:animations:completion:或transitionFromView:toView:duration:options:completion:用于替换新视图的整个视图集的方法。

与核心动画层交互

每个视图对象都有一个专用的Core Animation层,用于管理屏幕上视图内容的显示和动画。虽然我们可以对视图对象进行大量操作,但也可以根据需要直接使用相应的图层对象。视图的图层对象存储在视图的layer属性中

更改与视图关联的图层类

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

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

每个视图在其初始化过程的早期调用其layerClass方法,并使用返回的类来创建其图层对象。此外,视图始终将自己指定为其图层对象的委托。此时,视图拥有其图层,视图和图层之间的关系不得更改。您还必须不分配与任何其他图层对象的委托相同的视图。更改视图的所有权或委托关系将导致绘图问题和应用程序中的潜在崩溃。

在视图中嵌入图层对象

如果你更喜欢使用图层对象而不是视图,则可以根据需要将自定义图层对象合并到视图层次结构中。自定义图层对象是CALayer的实例。我们通常以编程方式创建自定义图层,并使用Core Animation合并它们。自定义图层不会接收事件或参与响应者链,但会根据核心动画规则自行绘制并响应其父视图或图层中的大小更改。
清单3-2显示了视图控制器中viewDidLoad方法的一个示例,该方法创建自定义图层对象并将其添加到其根视图中。该图层用于显示动画的静态图像。我们可以将它添加到视图的底层,而不是将图层添加到视图本身。

- (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

如果系统标准的view不能满足我们的需求,我们可以定义一个自定义view,自定义view能够完全控制控件的外形以及如何处理与所显示内容的交互。
如果你使用OpenGL ES来绘制我们的界面,我们需要使用GLKView而不是UIView.

实现自定义视图的清单

自定义视图的任务是呈现内容并管理与该内容的交互。但是自定义视图的成功实现不仅仅涉及绘制和处理事件。以下清单包括在实现自定义视图时可以覆盖的更重要的方法(以及我们可以提供的行为):

为视图定义适当的初始化方法:
如果你想以编程方式创建的视图,需要覆盖initWithFrame:方法或定义自定义初始化方法。
如果我们想从nib文件加载的视图,需要覆盖initWithCoder:方法。使用此方法初始化视图并将其置于已知状态。
实现dealloc方法来处理任何自定义数据的清理。
要处理任何自定义绘图,需要覆盖drawRect:方法并在那里进行绘制。
设置视图的autoresizingMask属性以定义其自动调整行为。
如果您的视图类管理一个或多个完整子视图,请执行以下操作:
在视图的初始化序列中创建这些子视图。
在创建时设置每个子视图的autoresizingMask属性。
如果您的子视图需要自定义布局,请覆盖layoutSubviews方法并在那里实现布局代码。
要处理基于触摸的事件,请执行以下操作:
使用addGestureRecognizer:方法将任何合适的手势识别器附加到视图。
对于想自己处理触摸的情况,请覆盖touchesBegan:withEvent:,touchesMoved:withEvent:,touchesEnded:withEvent:和touchesCancelled:withEvent:方法。
如果希望视图的打印版本与屏幕版本不同,请实现drawRect:forViewPrintFormatter:方法。有关如何在视图中支持打印的详细信息,请参阅适用于iOS的“绘图和打印指南”。
除了重写方法之外,需要记住,我们可以使用视图的现有属性和方法执行许多操作。例如,contentMode和contentStretch属性允许您更改视图的最终渲染外观,并且可能更适合自己重新绘制内容。除了UIView类本身之外,还可以直接或间接配置视图的基础CALayer对象的许多方面。您甚至可以更改图层对象本身的类

初始化自定义视图

我们定义的每个新视图对象都应包含自定义initWithFrame:initializer方法。该方法负责在创建时初始化类并将视图对象置于已知状态。在代码中以编程方式创建视图实例时,可以使用此方法。
清单3-3显示了标准initWithFrame:方法的基本实现。此方法首先调用方法的继承实现,然后在返回初始化对象之前初始化类的实例变量和状态信息。传统上首先执行调用继承的实现,以便在出现问题时,可以中止自己的初始化代码并返回nil

- (id)initWithFrame:(CGRect)aRect {
self = [super initWithFrame:aRect];
if (self) {
// setup the initial properties of the view
...
}
return self;
}
实现绘图代码

对于需要进行自定义绘制的视图,需要覆盖drawRect:方法并在那里进行绘制。建议仅将自定义绘图作为最后的手段。通常,如果可以使用其他视图来展示您的内容,那么这是首选。
drawRect:方法的实现应该只做一件事:绘制我们的内容。此方法不是更新应用程序的数据结构或执行与绘图无关的任何任务的地方。它应配置绘图环境,绘制内容,并尽快退出。如果你的drawRect:方法可能经常被调用,你应该尽一切可能优化你的绘图代码,并在每次调用方法时尽可能少地绘制。
在调用视图的drawRect:方法之前,UIKit会为您的视图配置基本绘图环境。具体来说,它会创建图形上下文并调整坐标系和裁剪区域以匹配视图的坐标系和可见边界。因此,在调用drawRect:方法时,您可以使用UIKit和Core Graphics等绘图技术开始绘制内容。您可以使用UIGraphicsGetCurrentContext函数获取指向当前图形上下文的指针。

- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect myFrame = self.bounds;

// Set the line width to 10 and inset the rectangle by
// 5 pixels on all sides to compensate for the wider line.
CGContextSetLineWidth(context, 10);
CGRectInset(myFrame, 5, 5);

[[UIColor redColor] set];
UIRectFrame(myFrame);
}

如果您知道视图的绘图代码始终覆盖视图的整个表面并且内容不透明,则可以通过将视图的opaque属性设置为YES来提高系统性能。将视图标记为不透明时,UIKit会避免绘制位于视图后面的内容。这不仅减少了绘图所花费的时间,而且最大限度地减少了将视图与其他内容合成所必须完成的工作。但是,只有在您知道视图的内容完全不透明时,才应将此属性设置为YES。如果您的视图无法保证其内容始终不透明,则应将该属性设置为NO。
另一种提高绘图性能的方法,特别是在滚动期间,是将视图的clearsContextBeforeDrawing属性设置为NO。当此属性设置为YES时,UIKIt会在调用方法之前使用透明黑色自动填充要由drawRect:方法更新的区域。将此属性设置为NO可消除该填充操作的开销,但会给应用程序带来负担,以填充传递给带有内容的drawRect:方法的更新矩形

响应事件

View对象是响应者对象 - UIResponder类的实例,因此能够接收触摸事件。当发生触摸事件时,UIWindow将相应的事件对象调度到发生触摸的视图。如果视图对某个事件不感兴趣,可以忽略它或将其传递给响应者链以由另一个对象处理。
除了直接处理触摸事件之外,view还可以使用手势识别器来检测taps, swipes, pinches以及其他类型的常见触摸相关手势。我们可以创建手势识别器,为其分配适当的目标对象和操作方法,并使用addGestureRecognizer:方法将其安装在视图上,然后,手势识别器会在相应的手势发生时调用您的操作方法。
如果您希望直接处理触摸事件,可以为视图实现以下方法,这些方法在iOS事件处理指南中有更详细的描述:

touchesBegan:withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
touchesCancelled:withEvent:

View的默认行为是一次只响应一次触摸。如果用户放下第二根手指,系统将忽略触摸事件,并且不会将其报告给我们的View。如果我们想要从视图的事件处理程序方法中跟踪多指手势,需要通过将视图的multipleTouchEnabled属性设置为YES来启用多点触控事件。
某些视图最初会完全禁用事件处理。我们可以通过更改视图的userInteractionEnabled属性的值来控制视图是否能够接收触摸事件。我们可以暂时将此属性设置为NO,以防止用户在长时间操作挂起时操作视图的内容。要防止事件到达任何视图,还可以使用UIApplication对象的beginIgnoringInteractionEvents和endIgnoringInteractionEvents方法。这些方法影响整个应用程序的事件传递,而不仅仅是单个视图。

在处理触摸事件时,UIKit使用UIView的hitTest:withEvent:和pointInside:withEvent:方法来确定触摸事件是否发生在给定视图的边界内。虽然很少需要覆盖这些方法,但我们可以通过这种方式实现视图的自定义触摸行为。例如,我们可以覆盖这些方法以防止子视图处理触摸事件

在视图之后做对应的清理工作

如果我们为视图类分配过任何内存,存储对任何自定义对象的引用,或者保存在发布视图时必须释放的资源,则必须实现dealloc方法。当视图的保留计数达到零并且是时候取消分配视图时,系统会调用dealloc方法。对此方法的实现应该释放视图所持有的任何对象或资源,然后调用继承的实现,如清单3-5所示。我们不应该使用此方法来执行任何其他类型的任务。

- (void)dealloc {
// Release a retained UIColor object
[color release];

// Call the inherited implementation
[super dealloc];
}

每个iOS应用至少有一个window,有些应用甚至可能有多个,每个window有多个任务:

  1. 它是我们应用可见部分的容器
  2. 它在传递触摸事件到view以及其他应用对象中起了关键作用
  3. 它作用于应用程序的视图控制器,以方便更改设备方向。

在iOS,window 没有title bar,close box,以及任意可见的装饰。一个window仅仅是一个用于装载一个或者多个view的空容器,
同时,并不能通过展示新的window来修改它们的内容。当我们想改变展示当内容的时候,我们可以改变我们window的最前端的view。

绝大多数的iOS应用在整个生命周期中只使用一个window。这个window占据着设备的整个界面。它一般在应用生命周期的早期从应用的main nib文件加载,或者代码创建而来。
但是,如果一个应用支持支持一个视频输出的外部显示。我们可以创建一个额外的window来展示在外部显示的内容。

涉及到Windows的任务:

  • 对于多数应用来说,应用和它的window交互的唯一时间点就是启动应用的时候应用会创建它的window。
    但是我们可以使用我们应用的window处理一些应用相关的任务:
    使用window来转换点和矩形到window到坐标系统。比如,你有一个在window坐标系统上的一个值,你可能想在使用它的时候转换到特定view的坐标系统。
    关于如何转换坐标系统,可以查看 Converting Coordinates in the View Hierarchy.

  • 使用window通知来跟踪window相关的改变
    Window 会在它们显示或者隐藏或者当它们接收或者重新分配关键状态的时候产生通知。我们可以使用这些通知来执行我们应用的其他部分任务。对于这部分信息可以查看:
    Monitoring Window Changes.

创建配置Window

我们可以手动或者使用Interface Builder来创建应用的main window。我们可以在启动时间创建window,并且在我们应用的delegate中保持着它的引用。如果你的应用需要创建额外的window。我们必须在需要它们的时候进行懒加载创建。例如,如果我们的应用支持在一个外部设备上显示,那么window必须在设备连接后再去创建它。

不论你的应用在前台还是后台启动,我们必须在应用的启动的时候创建我们的main window。创建和配置window不是一个耗时耗资源的操作,但是如果你的应用直接在后台启动,你应该在你的应用进入前台前避免让window可见。

通过Interface Builder 创建 Window [这部分请见原文档]
通过编程创建Window

如果你偏向通过代码来创建应用的main window。可以将下面的代码放置到你的应用代理的 application:didFinishLaunchingWithOptions:
方法中。

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

在前面的例子中,self.window是你应用delegate中声明来存储window引用的一个属性。如果你正要为一个外部显示的window创建一个window,你应该将它赋给另外的一个变量并且指定用于呈现显示内容的非main UIScreen对象的边界。
当创建window的时候,我们应该将window的尺寸设置为设备屏幕的整个边界。而不应该减小window的尺寸来容纳状态栏,或者其他内容。状态栏总是悬浮在window的顶部。如果你正在使用view controller,那么view controller 应该动态处理我们view的尺寸。

添加内容到Window

每个window一般都有一个唯一都root view对象,用于包含所有用于显示其他内容都view.使用唯一都root view 简化了处理我们界面改变的处理。要显示新的内容我们要做的就是替换root view。要添加一个view到应用的window可以通过 addSubview:方法。比如要安装一个由某个view controller管理着的view,我们可以通过下面的代码来实现:

[window addSubview:viewController.view];

除了上面的方式,我们可以在我们的nib文件中修改window rootViewController属性的值。这个属性提供了一种通过nil文件便捷的方式配置window的root view而不是代码方式。
如果这个属性在window从它的nib文件中加载后设置,UIKit将会自动从相关联的View Controller中加载view作为window的root view。 这个属性只是用于装载root view而不用于和view controller进行交互。
你可以使用你想要的任意view作为window的root view。这取决于你的界面交互设计,root view可以是一个用于作为一个或者更多子view容器的通用UIView对象,root view 可以是标准的系统控件或者我们自己自定义的view。

当我们配置window的root view的时候,也需要负责设置它的初始尺寸和位置。对于不包含状态栏,或者透明的状态栏的应用,我们需要将view的尺寸设置为匹配整个window的尺寸。对于有状态栏的应用,我们需要将view放置到状态栏下,并且相应减小view的尺寸,从它的高度上减去状态栏的高度。

注意:如果window的root view 是一个容器view controller。我们不需要设置view的初始尺寸,容器view controller将会自动根据是否显示状态栏来动态修改它的尺寸。

修改window的等级:

每个UIWindow对象都有一个可配置的windowLevel属性,这个属性决定了当前window相对于其他的window如何放置。
绝大部分而言我们不应该修改应用window的这个属性。新建的window在创建的时候会被自动分配一个normal window等级给它。
normal window等级意味着展示一个应用相关的内容。更高级的window等级预留给需要显示悬浮在应用内容之上的信息。比如系统的状态栏,或者警告弹窗,虽然我们可以将window设置到这些等级之上,但是系统通常在我们使用某个界面的时候自动为我们设置好它们的等级。比如,我们显示或者隐藏状态栏的时候,再或者我们显示一个警告弹窗的时候,系统将会自动创建一个必须的window来显示它们的item。

跟踪window的改变:

如果你想跟踪我们应用内部window的显示或者消失。我们可以通过监听window相关的通知:

UIWindowDidBecomeVisibleNotification
UIWindowDidBecomeHiddenNotification
UIWindowDidBecomeKeyNotification
UIWindowDidResignKeyNotification

这些通知是为响应应用程序窗口中的程序更改而提供的。 因此,当我们应用程序显示或隐藏窗口时,会相应地传递UIWindowDidBecomeVisibleNotification和UIWindowDidBecomeHiddenNotification通知。
当我们的应用程序进入后台执行状态时便不会传递这些通知。 即使我们的应用程序在后台时屏幕上没有显示我们应用的窗口,但在应用程序的上下文中仍然可以看到它。

UIWindowDidBecomeKeyNotification和UIWindowDidResignKeyNotification通知可帮助您的应用程序跟踪哪个窗口是关键窗口 - 即哪个窗口当前正在接收键盘事件和其他非触摸相关事件。 触摸事件被传递到发生触摸的窗口,而没有相关坐标值的事件将被传递到应用程序的关键窗口。 一次只可能有一个窗口是key window。

在外部显示器中展示内容:

为了在外部显示中显示内容,我们必须为我们的应用创建一个额外的window,并且和代表外部显示器的screen 对象相关联。新的window正常情况下是和main window相关联的。
改变window的相关screen对象,将会导致window的内容重定向到相应的显示器,一旦window和相关的screen关联起来,
我们就可以在上面添加和显示view。就像在我们应用main screen上面操作一样。

UIScreen 类维护着一个代表可用硬件设备的screen对象,正常情况下,在任何iOS设备中,只有一个screen对象表示主设备。
但是支持连接外界显示器的设备中,可以有额外的screen对象。支持外接显示器的设备,包括那些拥有Retina显示屏的iPhone 和 iPod touch 设备
以及iPad。iPhone 3GS这类旧设备是不支持外部显示器的。

下面是将内容显示到外部显示器的基本步骤:

  1. 在应用启动的时候,注册screen连接和断开连接的通知
  2. 当需要在外部显示器显示内容的时候,创建并配置一个window
  • 使用UIScreen的screens 属性来获取代表外部显示器的screen对象。
  • 创建一个UIWindow对象,设置它的尺寸
  • 将screen对象赋给window的screen属性
  • 调整screen对象的分辨率来适应我们要呈现的内容
  • 添加view到window上。
  1. 显示window并更新它。
处理Screen连接和断开连接到通知:

Screen连接和断开连接通知对于正常处理外部显示器的更改至关重要。 当用户连接或断开显示器时,系统会向应用程序发送适当的通知。
我们应该使用这些通知来更新应用程序状态,并创建或释放与外部显示器关联的窗口。

关于连接和断开连接通知要记住的重要事项是:即使应用程序在后台暂停,它们也可以随时出现。 因此,最好观察来自对象的通知,该对象将在应用程序运行时期间存在,
例如应用程序委托。 如果应用程序被挂起,通知将排队,直到应用程序退出挂起状态并开始在前台或后台运行。

Listing 2-1 Registering for screen connect and disconnect notifications


- (void)setupScreenConnectionNotificationHandlers
{
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];

[center addObserver:self selector:@selector(handleScreenConnectNotification:)
name:UIScreenDidConnectNotification object:nil];
[center addObserver:self selector:@selector(handleScreenDisconnectNotification:)
name:UIScreenDidDisconnectNotification object:nil];
}

如果外部显示器连接到设备的时候我们的应用处于活动状态,我们应该为这个显示器创建另外一个window。
并填充要显示的内容,这内容不需要是最终要显示的内容,比如,如果你的应用还没准备使用额外的显示器,
我们可以在上面放置一个占位的视图,如果你不为这个screen创建一个window或者,如果你创建了一个window但是你没有显示它,
这时候外接的显示器上将会显示黑屏。

- (void)handleScreenConnectNotification:(NSNotification*)aNotification
{
UIScreen* newScreen = [aNotification object];
CGRect screenBounds = newScreen.bounds;

if (!_secondWindow)
{
_secondWindow = [[UIWindow alloc] initWithFrame:screenBounds];
_secondWindow.screen = newScreen;

// Set the initial UI for the window.
[viewController displaySelectionInSecondaryWindow:_secondWindow];
}
}

- (void)handleScreenDisconnectNotification:(NSNotification*)aNotification
{
if (_secondWindow)
{
// Hide and then delete the window.
_secondWindow.hidden = YES;
[_secondWindow release];
_secondWindow = nil;

// Update the main screen based on what is showing here.
[viewController displaySelectionOnMainScreen];
}

}
为外部显示器配置一个window:
- (void)checkForExistingScreenAndInitializeIfPresent
{
if ([[UIScreen screens] count] > 1)
{
// Associate the window with the second screen.
// The main screen is always at index 0.
UIScreen* secondScreen = [[UIScreen screens] objectAtIndex:1];
CGRect screenBounds = secondScreen.bounds;

_secondWindow = [[UIWindow alloc] initWithFrame:screenBounds];
_secondWindow.screen = secondScreen;

// Add a white background to the window
UIView* whiteField = [[UIView alloc] initWithFrame:screenBounds];
whiteField.backgroundColor = [UIColor whiteColor];

[_secondWindow addSubview:whiteField];
[whiteField release];

// Center a label in the view.
NSString* noContentString = [NSString stringWithFormat:@"<no content>"];
CGSize stringSize = [noContentString sizeWithFont:[UIFont systemFontOfSize:18]];

CGRect labelSize = CGRectMake((screenBounds.size.width - stringSize.width) / 2.0,
(screenBounds.size.height - stringSize.height) / 2.0,
stringSize.width, stringSize.height);

UILabel* noContentLabel = [[UILabel alloc] initWithFrame:labelSize];
noContentLabel.text = noContentString;
noContentLabel.font = [UIFont systemFontOfSize:18];
[whiteField addSubview:noContentLabel];

// Go ahead and show the window.
_secondWindow.hidden = NO;
}
}

在显示窗口之前,应始终将屏幕与窗口关联。 虽然可以更改当前可见的窗口的屏幕,但这样做是一项昂贵的操作,应该避免。
一旦显示外部屏幕的窗口,您的应用程序就可以像任何其他窗口一样开始更新它。 您可以根据需要添加和删除子视图,更改子视图的内容,为视图添加动画更改,并根据需要使其内容无效。

配置外部显示器的显示器模式:

根据您的内容,您可能希望在将窗口与其关联之前更改屏幕模式。 许多屏幕支持多种分辨率,其中一些使用不同的像素长宽比。 屏幕对象默认使用最常用的屏幕模式,但您可以将该模式更改为更适合您的内容的模式。 例如,如果您使用OpenGL ES实现游戏并且您的纹理设计为640 x 480像素的屏幕,
则可能会更改具有更高默认分辨率的屏幕的屏幕模式。如果计划使用默认屏幕模式以外的屏幕模式,则应在将屏幕与窗口关联之前将该模式应用于UIScreen对象。 UIScreenMode类定义单个屏幕模式的属性。 您可以从其availableModes属性中获取屏幕支持的模式列表,并遍历列表以查找符合您需求的模式。

更多的ScreenMode 可以查看如下的链接
https://developer.apple.com/documentation/uikit/uiscreenmode?language=objc

关于Windows和Views

在iOS开发中,我们使用window和view在界面上呈现我们应用的内容。Windows自身没有可见的内容,只是为我们应用上的view提供一个基本的容器,
View用于填充Window想要填充部分的内容。比如你可以用view来显示图片,文本,形状或者它们之间的一些组合。你也可以使用view来组织和管理其他的view。

概览

每个应用至少拥有一个window和一个用于呈现内容的view。UIKit和其他的系统框架提供预先定义好的用于呈现我们想要呈现内容的view。
这些view从简单的按钮,文本标签,到复杂到table view,picker view,scroll view。如果这些预先定义好的view还不能满足你的需求,你还可以通过自定义view,绘制和管理自身的事件。

View 管理我们应用上的可见内容

view 是UIView或者它子类的一个实例,它管理着我们应用window上的一个矩形区域。View负责绘制这个区域的内容,管理这个区域里面的多点触控事件,以及它子view的布局。
view的绘制涉及到使用例如Core Graphics, OpenGL ES 或者UIKit来绘制view矩形区域里面的形状,图像和文本
view 通过手势识别或者直接处理触摸事件来相应自身矩形区域的事件。在view层级中,父view负责安放和计算它们子view的尺寸,这种能够动态修改子view位置和尺寸的能力,能够让我们的view能够适应动态改变的条件。
比如界面旋转和动画。
我们可以考虑将view作为建立我们界面的块,而不是使用单独一个view来呈现所有的内容。我们经常会使用几个view来构建一个view的层级结构。
在view层级结构上的view呈现我们界面上的一部分内容。比如UIKit有特定的view用于呈现image,text,以及其他类型的内容。

Window 协调我们View的显示

Window 是 UIWindow的一个实例,它负责处理我们应用界面的总体呈现。
Windows使用视图(及其拥有的视图控制器)来管理可见视图层次结构的交互和更改。绝大多数而言,我们应用的window是不会改变的。在window创建后,它自身保持不变
只有在它上面显示的view不断变化,每个应用在设备的main screen至少有一个window来显示我们应用的交互界面。如果外部显示器连接到设备,应用程序也可以创建第二个窗口
以在该屏幕上显示内容。

动画用于提供呈现用户界面修改的可见反馈

动画提供了一种用户界面层级结构发生改变的一种可见的反馈。系统定义了标准的动画,用于呈现模态view,以及在不同组view之间进行转换。
可是view的很多属性也可以直接动画。比如通过动画我们可以改变view的透明度,它在界面上的位置,它的尺寸,它的背景颜色,或者其他的属性
如果你直接作用于view的Core Animation layer我们还可以呈现更多的动画。

参考更多:

由于view是一个复杂的对象,不可能一个文档就能覆盖它方方面面的内容,下面的文档也可以帮助你了解管理view和我们用户界面其他方面的内容。

  1. View controller 是管理我们应用view的一个非常重要的部分,它将所有的view都归于一个层级结构以便在界面上呈现这些view的内容。
    更多关于view controller的信息以及它所扮演的角色可以查看View Controller Programming Guide for iOS.

  2. View 我们应用是手势和触摸事件的主要接收者,更多关于使用手势识别和处理处理触摸事件的信息,可以查看Event Handling Guide for iOS.

  3. 自定义view必须用到绘画和渲染技术,关于如何使用这些技术来绘制我们的view可以查看 Drawing and Printing Guide for iOS.
    文档。

  4. 标准的动画可能不那么高效,我们可以使用Core Animation来替代,关于如何使用Core Animation来实现动画可以查看Core Animation Programming Guide.

一:关于ReactiveCocoa的知识点

1:RACSigner基础知识点


信号类(RACSiganl),只是表示当数据改变时,信号内部会发出数据,它本身不具备发送信号的能力,而是交给内部一个订阅者去发出。

默认一个信号都是冷信号,也就是值改变了,也不会触发,只有订阅了这个信号,这个信号才会变为热信号,值改变了才会触发。

如何订阅信号:调用信号RACSignal的subscribeNext就能订阅

常见的操作方法:


flattenMap map 用于把源信号内容映射成新的内容。

concat 组合 按一定顺序拼接信号,当多个信号发出的时候,有顺序的接收信号

then 用于连接两个信号,当第一个信号完成,才会连接then返回的信号。

merge 把多个信号合并为一个信号,任何一个信号有新值的时候就会调用

zipWith 把两个信号压缩成一个信号,只有当两个信号同时发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发压缩流的next事件。

combineLatest:将多个信号合并起来,并且拿到各个信号的最新的值,必须每个合并的signal至少都有过一次sendNext,才会触发合并的信号。

reduce聚合:用于信号发出的内容是元组,把信号发出元组的值聚合成一个值

filter:过滤信号,使用它可以获取满足条件的信号.

ignore:忽略完某些值的信号.

distinctUntilChanged:当上一次的值和当前的值有明显的变化就会发出信号,否则会被忽略掉。

take:从开始一共取N次的信号

takeLast:取最后N次的信号,前提条件,订阅者必须调用完成,因为只有完成,就知道总共有多少信号.

takeUntil:(RACSignal *):获取信号直到某个信号执行完成

skip:(NSUInteger):跳过几个信号,不接受。

switchToLatest:用于signalOfSignals(信号的信号),有时候信号也会发出信号,会在signalOfSignals中,获取signalOfSignals发送的最新信号。

doNext: 执行Next之前,会先执行这个Block

doCompleted: 执行sendCompleted之前,会先执行这个Block

timeout:超时,可以让一个信号在一定的时间后,自动报错。

interval 定时:每隔一段时间发出信号

delay 延迟发送next

retry重试 :只要失败,就会重新执行创建信号中的block,直到成功.

replay重放:当一个信号被多次订阅,反复播放内容

throttle节流:当某个信号发送比较频繁时,可以使用节流,在某一段时间不发送信号内容,过了一段时间获取信号的最新内容发出。

2:RACSubject基础知识点

RACSubject:信号提供者,自己可以充当信号,又能发送信号  使用场景:通常用来代替代理,有了它,就不必要定义代理了

RACSubject使用步骤
1.创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
2.订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
3.发送信号 sendNext:(id)value

RACSubject:底层实现和RACSignal不一样。
1.调用subscribeNext订阅信号,只是把订阅者保存起来,并且订阅者的nextBlock已经赋值了。
2.调用sendNext发送信号,遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。

RACSubject实例进行map操作之后, 发送完毕一定要调用-sendCompleted, 否则会出现内存泄漏; 而RACSignal实例不管是否进行map操作, 不管是否调用-sendCompleted, 都不会出现内存泄漏.
原因 : 因为RACSubject是热信号, 为了保证未来有事件发生的时候, 订阅者可以收到信息, 所以需要对持有订阅者!

3:RACSequence基础知识点


RACSequence:RAC中的集合类,用于代替NSArray,NSDictionary,可以使用它来快速遍历数组和字典

通过RACSequence对数组进行操作
这里其实是三步
第一步: 把数组转换成集合RACSequence numbers.rac_sequence
第二步: 把集合RACSequence转换RACSignal信号类,numbers.rac_sequence.signal
第三步: 订阅信号,激活信号,会自动把集合中的所有值,遍历出来。

4:RACCommand基础知识点


RACCommand:RAC中用于处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程

一、RACCommand使用步骤:
1.创建命令 initWithSignalBlock:(RACSignal * (^)(id input))signalBlock
2.在signalBlock中,创建RACSignal,并且作为signalBlock的返回值
3.执行命令 - (RACSignal *)execute:(id)input

二、RACCommand使用注意:
1.signalBlock必须要返回一个信号,不能传nil.
2.如果不想要传递信号,直接创建空的信号[RACSignal empty];
3.RACCommand中信号如果数据传递完,必须调用[subscriber sendCompleted],这时命令才会执行完毕,否则永远处于执行中。
4.RACCommand需要被强引用,否则接收不到RACCommand中的信号,因此RACCommand中的信号是延迟发送的。

三、RACCommand设计思想:内部signalBlock为什么要返回一个信号,这个信号有什么用。
1.在RAC开发中,通常会把网络请求封装到RACCommand,直接执行某个RACCommand就能发送请求。
2.当RACCommand内部请求到数据的时候,需要把请求的数据传递给外界,这时候就需要通过signalBlock返回的信号传递了。

四、如何拿到RACCommand中返回信号发出的数据。
1.RACCommand有个执行信号源executionSignals,这个是signal of signals(信号的信号),意思是信号发出的数据是信号,不是普通的类型。
2.订阅executionSignals就能拿到RACCommand中返回的信号,然后订阅signalBlock返回的信号,就能获取发出的值。

五、监听当前命令是否正在执行executing

六、使用场景,监听按钮点击,网络请求

5:RACMulticastConnection基础知识点


RACMulticastConnection:用于当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的block,造成副作用,可以使用这个类处理
使用注意:RACMulticastConnection通过RACSignal的-publish或者-muticast:方法创建.

RACMulticastConnection使用步骤:
1.创建信号 + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
2.创建连接 RACMulticastConnection *connect = [signal publish];
3.订阅信号,注意:订阅的不在是之前的信号,而是连接的信号。 [connect.signal subscribeNext:nextBlock]
4.连接 [connect connect]

RACMulticastConnection底层原理:
1.创建connect,connect.sourceSignal -> RACSignal(原始信号) connect.signal -> RACSubject
2.订阅connect.signal,会调用RACSubject的subscribeNext,创建订阅者,而且把订阅者保存起来,不会执行block。
3.[connect connect]内部会订阅RACSignal(原始信号),并且订阅者是RACSubject
3.1.订阅原始信号,就会调用原始信号中的didSubscribe
3.2 didSubscribe,拿到订阅者调用sendNext,其实是调用RACSubject的sendNext
4.RACSubject的sendNext,会遍历RACSubject所有订阅者发送信号。
4.1 因为刚刚第二步,都是在订阅RACSubject,因此会拿到第二步所有的订阅者,调用他们的nextBlock


需求:假设在一个信号中发送请求,每次订阅一次都会发送请求,这样就会导致多次请求。
解决:使用RACMulticastConnection就能解决.

6:RAC结合UI一般事件


rac_signalForSelector : 代替代理

rac_valuesAndChangesForKeyPath: KVO

rac_signalForControlEvents:监听事件

rac_addObserverForName 代替通知

rac_textSignal:监听文本框文字改变

rac_liftSelector:withSignalsFromArray:Signals:当传入的Signals(信号数组),每一个signal都至少sendNext过一次,就会去触发第一个selector参数的方法。

7:高阶操作知识内容

8:RAC并发编程知识点


1: subscribeOn运用

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"%@ 111",[NSThread currentThread]);

//可以放更新UI操作

[subscriber sendNext:@0.1];
RACDisposable *disposable = [[RACScheduler scheduler] schedule:^{
NSLog(@"%@ 5555",[NSThread currentThread]);
[subscriber sendNext:@1.1];
[subscriber sendCompleted];
}];
return disposable;
}];
[[RACScheduler scheduler] schedule:^{
NSLog(@"%@ 222",[NSThread currentThread]);
[[signal subscribeOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(id x) {
NSLog(@"%@ %@",[NSThread currentThread], x);
}]; }];
NSLog(@"%@ 4444",[NSThread currentThread]);

//使用subscribeOn 可以让signal内的代码在主线程中运行,sendNext在哪个线程 则对应的订阅输出就在对应线程上,所以0.1输出是在主线程中; 所以当在signal里面可能要放一些更新UI的操作,而这些是要在主线程才能处理,而订阅者却无法确认,所以要使用subscribeOn让它在主线程中;
//能够保证didSubscribe block在指定的scheduler
//不能保证sendNexterrorcomplete在哪个scheduler


2deliverOn运用

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"%@ 111",[NSThread currentThread]);
[subscriber sendNext:@0.1];
RACDisposable *disposable = [[RACScheduler scheduler] schedule:^{
NSLog(@"%@ 555",[NSThread currentThread]);
[subscriber sendNext:@1.1];
[subscriber sendCompleted];
}];
return disposable;
}];
[[RACScheduler scheduler] schedule:^{
NSLog(@"%@ 222",[NSThread currentThread]);
[[signal deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(id x) {
NSLog(@"%@ %@",[NSThread currentThread], x);

//可以放UI更新操作

}]; }];

//当我们让订阅的处理代码在指定的线程中执行,而不必去关心发送信号的当前线程,就可以deliverOn

9:冷信号跟热信号知识点


Hot Observable是主动的,尽管你并没有订阅事件,但是它会时刻推送,就像鼠标移动;而Cold Observable是被动的,只有当你订阅的时候,它才会发布消息。

Hot Observable可以有多个订阅者,是一对多,集合可以与订阅者共享信息;而Cold Observable只能一对一,当有不同的订阅者,消息是重新完整发送。

热信号是主动的,即使你没有订阅事件,它仍然会时刻推送 而冷信号是被动的,只有当你订阅的时候,它才会发送消息
热信号可以有多个订阅者,是一对多,信号可以与订阅者共享信息 而冷信号只能一对一,当有不同的订阅者,消息会从新完整发送

冷信号与热信号的本质区别在于是否保持状态,冷信号的多次订阅是不保持状态的,而热信号的多次订阅可以保持状态

10:RACDisposable知识点


RACDisposable用于取消订阅信号,默认信号发送完之后就会主动的取消订阅。订阅信号使用的subscribeNext:方法返回的就是RACDisposable类型的对象

当订阅者发送信号- (void)sendNext:(id)value之后,会执行:- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock中的nextBlock。当nextBlock执行完毕也就意味着subscribeNext方法返回了RACDisposable对象。

1.如果不强引用订阅者对象,默认情况下会自动取消订阅,我们可以拿到RACDisposable 用+ (instancetype)disposableWithBlock:(void (^)(void))block做清空资源的一些操作了。

2.如果不希望自动取消订阅,我们应该强引用RACSubscriber * subscriber。在想要取消订阅的时候用- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock返回的RACDisposable对象去调用- (void)dispose方法

11:RACChannel知识点

RACChannelTerminal *channelA = RACChannelTo(self, valueA);
RACChannelTerminal *channelB = RACChannelTo(self, valueB);
[[channelA map:^id(NSString *value) {
if ([value isEqualToString:@"西"]) {
return @"东";
}
return value;
}] subscribe:channelB];
[[channelB map:^id(NSString *value) {
if ([value isEqualToString:@"左"]) {
return @"右";
}
return value;
}] subscribe:channelA];
[[RACObserve(self, valueA) filter:^BOOL(id value) {
return value ? YES : NO;
}] subscribeNext:^(NSString* x) {
NSLog(@"你向%@", x);
}];
[[RACObserve(self, valueB) filter:^BOOL(id value) {
return value ? YES : NO;
}] subscribeNext:^(NSString* x) {
NSLog(@"他向%@", x);
}];
self.valueA = @"西";
self.valueB = @"左";


RACChannelTerminal *characterRemainingTerminal = RACChannelTo(_loginButton, titleLabel.text);

[[self.userNameText.rac_textSignal map:^id(NSString *text) {
return [@(100 - (NSInteger)text.length) stringValue];
}] subscribe:characterRemainingTerminal];

12:RAC倒计时小实例

//倒计时的效果
RACSignal *(^counterSigner)(NSNumber *count)=^RACSignal *(NSNumber *count)
{
RACSignal *timerSignal=[RACSignal interval:1 onScheduler:RACScheduler.mainThreadScheduler];
RACSignal *counterSignal=[[timerSignal scanWithStart:count reduce:^id(NSNumber *running, id next) {
return @(running.integerValue -1);
}] takeUntilBlock:^BOOL(NSNumber *x) {
return x.integerValue<0;
}];

return [counterSignal startWith:count];
};


RACSignal *enableSignal=[self.myTextField.rac_textSignal map:^id(NSString *value) {
return @(value.length==11);
}];

RACCommand *command=[[RACCommand alloc]initWithEnabled:enableSignal signalBlock:^RACSignal *(id input) {
return counterSigner(@10);
}];

RACSignal *counterStringSignal=[[command.executionSignals switchToLatest] map:^id(NSNumber *value) {
return [value stringValue];
}];

RACSignal *resetStringSignal=[[command.executing filter:^BOOL(NSNumber *value) {
return !value.boolValue;
}] mapReplace:@"点击获得验证码"];

//[self.myButton rac_liftSelector:@selector(setTitle:forState:) withSignals:[RACSignal merge:@[counterStringSignal,resetStringSignal]],[RACSignal return:@(UIControlStateNormal)],nil];

//上面也可以写成下面这样
@weakify(self);
[[RACSignal merge:@[counterStringSignal,resetStringSignal]] subscribeNext:^(id x) {
@strongify(self);
[self.myButton setTitle:x forState:UIControlStateNormal];
}];

self.myButton.rac_command=command;


//编写关于委托的编写方式 是在self上面进行rac_signalForSelector
[[self
rac_signalForSelector:@selector(textFieldShouldReturn:)
fromProtocol:@protocol(UITextFieldDelegate)]
subscribeNext:^(RACTuple *tuple) {
@strongify(self)
if (tuple.first == self.myTextField)
{
NSLog(@"触发");
};
}];

self.myTextField.delegate = self;

13:常见的宏定义运用


1
RAC(TARGET, [KEYPATH, [NIL_VALUE]]):用于给某个对象的某个属性绑定
只要文本框文字改变,就会修改label的文字
RAC(self.labelView,text) = _textField.rac_textSignal;

2:
RACObserve(self, name):监听某个对象的某个属性,返回的是信号。
[RACObserve(self.view, center) subscribeNext:^(id x) {
NSLog(@"%@",x);
}];


RACObserve放在block里面使用时一定要加上weakify,不管里面有没有使用到self;否则会内存泄漏,因为RACObserve宏里面就有一个self
@weakify(self);
RACSignal *signal3 = [anotherSignal flattenMap:^(NSArrayController *arrayController) {
//Avoids a retain cycle because of RACObserve implicitly referencing self
@strongify(self);
return RACObserve(arrayController, items);
}];

3:
@weakify(Obj)@strongify(Obj),一般两个都是配套使用,在主头文件(ReactiveCocoa.h)中并没有导入,需要自己手动导入,RACEXTScope.h才可以使用。但是每次导入都非常麻烦,只需要在主头文件自己导入就好了

4:
RACTuplePack:把数据包装成RACTuple(元组类)
把参数中的数据包装成元组
RACTuple *tuple = RACTuplePack(@10,@20);

5:
RACTupleUnpack:RACTuple(元组类)解包成对应的数据
把参数中的数据包装成元组
RACTuple *tuple = RACTuplePack(@"xmg",@20);

解包元组,会把元组的值,按顺序给参数里面的变量赋值
name = @"xmg" age = @20
RACTupleUnpack(NSString *name,NSNumber *age) = tuple;

二:关于使用ReactiveCocoa结合MVVM模式的实例;

MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点

  1. 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的”View”上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。

  2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。

  3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。

  4. 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。

三:单元测试知识

单元测试这边主要采用两种方式,一种是XCode自动的XCTestCase进行,如下面这些就是它所对应的断言等,另外一种是采有KIWI的插件进行测试;项目中有针对viewController、viewModel、帮助类等的测试实例;运用快捷键(command+U)可以运行单元测试实例;


//知识点一:
//方法在XCTestCase的测试方法调用之前调用,可以在测试之前创建在test case方法中需要用到的一些对象等
//- (void)setUp ;
//当测试全部结束之后调用tearDown方法,法则在全部的test case执行结束之后清理测试现场,释放资源删除不用的对象等
//- (void)tearDown ;
//测试代码执行性能
//- (void)testPerformanceExample


//知识点二:
//通用断言
XCTFail(format…)
//为空判断,a1为空时通过,反之不通过;
XCTAssertNil(a1, format...)
//不为空判断,a1不为空时通过,反之不通过;
XCTAssertNotNil(a1, format…)
//当expression求值为TRUE时通过;
XCTAssert(expression, format...)
//当expression求值为TRUE时通过;
XCTAssertTrue(expression, format...)
//当expression求值为False时通过;
XCTAssertFalse(expression, format...)
//判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
XCTAssertEqualObjects(a1, a2, format...)
//判断不等,[a1 isEqual:a2]值为False时通过;
XCTAssertNotEqualObjects(a1, a2, format...)
//判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以);
XCTAssertEqual(a1, a2, format...)
//判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
XCTAssertNotEqual(a1, a2, format...)
//判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)
//判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...)
//异常测试,当expression发生异常时通过,反之不通过;
XCTAssertThrows(expression, format...)
//异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过
XCTAssertThrowsSpecific(expression, specificException, format...)
//异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)
//异常测试,当expression没有发生异常时通过测试;
XCTAssertNoThrow(expression, format…)
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertNoThrowSpecific(expression, specificException, format...)
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)

采用KiWi的单元测试效果:


#import <Kiwi/Kiwi.h>
//把原本在项目pch中那些第三方插件的头文件也要引入
#import <ReactiveCocoa/ReactiveCocoa.h>

//测试LogInViewController
#import "RACTestLoginViewController.h"


SPEC_BEGIN(LoginViewControllerSpec)

describe(@"RACTestLoginViewController", ^{
__block RACTestLoginViewController *controller = nil;

beforeEach(^{
controller = [RACTestLoginViewController new];
[controller view];
});

afterEach(^{
controller = nil;
});

describe(@"Root View", ^{

context(@"when view did load", ^{
it(@"should bind data", ^{
controller.userNameText.text=@"wujunyang";
controller.passWordTest.text=@"123456";
//
//一定要调用sendActionsForControlEvents方法来通知UI已经更新 因为RAC是监听这个输入框的变化
[controller.userNameText sendActionsForControlEvents:UIControlEventEditingChanged];
[controller.passWordTest sendActionsForControlEvents:UIControlEventEditingChanged];

[[controller.myLoginViewModel.username should] equal:controller.userNameText.text];
[[controller.myLoginViewModel.password should] equal:controller.passWordTest.text];
});
});

});
});

SPEC_END

关于kiwi中的操作类型可以直接查看:https://github.com/allending/Kiwi/wiki/Expectations

注意:发现在进行单元测试时,针对RAC就会报[RACStream(Operations) reduceEach:]_block_invoke,后来发现是Pod引入写法有问题,导致的【it usually means RAC is being linked twice. Make sure it’s only in your app target.】 所以测试的MobileProjectTests特别要注意;


platform :ios, '7.0'

abstract_target 'MobileProjectDefault' do
pod 'AFNetworking', '~>2.6.0'
pod 'SDWebImage', '~>3.7'
pod 'JSONModel', '~> 1.0.1'
pod 'Masonry','~>0.6.1'
pod 'FMDB/common' , '~>2.5'
pod 'FMDB/SQLCipher', '~>2.5'
pod 'CocoaLumberjack', '~> 2.0.0-rc'
pod 'ReactiveCocoa', '2.5'
pod 'CYLTabBarController'
pod 'MLeaksFinder' #可以把它放在MobileProject_Local的target中 这样就不会影响到产品环境
pod 'RealReachability'

target 'MobileProject_Local' do

end

target 'MobileProject' do

target 'MobileProjectTests' do
inherit! :search_paths
pod 'Kiwi', '~> 2.3.1'
end
end
end

四:ReactiveCocoa知识分享地址


ReactiveCocoa 和 MVVM 入门 http://yulingtianxia.com/blog/2015/05/21/ReactiveCocoa-and-MVVM-an-Introduction/

MVVM Tutorial with ReactiveCocoa http://southpeak.github.io/blog/2014/08/08/mvvmzhi-nan-yi-:flickrsou-suo-shi-li/

ReactiveCocoa 1-官方readme文档翻译 http://cindyfn.com/reactivecocoa/2014/12/01/ios-frame-use-ReactiveCocoa.html

这样好用的ReactiveCocoa,根本停不下来 http://www.cocoachina.com/ios/20150817/13071.html

ReactiveCocoa基本组件:深入浅出RACCommand http://www.tuicool.com/articles/nYJRvu

ReactiveCocoa自述:工作原理和应用 http://www.cocoachina.com/ios/20150702/12302.html

RACSignal的巧克力工厂 http://www.cnblogs.com/sunnyxx/p/3547763.html

ReactiveCocoa一些概念讲解 http://www.thinksaas.cn/group/topic/347067/

细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号 http://www.tuicool.com/articles/e2uMzyq

细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号与热信号 http://www.tuicool.com/articles/emIVZjY

最快让你上手ReactiveCocoa之基础篇 http://www.jianshu.com/p/87ef6720a096

最快让你上手ReactiveCocoa之进阶篇 http://www.jianshu.com/p/e10e5ca413b7

ReactiveCocoa基础:理解并使用RACCommand http://www.yiqivr.com/2015/10/19/%E8%AF%91-ReactiveCocoa%E5%9F%BA%E7%A1%80%EF%BC%9A%E7%90%86%E8%A7%A3%E5%B9%B6%E4%BD%BF%E7%94%A8RACCommand/

RAC一些代码总结:https://github.com/shuaiwang007/RAC

ReactiveCocoa小总结 http://www.jianshu.com/p/8fd6c8349774

如何在ReactiveCocoa中写单元测试 http://www.jianshu.com/p/412875512bd1

TDD的iOS开发初步以及Kiwi使用入门 https://onevcat.com/2014/02/ios-test-with-kiwi/