UI篇
UI是一个iOS开发工程师的基本功。
怎么说?
UI本质上就是你调用苹果提供给你的API来完成设计师的设计。
所以,想提升UI的功力也很简单,没事就看看UIKit里的各个类的头文件。如果能做到烂熟于胸,相信会有很大的提升。
Autolayout
顾名思义,Autolayout = 自动+布局,也就是当你设置好一定的约束之后,系统会帮你处理布局的细节。
那么,在不那么自动的年代,我们用的是什么?
我们用的是Frame布局。
那么,先来讨论一下Frame布局有哪些问题?
举个简单的例子好了。
如图。
代码如下。
- (void)viewDidLoad { [super viewDidLoad]; redView = [UIView new]; redView.frame = CGRectMake(0, 200, 200, 200); redView.backgroundColor = [UIColor redColor]; [self.view addSubview:redView]; yellowView = [UIView new]; yellowView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; yellowView.frame = CGRectInset(redView.bounds, 20, 20); yellowView.backgroundColor = [UIColor yellowColor]; [redView addSubview:yellowView]; // Do any additional setup after loading the view, typically from a nib. }
图中黄色的View是红色View的子View,那么,如果我期望无论红色View变大还是变小,黄色View距离红色View的边距总是不变的,该怎么做呢?
一般来说有两种做法。
- 设置黄色View的
autoresizingMask
属性,设置为UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight
,这样设置的结果是黄色View的宽高会随着父View宽高的改变而改变,但是不改变间距。 - 继承一个UIView,在UIView里的
layoutSubviews
里设置子View的宽高。如下。
- (void)layoutSubviews
{
yellowView.frame = CGRectInset(self.bounds, 20, 20);
[super layoutSubviews];
}
实际上这两种做法都是很麻烦而且不灵活的。例如autoresizingMask
对应的UIViewAutoresizing
模式只有6种,我们常常用到的居中对齐等完全没有。二是如果在layoutSubviews
里设置布局,又会造成如果父View有动画,那么会出现奇怪的动画效果。
所以,为了解除这些痛点,苹果公司为我们带来了Autolayout。
Autolayout的用法
在这里,我会假设看本书的都是会使用Autolayout基本功能的朋友。所以,我会直接讲解一些较为深入和偏实战的东西。
1. Intrinsic Content Size
思考一下,当你拉约束的时候,为什么UILabel和UIButton只需要拉能够确定坐标的约束即可,而不需要确定宽高的约束?比如说。
这个UILabel就是只确定了左边距和上边距,就可以了。但是如果我们放了一个UIView,只确定坐标而不指定大小,就会出错。例如。
Xcode会提示,这个View还需要指定宽和高。
造成这种情况的原因就是,有些View可以通过自己的内容计算宽高,而有些View不可以。这也是我们要讲的内容,也即我们的标题,Intrinsic Content Size
,翻译过来就是,固有内容大小。
对应的系统方法就是- (CGSize)intrinsicContentSize
要知道,Autolayout的本质无非就是系统通过你设置的约束来帮你计算一个控件的位置和大小。
所以,一个UILabel肯定具备的四个条件是,内容、字体、行数、换行模式。也就是说,只要我们赋予了Label内容,那么它的大小也就确定了。所以,我们不需要特意指定一个UIlabel的宽高,除非你有什么特殊要求。
那么UIView为什么不可以自己计算大小?
答案其实也可以猜到,因为他没有内容。
来看一个例子。
首先,我们在storyboard上放置一个UILabel,然后设置Leading和Trailing。如图。
运行模拟器,看一下效果。
我们会发现,这样设置的UILabel,它的大小总是会和内容大小刚好一致,但是如果我们期望UILabel的大小总是比内容宽高都大一些,也就是所谓的留白。比如这样。
那么,我们应该怎么做呢?
首先,我们创建一个继承于UILabel的自定义试图,然后重写
- (CGSize)intrinsicContentSize,这个方法。代码如下。 - (CGSize)intrinsicContentSize { CGSize originalSize = [super intrinsicContentSize]; CGSize size = CGSizeMake(originalSize.width+20, originalSize.height+20); return size; }
上述代码的意思就是,我们先获取系统通过Label的内容计算出来的宽和高,再分别给他增大再返回新的Size就可以了。再运行一下,你就会发现,Label的大小就会比内容大了。(别忘了,把对齐方式设置为居中)
再回到之前的那个问题,UIView如果只设置坐标,不设置大小会报错的问题。
如果是用代码写约束,如果你只想设置坐标不想设置大小,那么你需要像上面的代码一样,在- (CGSize)intrinsicContentSize
为你的UIView指定一个默认大小。
如果是在XIB里,那么你需要在下图这个Instrinsic Size
的属性里设置为Placeholder
。这样,Xcode就不会报错了。
例子
其实看完上面的叙述,你会思考,到底什么情况下,一个UIView需要只设置坐标不设置大小呢?
其实这种场景相当普遍。比如,我们常常会碰到,一个View中有两个Label,两个Label的高度均和内容有关,这时候,你的View的高度就必须由两个Label的高度有关,而不能一开始就定死。例如。
一个已知宽度的UIView中,有两个UILabel,我希望这个UIView的高度由两个UIlabel的高度来确定。效果如下图。
也就是说,图中红色view的高度是和两个UIlabel相关联的。我们尝试来实现它。
首先,在storyboard上拉取一个UIView。设置背景色为红色。如图。
我们为这个红色View设置了3个约束,分别是。
- Leading space to SuperView:8
- Trailing space to SuperView:8
- Top Space to SuperView:8
也就是分别设置了View的左边距,右边距和上边距,熟悉约束的人应该知道,这时候View的约束是不够的。为什么?
因为,左边距和上边距确定了View的(x,y),然后左边距和右边距确定了这个View的宽度,我们缺少了Height。
但是这个Height,要由View内部的两个label来确定,为了让Xcode不再认为我们拉的约束有问题,再结合我们上面讲的Intrinsic Content Size
,我们可以在Xib的这个位置设置Intrinsic Size
为Placeholder
,这样,Xcode就认为这个View有默认的大小,所以就不会报错了。
然后,自然是放入两个UILabel了。如图。
为了label能够多行显示,别忘记设置lineofnumber为0.
还有一点需要注意的是,两个UILabel之间肯定是需要一个设置一个垂直约束的,否则整个View就没有办法确定自己的高度,思考一下这是为什么?
运行一下,看看结果。
成功了。
2. Content Hugging Priority和Content Compression Resistance
这两个概念需要结合我们上面讲的Intrinsic Content Size
来理解,每一个控件都有一个系统计算的最佳大小。
所以,Content Hugging Priority
这个属性就代表着,一个控件拒绝本身size大于InstrinsicSize
的优先级。
那么Content Compression Resistance
,这个属性代表着,一个控件拒绝本身size小于InstrinsicSize
的优先级。
这样子说还是有点抽象。那么我们来看一个例子好了。
我们在storyboard中创建了一个UITableView,并在其中创建了一个自定义的UITableViewCell。
这个自定义的UITableviewCell中有两个Label,都是多行显示的Label。然后给他们设定约束。
第一个Label设定的约束为:
- Leading Space:8
- Trailing Sace:8
- Top Space:8
- Bottom Space(距离下面的Label的间距): 9
第二个Label设定的约束为:
- Leading Space:8
- Trailing Sace:8
- Bottom Space:8
- Top Space(距离上面面的Label的间距): 9
设定好了,ok,好像没什么问题。如图。
但是,如果这时候你把UITableViewCell的高度扩大。看看是怎样的结果。如图。
Xcode报错了。为什么?
因为Cell的高度扩大,势必会影响两个Label的位置和大小,所以,现在Label面临着一个问题,在保持和父View(也就是UITableViewCell的contentView)间距不变的情况下,必须有一个Label是需要妥协的,怎么个妥协法呢?就是要扩大高度,扩大高度,就意味着比label本身的文字内容的高要大了。
那么,到底是两个Label中的哪一个label做出这个妥协呢?
Xcode并不知道,因为这个不知道,所以Xcode报错了。
话说回来了,Xcode为什么会不知道,就是因为两个Label的Content Hugging Priority
里面的Vertical
这个属性的值是一样的。因为两个Label拒绝变高的优先级相同,以至于Xcode不知道到底该拉伸哪个Label。所以,解决方案就是,把其中一个的改小。如图。
这样就可以了。那么,这样就结束了么?
如果这时候,你再把UITableViewCell的高度减小,会发生什么情况呢?Xcode又报错了。那么,这次的原因又是什么?
其实和上一个原因较为类似。
Cell的高度减小导致UILabel为了保持和父View间距不变,所以面临一个高度压缩的情况,那么到底谁压缩,Xcode依然不知道,因为两个Label的Content Compression Resistance
这个属性里的Vertical
优先级一样,和上面的解决办法一样只需要改小一个即可。
如果是手写Autolayout,在哪里写最好?
其实这个东西我在Reviewcode.cn里讨论过。但是鉴于可能会有人没有看过,并且对这个问题存疑,还是在这里说一下。
结论如下:
- 如果是在自定义view中,写在init方法中。
- 如果是在ViewController中,写在
- (void)viewDidLoad()
中。
为什么不能写在viewDidAppear
或者viewWillAppear
中?
因为这个东西和NSNotification
是一样的,你不能确定viewDidAppear
和viewWillAppear
调用的时机和调用的次序,不信的话,你可以用NSLog打印一下,并且使用手势在NavigationController中不停的左右滑动控制器,看看打印的结果。
但,viewDidLoad
是可以保证在整个生命周期只出现一次的。为了避免约束重复添加,所以你应该在viewDidLoad
中添加。