IOS开发入门(7)-自动布局(2)

本节中,会先哔哔一下一些可能你们不想看的概念,然后来实现之前我们做的Car Valet程序的横纵向显示。

完美纵向显示

  有的iPhone和iPod touch型号具有不同的屏幕高度。自动布局让我们能够创建一组对所有的几何形状和大小都起作用的约束。
  
  设计与添加约束是设计和创建用户体验(UX)流程中的一部分。首先,设计屏幕模型,通常作为初始的应用程序规格说明。当开始开发时,不需要使用约束就能进行简单的初始布局。这是到此为止对Car Valet应用程序已经做过的事情。在实践中,初始设计在实现过程中很容易被修改。因此在界面相当稳定之前最好不要在创建约束上面投入太多时间(毕竟之后还是要做一大堆修改的)。

  最后,开始设计与添加约束。当在不同屏幕大小和方向上测试界面时,我们通常会发现一些问题。这会让我们进入添加约束、尝试它们、调试、调优,然后添加更多约束的循环。

以约束的方式思考

  如果想有效地使用约束,我们需要改变看待界面设计以及布局的方式。典型的方式是考虑在座标系统中包含视图元素的矩形。设计活动包括将外观翻译为正确的座标,以及添加更多代码或额外布局来调整不同屏幕尺寸和方向的外观。

  借助约束,可用一种全新的方式来看待界面:屏幕上的可视化元素如何相互关联?目标是找到的约束(视图之间的关系)的集合,使得iOS让视图能适应任何支持的屏幕大小和方向。这不仅包括一个视图如何与另一个视图关联,还包括如何将试图分组以及这些组之间如何关联,这可以包括一个层次结构下的视图与另一层次结构下的视图之间的关系。

  尽管这看起来很复杂,但关键是聚集关系。约束来自那些关系。表示关系的最简单的方法是使用语言。

  在开始设计之前,理解屏幕的约束集合必须满足两个条件是很重要的。首先,约束结合在一起,应当针对一种给定的屏幕大小规定一种且仅有一种布局——也就是说,它们不应当有歧义。其次,约束必须没有冲突。

  对于相互冲突的约束,没有办法满足最高优先级的约束。每个约束的优先级在0到1000之间。1000是默认值,意味着这个约束不许满足。例如,Previous按钮不能既有优先级为1000的固定宽度约束,又有优先级同样为1000的自适应内容的宽度约束。降低其中任何一个约束的优先级,或者移除一个约束可以解决此约束冲突。冲突会导致让应用程序崩溃的运行时异常。

  歧义在设计阶段更难发现。不像冲突,约束系统可以防止受(歧义)影响的视图。然而,以不止一种方式来放置它们。歧义出现的典型方式是当改变方向而后变回来时,这时一个或更多个视图会不在旋转之前的位置。(之后我们在修改CarValet时会发现的)。

完整的规定

  如何知道何时完成布局?要让自动布局无歧义地放置界面中的所有视图,需要为每个视图找到原点和大小。谓词,每个视图必须属于一个或多个干洗,并规定4个约束:两个帮助系统设计水平位置和大小,另外两个计算垂直位置和大小。

OK,正文开始

纵向约束

这里写图片描述
  我们很容易知道,上图也就是我们的程序,可以分成三部分(不要在意这个图,我是从之前的文章里面弄过来的,只是为了说明),第一部分:“Total Car:999标签”和“New Car按钮”;第二部分:中间的分割线;第三部分:剩下的标签和按钮;

  这时候,我们需要引入三个UIView来分割开来。

  首先是顶部

  选中一个View

这里写图片描述

  拉到面板中

这里写图片描述

  设置约束

这里写图片描述

  然后把Total Car标签跟New Car按钮扔进去,直接用鼠标拖动左侧的菜单栏,并将刚加入的View改名改成Add Car Group

这里写图片描述

  这时候如下图(当然你看到的应该是挤在一起的,下图是我完成布局之后的截图)

