iOS 10 Auto Layout界面自动布局系列3-使用原生NSLayoutConstraint添加布局约束
本系列的第一篇文章介绍了自动布局的基本原理,第二篇文章通过一个简单的例子演示了如何使用Xcode的Interface Builder(简称IB)以可视化方式添加约束。本篇为该系列的第三篇文章,主要介绍如何通过写代码来添加布局约束。
说句题外话,通过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 (nullable, 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, ......//省略剩余 };
每一个枚举值代表了一个布局属性,名字都很直观,例如Left代表左侧,Height代表高度等等。注意,firstItem与secondItem不一定非得是同样的值,允许定义诸如某视图的高度等于另一个视图的宽度这样的约束(尽管很少这样做)。NSLayoutAttributeNotAnAttribute这个额外解释一下,当我们需要为某个视图指定固定宽度或者高度时,这时候secondItem为nil,secondAttribute为NSLayoutAttributeNotAnAttribute。
relation定义了布局关系(NSLayoutRelation):
typedef NS_ENUM(NSInteger, NSLayoutRelation) {
NSLayoutRelationLessThanOrEqual = -1,
NSLayoutRelationEqual = 0,
NSLayoutRelationGreaterThanOrEqual = 1, };
布局关系不仅限于相等,还可以是大于等于或者小于等于,这种不等关系在处理UILabel、UIImageView等具有自身内容尺寸的控件(自身内容尺寸参见本系列第五篇文章)时非常常用。举个简单的例子,UILabel的长度会随文字的长度而变化,那么我们可以向UILabel控件添加两个约束,分别是“长度大于等于50”与“长度小于等于200”。这样,当文字很少时,宽度也至少为50;当文字非常多时,宽度也不会超过200。
multiplier即比例系数。constant即常量。
因此,每个约束就对应如下关系:
firstItem.firstAttribute {==,<=,>=} secondItem.secondAttribute * multiplier + constant (公式3.2)
我们可以调用NSLayoutConstraint类的constraintWithItem:…方法,传入所有需要的参数构造一个新的约束。
理论就到此为止,下面我们还是以第二篇的例子来讲解如何使用代码添加约束。
打开Xcode(8.2.1版),新建Single View Application项目,项目命名为AutoLayoutByConstraint,本文使用Objective-C讲解,设备选择Universal。下载苹果Logo图片apple.jpg,并将其拖入项目中。文件下载地址:
链接:https://pan.baidu.com/s/1b5AqDo 密码:e4ff
首先,界面上方用来显示苹果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:logoImageView.superview attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f]; //logoImageView右侧与父视图右侧对齐
- NSLayoutConstraint* rightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:logoImageView.superview attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f]; //logoImageView顶部与父视图顶部对齐
- NSLayoutConstraint* topConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:logoImageView.superview attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f]; //logoImageView高度为父视图高度一半
- NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:logoImageView.superview 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;
UIView类提供了若干方法和属性,用于添加或者移除约束。对于iOS 6或者iOS 7可以调用addConstraint(s):和removeConstraint(s):方法;对于iOS 8及更新的版本,直接设置约束的active属性(BOOL值)或者调用activateConstraints:与deactivateConstraints:类方法。
就是这么简单!现在编译并运行项目,
貌似logoImageView的尺寸不太对。如果在viewDidLoad方法中设置self.view的背景色为红色,看得会更清楚:
同时注意到Xcode控制台打印出了一大段信息:
- 2017-02-15 16:44:13.453948 AutoLayoutByConstraint[17260:1271951] [LayoutConstraints] Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) ( "<NSAutoresizingMaskLayoutConstraint:0x608000086fe0 h=--& v=--& UIImageView:0x7facc44020a0.width == 241 (active)>", "<NSLayoutConstraint:0x6000000871c0 H:|-(0)-[UIImageView:0x7facc44020a0] (active, names: '|':UIView:0x7facc4506110 )>", "<NSLayoutConstraint:0x600000087210 UIImageView:0x7facc44020a0.trailing == UIView:0x7facc4506110.trailing (active)>", "<NSLayoutConstraint:0x6080000871c0 'UIView-Encapsulated-Layout-Width' UIView:0x7facc4506110.width == 320 (active)>" ) //后面省略若干字。。。我简要翻译一下: 不能同时满足约束。或许下列约束中的其中一个是你并不想要的。尝试如下方法: (1) 检查每个约束,试着找出并不期望的约束。 (2) 找到添加该约束的代码,并进行修正。 (备注:如果你看到NSAutoresizingMaskLayoutConstraint却并不理解,请查阅UIview文档中的translatesAutoresizingMaskIntoConstraints属性。)
看来是出错了,为什么会这样?这是由于自动布局技术是苹果在iOS 6当中新加入的,但在那时仍然有很多项目代码使用autoresizingMask与setFrame:的方式构建界面。试想,如果将一个已经设置好frame并使用autoresizingMask的视图添加到一个使用自动布局的视图中时,运行时需要隐式地将前者的frame和autoresizingMask转化为自动布局约束(这些隐式转换的约束的类型为NSAutoresizingMaskLayoutConstraint),这样才能明确其位置与尺寸而不会导致约束的缺失。这个隐式转换的过程,是由UIView的translatesAutoresizingMaskIntoConstraints属性的值决定的。默认情况下,为了保证兼容性,该值为YES,表示需要自动进行隐式转换。这对于兼容旧的代码当然是好的,然而当我们明确为视图添加了约束后,我们就不希望再进行autoresizingMask的隐式转换了,否则就会引起约束的冲突。因此,需要特别注意的是,当我们使用代码创建视图时,需要将translatesAutoresizingMaskIntoConstraints属性的值设置为NO。在viewDidLoad方法中创建logoImageView的代码之后,添加如下代码:
- 1 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; }
程序最终项目文件链接:链接: https://pan.baidu.com/s/1jIdpPK6 密码: nb3u
自动布局约束是通过描述视图间的关系而非强加坐标值来进行定位的,它更能满足不同设备尺寸的界面布局,并且更容易让人理解。虽然上面的代码很冗长,但每一句所描述的事实都十分清楚。在此省略自动布局的好处10000字。。。
区区几个简单的视图,就要写这么长的代码。。。
估计你看得有点眼花缭乱了吧,其实我也是修改并检查了好几次,又调试了好几次才完全写对的。在下一篇文章中,我将介绍另一种更简洁的方式,即使用VFL来添加约束,敬请期待吧。