目前网络上已经有很多关于AutoLayout的讲义可供大家学习,大部分的Demo都是通过IB或者Storyboard上完成的。很多人也在思考,到目前iOS 8这个版本,使用代码来实现UI布局是不是合适?今天有时间,使用纯代码写了一小段布局代码,供大家比较。
本文所需要实现的界面布局来自这一篇博客:ADAPTIVE LAYOUTS FOR iPHONE 6,对应的中文翻译版本为:为iPhone6设计自适应布局。读者可以先行阅读以上这篇博客,来了解AutoLayout和Size Class的基本概念,跟着博客中的步骤,使用Storyboard完成上述Demo。
需求
本文Demo需要同时在iPhone 4, iPhone 5, iPhone 6 和 iPhone 6 Plus完成以下布局
并且支持横竖屏旋转,旋转动画如下:
本文的示例代码可以在Github上直接获取。
分析
通过参考图,我们可以发现需要实现的页面布局中:最外层是一个NavigationController。内容区域,在竖屏情况下,把个人信息从上到下布局,在横屏情况下,从左到右布局。
内容区域代码实现
1) 初始化控件
- (void)loadView
{
[super loadView];
self.avatarImageView = [self createAvatarImageView];
[self.view addSubview:self.avatarImageView];
self.nameLabel = [self createNameLabel];
[self.view addSubview:self.nameLabel];
self.timeLabel = [self createTimeLabel];
[self.view addSubview:self.timeLabel];
self.descriptionLabel = [self createDescriptionLabel];
[self.view addSubview:self.descriptionLabel];
self.photoImageView = [self createPhotoImageView];
[self.view addSubview:self.photoImageView];
}
- (UIImageView *)createAvatarImageView
{
UIImageView *avatarImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"avatar"]];
avatarImageView.translatesAutoresizingMaskIntoConstraints = NO;
return avatarImageView;
}
- (UILabel *)createNameLabel
{
UILabel *nameLabel = [[UILabel alloc] initWithFrame:CGRectZero];
nameLabel.font = [UIFont systemFontOfSize:12.0f];
nameLabel.translatesAutoresizingMaskIntoConstraints = NO;
nameLabel.text = @"Chun Tips";
return nameLabel;
}
- (UILabel *)createTimeLabel
{
UILabel *timeLabel = [[UILabel alloc] initWithFrame:CGRectZero];
timeLabel.font = [UIFont systemFontOfSize:11.0f];
timeLabel.translatesAutoresizingMaskIntoConstraints = NO;
timeLabel.text = @"2w ago";
return timeLabel;
}
- (UILabel *)createDescriptionLabel
{
UILabel *descriptionLabel = [[UILabel alloc] initWithFrame:CGRectZero];
descriptionLabel.font = [UIFont systemFontOfSize:11.0f];
descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO;
descriptionLabel.text = @"Apple, Google, Microsoft, Instagram, Twitter, Facebook and 4 others like this";
descriptionLabel.numberOfLines = 0;
return descriptionLabel;
}
- (UIImageView *)createPhotoImageView
{
UIImageView *photoImageView = [[UIImageView alloc] initWithFrame:CGRectZero];
photoImageView.translatesAutoresizingMaskIntoConstraints = NO;
photoImageView.image = [UIImage imageNamed:@"photo"];
return photoImageView;
}
以上代码,和以往手写代码布局基本雷同。有三点需要注意的地方:
- 使用AutoLayout布局时初始化控件的frame应该为 CGRectZero
- 对于使用AutoLayout布局的控件应该设置translatesAutoresizingMaskIntoConstraints为 NO
- 如果需要让UILabel在AutoLayout中正确的支持多行显示,需要设置numberOfLines==0(默认为1),并且在适当的地方设置 preferredMaxLayoutWidth 这个值
2) 初始化控件好了之后,我们就应该开始让控件支持自动布局:
- (void)updateConstraintsForTraitCollection:(UITraitCollection *)collection
{
//等待实现
}
- (vodi)loadView
{
//...
//上面已经实现过的代码
[self updateConstraintsForTraitCollection:self.traitCollection];
}
需求中我们需要支持iPhone设备的横竖两个方向,在Size Class概念中,UITraitCollection包含了我们所需要的信息。所以我们依据当前ViewController的traitCollection来构造不同的布局约束。
在设备的traitCollection改变时(旋转),我们可以在willTransitionToTraitCollection方法中捕获到相应信息,并开始做旋转动画:(关于该方法的详细描述,可以参考上一篇博客)
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
[super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
[self updateConstraintsForTraitCollection:newCollection];
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
[self updateConstraintsForTraitCollection:newCollection];
[self.view setNeedsLayout];
} completion:nil];
}
3)好了,最后一步,我们开始实现布局代码。
3.1 初始化我们在布局约束中需要的所有Views和一些常量值,放到字典里。初始化需要更新的constraits,放到数组里。
CGFloat const kLayoutPadding = 10.0f; // 每个控件之间的间距为10.0f
- (void)updateConstraintsForTraitCollection:(UITraitCollection *)collection
{
NSDictionary *views = @{@"topLayoutGuide": self.topLayoutGuide, @"avatarImageView": self.avatarImageView, @"nameLabel": self.nameLabel, @"timeLabel": self.timeLabel, @"descriptionLabel": self.descriptionLabel, @"photoImageView": self.photoImageView};
NSDictionary *metrics = @{@"padding": @(kLayoutPadding)};
NSMutableArray *updateConstraits = [NSMutableArray array];
}
上面提到的topLayoutGuide属性,在当前的场景下,它对应的NavigationBar,高度也为NavigationBar的高度。有不知道的朋友可以参考之前写过的一篇博客,有详细叙述。
3.2 从Size Class的文档中,我们可以查询到,iPhone在竖屏情况下,水平方向是 Compact, 竖直方向是 Regular,而在横屏情况下,两个方向都是 Compact。从我们的设计稿的需求,我们可以通过判断竖屏方向的Size Class来进行分别约束:
- (void)updateConstraintsForTraitCollection:(UITraitCollection *)collection
{
//...
//上面已经实现过的代码
if (collection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
// 横屏情况
} else {
// 竖屏情况
}
if (self.constraits) {
[NSLayoutConstraint deactivateConstraints:self.constraits];
}
self.constraits = updateConstraits;
[NSLayoutConstraint activateConstraints:self.constraits];
}
完成布局约束之后,使用activateConstraints激活布局约束。如果之前有旧的布局约束,应该先使用deactivateConstraints移除。
3.3 实现约束代码:
// 横屏
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|[photoImageView]-(padding)-[avatarImageView]-(padding)-[nameLabel]" options:0 metrics:metrics views:views]];
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"[avatarImageView]-(padding)-[timeLabel]" options:0 metrics:metrics views:views]];
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"[photoImageView]-(padding)-[descriptionLabel]" options:0 metrics:metrics views:views]];
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide][photoImageView]|" options:0 metrics:metrics views:views]];
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-(padding)-[avatarImageView]-(padding)-[descriptionLabel]" options:0 metrics:metrics views:views]];
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-(padding)-[nameLabel]-(padding)-[timeLabel]" options:0 metrics:metrics views:views]];
self.descriptionLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - self.view.bounds.size.height + self.topLayoutGuide.length - 2 * kLayoutPadding; //label在横屏下能显示的最大宽度 = 屏幕宽度 - 图片宽度(屏幕高度 - NavigatioBar的高度) - label本身显示的左右间距。
// 竖屏
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|-(padding)-[avatarImageView]-(padding)-[nameLabel]" options:0 metrics:metrics views:views]];
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"[avatarImageView]-(padding)-[timeLabel]" options:0 metrics:metrics views:views]];
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|[photoImageView]|" options:0 metrics:nil views:views]];
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"|-(padding)-[descriptionLabel]" options:0 metrics:metrics views:views]];
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-(padding)-[avatarImageView]-(padding)-[photoImageView]-(padding)-[descriptionLabel]" options:0 metrics:metrics views:views]];
[updateConstraits addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-(padding)-[nameLabel]-(padding)-[timeLabel]" options:0 metrics:metrics views:views]];
self.descriptionLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 2 * kLayoutPadding; //label在竖屏下能显示的最大宽度 = 屏幕宽度 - label本身显示的左右间距。
// 共享代码
[updateConstraits addObject:[NSLayoutConstraint constraintWithItem:self.avatarImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.avatarImageView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]];
[updateConstraits addObject:[NSLayoutConstraint constraintWithItem:self.photoImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.photoImageView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]];
上述布局代码中,有两点需要特别提出:
- 需要支持多行显示的descriptionLabel应该在不同界面显示的情况下更新preferredMaxLayoutWidth。
- 共享代码是让图片在横竖屏的情况下实现宽高等比(需求是1:1,显示为正方形)。
总结
上述通过纯代码实现了和这篇博客一样的Demo:ADAPTIVE LAYOUTS FOR iPHONE 6。不知道有兴趣的朋友在使用过两个方式实现UI布局之后,有什么感悟,觉得哪一种更加适合现在的生产开发。
在写这篇博客之前,在公司项目中,还一直保留着纯代码使用AutoLayout实现UI的方法。通过最近对Storyboard和IB新功能的进一步了解,会开始尝试在自己的项目中使用。