这里写图片描述

  同样的道理,对于分隔符,我们不用引入新的View,因为它本身就是一个View(忘了的看一下前面的章节在引入分隔符的那部分,我们引入的就是个View,只是把背景颜色给改了)。

  把分隔符的名字改成Separator View,然后进行约束

这里写图片描述

  左右两边的约束本应该是弄成标准距离,但是我这边突然爆炸无法选中,用0也是可以的。

  弄完之后可能会看到这个

这里写图片描述

  黄线表示他在运行的时候实际上在那个地方,我们可以手动的移动上去

这里写图片描述

  放到黄线位置黄线就没了。如果出现红线的话,则表示添加的约束有冲突,这个按上面说的方法自己修改吧。

  然后就剩下下半部分,再引入一个View,并将它命名为View Car Group,在将剩下的标签按钮扔进去。

  最后左侧菜单(IB菜单)应该是这个样子的(PS,Constraints里面的数量应该是不一样的,毕竟我这个是全部完成的样子,只要Add Car Group、Separator View 和 View Car Group一样就行)

这里写图片描述

  面板应该是这样子的(当然,你的面板上半部分跟下半部分View里面的元素应该是乱成一堆的)

这里写图片描述

  ok,现在呢分好类别了,需要对三个部分的内部元素进行整理。

  哦,对了,这里给不太懂的新手们说明一下。整个故事面板是个View相当于一个容器,或也可以说是一张布(大布),Label跟button就是画在布上的东西,现在扔进去新的View(小布),在把Label跟button扔进去,相当于我们在另一张布上面画好Label和button,然后再把这张布贴到之前那张大布上面,相当于完成好小部分再整合到一起。

  现在对添加汽车部分进行约束:显然,Total Car标签应该放到左上角,而New Car按钮应该在左下角,这样看起来才会好看点(至少我是这么认为,当然也可以放到别的地方),选中Total Car标签,修改如下

这里写图片描述

  对于New Car按钮的修改类似,我这里就不贴图了。注意出现黄色的线的话就更改元素位置或大小,直到黄线消失(不然看的闹心啊)

  对于下半部分,先修改Current Car Number标签到左上角,Previous到左下角,Next按钮到右下角。

设置Car Information如图

这里写图片描述

设置Edit如图

这里写图片描述

这里写图片描述

现在应该是一样的了

这里写图片描述

  好了,现在我们纵向的约束已经解决啦,可以自己试着换不同型号的模拟器或拿手机去试试看吧。

横向约束

  有些应用程序仅支持一种屏幕方向。我们现在编写的是同时支持纵向和横向屏幕的应用程序。

引用第6节的图片

这里写图片描述

  横向之后变成这个样子。

  我们想要的应该是这个样子或类似的

这里写图片描述

我们的思路是:

  • 在纵向变成横向的时候,把纵向的约束都去除,然后添加横向的约束。
  • 在横向变为纵向的时候,将横向的约束都去除,然后添加纵向的约束。

我们将用代码的形式来实现:

  首先,我们在代码中需要获得纵向屏幕的约束(如果你一开始的应用程序是面对横向的,那就改成横向)。

  我们之前已经用IBOutlet将连接拖拽到IB对象。虽然我们在这里也能这样,但是将很难维护,因为UI会随着时间而变化,并且约束会改变。

  如果根据所属视图将约束分组,会更简单也更容易维护。我们使用一种特殊的IBOutlet,即IBOutletCollection来实现。这些outlet将多个元素引用搜集到一个数组中。我们需要为每个需要删除约束的视图准备一个集合。

在代码中定义IBOutletCollection的通常方式如下:

@property <Optional Property Qualifiers> IBOutletConnection(CollectionElementClass) <Variable Declaration>

  第一个可选部分用于声明属性限定符,如weak。下一个可选部分是集合的定义,括号中可选的类告诉编译器集合中可以有什么类型的元素。当在IB中拖拽出连接时,只让链接到属于你所设置的类的元素。如果指定了UIView,那么可以链接到任何类型的视图,包括按钮、标签或单纯的视图。但如果指定UIButton,那么只能链接到按钮。最后一部分声明了实际变量,并且必须是NSArray的某种(类或子类)。

  选中一个Add Car Group中的Constraints,如同添加IBOutlet一样,拉到ViewController.h中

