本系列的第一篇文章介绍了自动布局的基本原理,第二篇文章通过一个简单的例子演示了如何使用Xcode的Interface Builder(简称IB)以可视化方式添加约束。本篇为该系列的第三篇文章,主要介绍如何通过代码来添加布局约束。
其实,我个人认为本篇才应该是系列的第二篇,因为通过代码构建自动布局约束是最基础的,也是最灵活的方式。而IB只不过是把复杂的过程以直观简单的方式呈现出来,而且并非所有的情况都能用IB来解决,所以学习并掌握通过代码来添加自动布局约束是非常必要的。但是为了降低学习的难度,提高读者理解和接受的程度,最终还是把本编放在第三的位置。闲言少叙,我们进入正题。
第一篇文章中讲到,每一个布局约束就是一个明确的线性变化规则,在数学上是以一次函数的形式表示,即:
y = m * x + c (公式3.1)
在UIKit中,每一个布局约束是一个NSLayoutConstraint实例,NSLayoutConstraint类的主要属性定义如下:
NS_CLASS_AVAILABLE_IOS(6_0)
@interface NSLayoutConstraint : NSObject
...
@property (readonly, assign) id firstItem;
@property (readonly) NSLayoutAttribute firstAttribute;
@property (readonly) NSLayoutRelation relation;
@property (readonly, assign) id secondItem;
@property (readonly) NSLayoutAttribute secondAttribute;
@property (readonly) CGFloat multiplier;
@property CGFloat constant;
...
+(instancetype)constraintWithItem:(id)firstItem attribute:(NSLayoutAttribute)firstAttribute
relatedBy:(NSLayoutRelation)relation
toItem:(id)secondItem attribute:(NSLayoutAttribute)secondAttribute
multiplier:(CGFloat)multiplier constant:(CGFloat)constant;
其中的firstItem与secondItem分别是界面中受约束的视图与被参照的视图。他们不一定非得是兄弟关系或者父子关系,只要是他们有着共同的祖先视图即可,这一点是autoresizingMask无法做到的。
firstAttribute与secondAttribute分别是firstItem与secondItem的某个布局属性(NSLayoutAttribute):
typedef NS_ENUM(NSInteger, NSLayoutAttribute)
{
NSLayoutAttributeLeft = 1,
NSLayoutAttributeRight,
NSLayoutAttributeTop,
NSLayoutAttributeBottom,
NSLayoutAttributeLeading,
NSLayoutAttributeTrailing,
NSLayoutAttributeWidth,
NSLayoutAttributeHeight,
NSLayoutAttributeCenterX,
NSLayoutAttributeCenterY,
NSLayoutAttributeBaseline,
NSLayoutAttributeNotAnAttribute = 0,
......//省略剩余
};
每一个枚举值代表了一个布局属性,名字都很直观就不一一解释了。注意,firstItem与secondItem不一定非得是同样的值,允许定义诸如某视图的高度等于另一个视图的宽度这样的约束。NSLayoutAttributeNotAnAttribute这个额外解释一下,当我们需要为某个视图精确指定一个宽度或者高度值时,这时候secondItem为nil,secondAttribute为NSLayoutAttributeNotAnAttribute。
relation定义了布局关系(NSLayoutRelation):
typedef NS_ENUM(NSInteger, NSLayoutRelation)
{
NSLayoutRelationLessThanOrEqual = -1,
NSLayoutRelationEqual = 0,
NSLayoutRelationGreaterThanOrEqual = 1,
};
布局关系可以是相等、大于等于或者小于等于。
multiplier即比例系数。constant即常量。
因此,每个约束就对应如下关系:
firstItem.firstAttribute {==,<=,>=} secondItem.secondAttribute * multiplier + constant (公式3.2)
我们可以调用NSLayoutConstraint类的constraintWithItem:…方法,传入所有需要的参数构造一个新的约束。
理论就到此为止,下面我们还是以第二篇的例子来讲解如何使用代码添加约束。
打开Xcode,新建项目,选择iOS -> Application -> Single View Application。项目命名为AutoLayoutByConstraint,语言任意选择,设备选择Universal。下载苹果Logo图片apple.jpg,并将其拖入项目中。文件下载地址:
http://yunpan.cn/cfmJB82dfSwf6(提取码:4049)
首先在界面上方用来显示苹果Logo图片的是一个UIImageView,ViewController类的viewDidLoad方法如下:
- (void)viewDidLoad
{
[super viewDidLoad];
UIImageView* logoImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"apple.jpg"]];
logoImageView.contentMode = UIViewContentModeScaleAspectFit;
[self.view addSubview:logoImageView];
}
我们需要为logoImageView其添加4个约束:
- logoImageView左侧与父视图左侧对齐
- logoImageView右侧与父视图右侧对齐
- logoImageView顶部与父视图顶部对齐
- logoImageView高度为父视图高度一半
根据公式3.2,在ViewController类的viewDidLoad方法中构造上述4个约束,代码如下:
//logoImageView左侧与父视图左侧对齐
NSLayoutConstraint* leftConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];
//logoImageView右侧与父视图右侧对齐
NSLayoutConstraint* rightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];
//logoImageView顶部与父视图顶部对齐
NSLayoutConstraint* topConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];
//logoImageView高度为父视图高度一半
NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeHeight multiplier:0.5f constant:0.0f];
//iOS 6.0或者7.0调用addConstraints
//[self.view addConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]];
//iOS 8.0以后设置active属性值
leftConstraint.active = YES;
rightConstraint.active = YES;
topConstraint.active = YES;
heightConstraint.active = YES;
注意,我们需要将创建的约束添加到约束所涉及到的两个视图的最小公共祖先视图上。例如,如果直接设置视图的宽度和高度,则将约束添加到该视图即可;如果约束建立在父视图和子视图上,则添加到父视图上;如果约束建立在两个兄弟视图上,则添加到两个兄弟视图的父视图上。
对于上面的4个约束而言,涉及到的两个视图分别是logoImageView与其父视图self.view,这两个视图的最小公共祖先视图为self.view。UIView类提供了若干方法和属性,用于添加或者移除约束。对于iOS 6或者iOS 7可以调用addConstraint(s):和removeConstraint(s):方法;对于iOS 8及更新的版本,直接设置约束的active属性或者调用activateConstraints:与deactivateConstraints:类方法。
貌似logoImageView的尺寸不太对。如果在viewDidLoad方法中设置self.view的背景色为红色,看得会更清楚:
为什么会这样?这是由于苹果在iOS 6当中引入了自动布局的新概念,但在那时仍然有很多旧的代码使用autoresizingMask与setFrame:的方式构建界面。试想,如果将一个已经设置好frame并使用autoresizingMask的视图添加到一个使用自动布局的视图中时,运行时需要隐式地将前者的frame和autoresizingMask转化为自动布局约束(这些隐式转换的约束的类型为NSAutoresizingMaskLayoutConstraint),这样才能明确其位置与尺寸而不会导致约束的缺失。这个隐式转换的过程,是由UIView的translatesAutoresizingMaskIntoConstraints属性的值决定的。默认情况下,该值为YES,表示需要运行时自动进行隐式转换。这对于兼容旧的代码当然是好的,然而当我们明确为视图添加了约束后,我们就不希望再进行autoresizingMask的隐式转换了,否则就会引起约束的冲突。因此,需要特别注意的是,当我们使用代码创建视图时,需要将translatesAutoresizingMaskIntoConstraints属性的值设置为NO。在viewDidLoad方法中创建logoImageView的代码之后,添加如下代码:
UIImageView* logoImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"apple.jpg"]];
logoImageView.translatesAutoresizingMaskIntoConstraints = NO;
再次运行,这次就没问题了。
到这里,我想你应该可以把剩余的视图和约束的代码添加上了,全部代码如下:
- (void)viewDidLoad
{
[super viewDidLoad];
// self.view.backgroundColor = [UIColor redColor];
UIImageView* logoImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"apple.jpg"]];
logoImageView.translatesAutoresizingMaskIntoConstraints = NO;
logoImageView.contentMode = UIViewContentModeScaleAspectFit;
[self.view addSubview:logoImageView];
//logoImageView左侧与父视图左侧对齐
NSLayoutConstraint* leftConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];
//logoImageView右侧与父视图右侧对齐
NSLayoutConstraint* rightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];
//logoImageView顶部与父视图顶部对齐
NSLayoutConstraint* topConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];
//logoImageView高度为父视图高度一半
NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeHeight multiplier:0.5f constant:0.0f];
//iOS 6.0或者7.0调用addConstraints
// [self.view addConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]];
//iOS 8.0以后设置active属性值
leftConstraint.active = YES;
rightConstraint.active = YES;
topConstraint.active = YES;
heightConstraint.active = YES;
UIScrollView* scrollView = [UIScrollView new];
scrollView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:scrollView];
//scrollView左侧与父视图左侧对齐
NSLayoutConstraint* scrollLeftConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];
//scrollView右侧与父视图右侧对齐
NSLayoutConstraint* scrollRightConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];
//scrollView底部与父视图底部对齐
NSLayoutConstraint* scrollBottomConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f];
//scrollView顶部与logoImageView底部对齐
NSLayoutConstraint* scrollTopConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:logoImageView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f];
scrollLeftConstraint.active = YES;
scrollRightConstraint.active = YES;
scrollBottomConstraint.active = YES;
scrollTopConstraint.active = YES;
UILabel* nameLabel = [UILabel new];
nameLabel.translatesAutoresizingMaskIntoConstraints = NO;
nameLabel.text = @"苹果公司";
nameLabel.backgroundColor = [UIColor greenColor];
[scrollView addSubview:nameLabel];
UILabel* descriptionLabel = [UILabel new];
descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO;
descriptionLabel.text = @"苹果公司(Apple Inc. )是美国的一家高科技公司。由史蒂夫·乔布斯、斯蒂夫·沃兹尼亚克和罗·韦恩(Ron Wayne)等三人于1976年4月1日创立,并命名为美国苹果电脑公司(Apple Computer Inc. ), 2007年1月9日更名为苹果公司,总部位于加利福尼亚州的库比蒂诺。\n苹果公司创立之初主要开发和销售的个人电脑,截至2014年致力于设计、开发和销售消费电子、计算机软件、在线服务和个人计算机。苹果的Apple II于1970年代助长了个人电脑革命,其后的Macintosh接力于1980年代持续发展。该公司硬件产品主要是Mac电脑系列、iPod媒体播放器、iPhone智能手机和iPad平板电脑;在线服务包括iCloud、iTunes Store和App Store;消费软件包括OS X和iOS操作系统、iTunes多媒体浏览器、Safari网络浏览器,还有iLife和iWork创意和生产力套件。苹果公司在高科技企业中以创新而闻名世界。\n苹果公司1980年12月12日公开招股上市,2012年创下6235亿美元的市值记录,截至2014年6月,苹果公司已经连续三年成为全球市值最大公司。苹果公司在2014年世界500强排行榜中排名第15名。2013年9月30日,在宏盟集团的“全球最佳品牌”报告中,苹果公司超过可口可乐成为世界最有价值品牌。2014年,苹果品牌超越谷歌(Google),成为世界最具价值品牌 。";
descriptionLabel.numberOfLines = 0;
descriptionLabel.backgroundColor = [UIColor yellowColor];
[scrollView addSubview:descriptionLabel];
//nameLabel左侧与父视图左侧对齐
NSLayoutConstraint* nameLabelLeftConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];
//nameLabel右侧与父视图右侧对齐
NSLayoutConstraint* nameLabelRightConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];
//nameLabel底部与descriptionLabel顶部对齐
NSLayoutConstraint* nameLabelBottomConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:descriptionLabel attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];
//nameLabel顶部与父视图顶部对齐
NSLayoutConstraint* nameLabelTopConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];
//nameLabel高度为20
NSLayoutConstraint* nameLabelHeightConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:20.0f];
nameLabelLeftConstraint.active = YES;
nameLabelRightConstraint.active = YES;
nameLabelBottomConstraint.active = YES;
nameLabelTopConstraint.active = YES;
nameLabelHeightConstraint.active = YES;
//descriptionLabel左侧与父视图左侧对齐
NSLayoutConstraint* descriptionLabelLeftConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];
//descriptionLabel右侧与父视图右侧对齐
NSLayoutConstraint* descriptionLabelRightConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];
//descriptionLabel底部与父视图底部对齐
NSLayoutConstraint* descriptionLabelBottomConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f];
descriptionLabelLeftConstraint.active = YES;
descriptionLabelRightConstraint.active = YES;
descriptionLabelBottomConstraint.active = YES;
//nameLabel宽度与logoImageView宽度相等
NSLayoutConstraint* nameLabelWidthConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:logoImageView attribute:NSLayoutAttributeWidth multiplier:1.0f constant:0.0f];
//nameLabel宽度与logoImageView宽度相等
NSLayoutConstraint* descriptionLabelWidthConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:logoImageView attribute:NSLayoutAttributeWidth multiplier:1.0f constant:0.0f];
nameLabelWidthConstraint.active = YES;
descriptionLabelWidthConstraint.active = YES;
}
程序最终项目文件链接:http://yunpan.cn/cV2cmtNMYz6UA (提取码:0f38)
自动布局约束是通过描述视图间的关系而非强加坐标值来进行定位的,它更能满足不同设备尺寸的界面布局,并且更容易让人理解。虽然上面的代码很冗长,但每一句所描述的事实都十分清楚。在此省略自动布局的好处10000字。。。
区区几个简单的视图,就要写这么长的代码。。。
估计你看得有点眼花缭乱了吧,其实我也是修改并检查了好几次,又调试了好几次才完全写对的。在下一篇文章中,我将介绍另一种更简洁的方式,即使用VFL来添加约束,敬请期待吧。