这里写图片描述

命名为:addCarViewPortraitConstraints,选择Outlet Collection
这里写图片描述

  然后在选择其他的Add Car Group中的Constraints,同样的方法,不过这时候是拉到刚才的addCarViewPortraitConstraints的代码中,如下图

这里写图片描述

  然后用上面的方法,对于Separator View的Constraints,命名为separatorViewPortraitConstraints

  然后注意了,最后一个是根目录的Constraints,命名为rootViewPortraitConstraints
这里写图片描述

这时候ViewController.h的代码应该如下:

#import <UIKit/UIKit.h>

#import "CarEditViewControllerProtocol.h"

@interface ViewController : UIViewController

<CarEditViewControllerProtocol>

@property (weak, nonatomic) IBOutlet UILabel *totalCarsLabel;
@property (weak, nonatomic) IBOutlet UILabel *CarNumberLabel;
@property (weak, nonatomic) IBOutlet UILabel *CarInfoLabel;

@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *addCarViewPortraitConstraints;

@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *separatorViewPortraitConstraints;

@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *rootViewPortraitConstraints;

- (IBAction)newCar:(id)sender;
- (IBAction)previousCar:(id)sender;
- (IBAction)nextCar:(id)sender;

@end

我们也可以在下图位置看到我们进行的约束

这里写图片描述

  或者是右键上图左边的那个圈圈(截图截不出来。。。),会出现一个类似右侧的菜单。

  好了,现在原本的约束已经保存到代码中。现在我们要进行保存横向屏幕的约束。

  在ViewController.m中,添加代码,

@implementation ViewController {
    NSMutableArray *arrayOfCars; //使用mutable array记录所有汽车对象
    NSInteger displayedCarIndex; //指定靠下位置显示的汽车的数组索引
    NSArray *separatorViewLandscapeConstraints;
    NSArray *addCarViewLandscapeConstraints;
    NSArray *rootViewLandscapeConstraints;
}

  新增的三个变量分别用来保存三部分的横向约束。

  然后我们要把
这里写图片描述
  这三个添加到ViewController.m中,也就是拉。

这里写图片描述

其中:
- Add Car Group名称为addCarView
- Separator名称为separatorView
- View Car Group名称为viewCarView

ViewController.m中代码如下:

@implementation ViewController {
    NSMutableArray *arrayOfCars; //使用mutable array记录所有汽车对象
    NSInteger displayedCarIndex; //指定靠下位置显示的汽车的数组索引
    NSArray *separatorViewLandscapeConstraints;
    NSArray *addCarViewLandscapeConstraints;
    NSArray *rootViewLandscapeConstraints;
    BOOL isShowingPortrait;
    __weak IBOutlet UIView *addCarView;
    __weak IBOutlet UIView *separatorView;
    __weak IBOutlet UIView *viewCarView;
}

好了,在ViewController.m中创建方法,代码如下(这是用来添加约束的)

- (void)setupLandscapeConstraints {
    NSDictionary *views;//1 创建一个变量绑定字典,用于根据字符串生成约束
    id topGuide = self.topLayoutGuide;
    id bottomGuide = self.bottomLayoutGuide;
    views = NSDictionaryOfVariableBindings(
                                           topGuide,
                                           bottomGuide,
                                           addCarView,
                                           separatorView,
                                           viewCarView
                                           );
    NSMutableArray *tempRootViewConstraints = [NSMutableArray new];//2 创建一个临时可变数组,存放附属主视图的生成的约束
    NSMutableArray *tempAddViewConstrains=[NSMutableArray new];
    NSMutableArray *tempSeparatorViewConstrains=[NSMutableArray new];

    NSArray *generatedConstraints;//3创建一个到所有返回的生成约束属组的可复用引用
    addObjectsFromArray:generatedConstraints];//5将生成的约束添加到主视图约束的临时数组中。然后生成其余的主视图约束,将每个新的集合添加到临时数组中

    generatedConstraints =
        [NSLayoutConstraint
         constraintsWithVisualFormat:@"V:[topGuide]-[separatorView]-[bottomGuide]"
                            options:0
                            metrics:nil
                            views:views];
    [tempRootViewConstraints addObjectsFromArray:generatedConstraints];

    generatedConstraints =
        [NSLayoutConstraint
         constraintsWithVisualFormat:@"V:[topGuide]-[addCarView]-[bottomGuide]"
                            options:0
                            metrics:nil
         views:views];
    [tempRootViewConstraints addObjectsFromArray:generatedConstraints];

    //获得上标题 Current Car Number 和Car info
    generatedConstraints =
    [NSLayoutConstraint
     constraintsWithVisualFormat:@"V:[topGuide]-[viewCarView]-[bottomGuide]"
                            options:0
                            metrics:nil
                            views:views];
    [tempRootViewConstraints addObjectsFromArray:generatedConstraints];
    //获得下面的三个按钮
    generatedConstraints =
    [NSLayoutConstraint
     constraintsWithVisualFormat:@"|-[addCarView]-2-[separatorView]-40-[viewCarView]-|"
                            options:0
                            metrics:nil
                            views:views];
    [tempRootViewConstraints addObjectsFromArray:generatedConstraints];
    //以下是tempAddViewConstrains的
    generatedConstraints=
    [NSLayoutConstraint
     constraintsWithVisualFormat:@"H:[addCarView(132)]"
     options:0
     metrics:nil
     views:views];
    [tempAddViewConstrains addObjectsFromArray:generatedConstraints];


    generatedConstraints=
    [NSLayoutConstraint
     constraintsWithVisualFormat:@"H:[separatorView(2)]"
     options:0
     metrics:nil
     views:views];
    [tempSeparatorViewConstrains addObjectsFromArray:generatedConstraints];


    //6 将rootViewLandscapeConstraints初始化为生成约束的可变数组的内容
    rootViewLandscapeConstraints = [NSArray arrayWithArray:tempRootViewConstraints];
    //7将addCarViewLandscapeConstraints初始化为包含添加汽车视图的宽度约束的数组
    addCarViewLandscapeConstraints =  [NSArray arrayWithArray:tempAddViewConstrains];
    //8将separatorViewLandscapeConstraints初始化为包含分隔符视图的宽度约束的数组
    separatorViewLandscapeConstraints =[NSArray arrayWithArray:tempSeparatorViewConstrains];

}

  然后我们要写一个判断屏幕方向的函数,以此来使用不同的约束

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];


    if (UIInterfaceOrientationIsPortrait(toInterfaceOrientation)) {//1 调用超类方法之后,弄清楚新的屏幕方向。UIInterfaceOrientationIsPortrait是个系统宏,在纵向屏幕时为true
        [self.view removeConstraints:rootViewLandscapeConstraints];//2这是纵向屏幕,因此移除横向约束,从主视图开始。对还没有附属到视图的约束调用removeConstraints:也是可行的
        [addCarView removeConstraints:addCarViewLandscapeConstraints];
        [separatorView removeConstraints:separatorViewLandscapeConstraints];

        [self.view addConstraints:self.rootViewPortraitConstraints];//3添加所有的纵向约束。添加已存在的会被忽略
        [addCarView addConstraints:self.addCarViewPortraitConstraints];
        [separatorView addConstraints:self.separatorViewPortraitConstraints];
    } else {//4横向屏幕
        [self.view removeConstraints:self.rootViewPortraitConstraints];//5移除所有特定于纵向屏幕的约束
        [addCarView removeConstraints:self.addCarViewPortraitConstraints];
        [separatorView removeConstraints:self.separatorViewPortraitConstraints];

        [self.view addConstraints:rootViewLandscapeConstraints];//6添加特定于横向屏幕的约束
        [addCarView addConstraints:addCarViewLandscapeConstraints];
        [separatorView addConstraints:separatorViewLandscapeConstraints];
    }
}

  然后让我们写的函数调用

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    arrayOfCars = [[NSMutableArray alloc] init];//初始化汽车的数组为空数组
    displayedCarIndex = 0;//显示创建的第一辆汽车
    [self setupLandscapeConstraints];
}

  这里来介绍一些可视化约束语言(具体的话自行百度咯),下面表格给出一些语句以及含义。

注释:

  • V表示垂直方向,H表示水平方向 “/”其实是|,但是md会认为是表格,所以用”/”代替”|”,用的时候要换成”|”
  • []中间包含的是视图名称
格式字符串 组件视图 容器视图
“V:[topGuide]-[separatorView]-[bottomGuide]” 主视图、分隔符视图 主视图
“V:[topGuide]-[addCarView]-[bottomGuide]” 添加汽车视图、主视图 主视图
“V:[topGuide]-[viewCarView]-[bottomGuide]” 主视图、查看汽车视图 主视图
“/-[addCarView]-2-[separatorView]-40-[viewCarView]-/” 添加汽车视图、分隔符视图、主视图、查看汽车视图 主(根)视图
“H:[addCarView(132)]” 添加汽车视图 添加汽车视图
“H:[separatorView(2)]” 分隔符视图 分隔符视图

解释:

  • H:[addCarView(132)]表示 addCarView视图水平方向长度为132
  • V:[topGuide]-[separatorView]-[bottomGuide]表示SeparatorView视图竖直方向上连顶端,下连低端

      其他的应该是能看懂的

      PS:由于我是调试完成之后才写的博客,所以有些过程的内容就没掉了。如下图

这里写图片描述

  这是我们在添加横向视图约束的时候会发生的事情,也就是冲突了。这时候需要我们设断点。可以设置在判断屏幕旋转的函数里面。然后遇到断点的时候,在输出面板里面会有(lldb)这个东西,然后在后面输入po [[UIWindow keyWindow] _autolayoutTrace]会出现一些数据,因为上图信息中出问题的地方给的是地址码,所以就根据上面的地址码找到po [[UIWindow keyWindow] _autolayoutTrace]后出现的信息里相应的地方,就大致能知道哪儿个地方的约束冲突了。然后修改即可。

  这时候旋转就能得到我们一开始要的结果了。
这里写图片描述

PS还没结束

  这时候我们要解决的就是歧义问题了。可以试一下,当我们在纵向屏幕中,点击Edit,进入到编辑界面,这时候,转屏,然后返回,你会发现下图

这里写图片描述

  它竟然没有转过来。这是因为旋转消息只会被发送到可见的视图控制器。当发生旋转的时候,Add/View场景并没有显示。旋转消息没有发送,所以约束集合仍然是针对上一次的位置——在当前这种情况下为纵向。然和纵向的约束不适合横向的屏幕

  所以修改代码如下

- (void)viewWillAppear:(BOOL)animated {//viewWillAppear:会在ViewController的视图每次即将在屏幕上显示时被调用。
    [super viewWillAppear:animated];

    UIInterfaceOrientation currOrientation = [[UIApplication sharedApplication]statusBarOrientation];//找到当前设备的方向
    BOOL currIsPortrait = UIInterfaceOrientationIsPortrait(currOrientation);//当前设备方向是否为纵向
    if((isShowingPortrait && !currIsPortrait) || (!isShowingPortrait && currIsPortrait)){//控制器的上一个方向是否与当前方向不同
        [self willAnimateRotationToInterfaceOrientation:currOrientation
                                               duration:0.0f];
    }
}

  这样就万事大吉啦
  今天的介绍就到这里咯

  我的另一个博客站点:Arnold-你们好啊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章