iOS自动布局3-UIScrollView

iOS的视图中,UIScrollView是比较常用的视图。但是UIScrollView在自动布局中是一种特殊的视图。

不使用自动布局

假设需要实现一个简单的需求:在一个UIScrollView中添加UILabel,标签中放置一个很长的字符串,要求可以根据字符串的长度滚动。先来看一下不使用自动布局来绘制UIScrollView:

- (void)setupWithFrame{
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width,self.view.bounds.size.height-100)];
    scrollView.backgroundColor = [UIColor yellowColor];
    scrollView.contentSize = CGSizeMake(self.view.bounds.size.width,700);
    [self.view addSubview:scrollView];
    
    UILabel *dataLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 700)];
    dataLabel.numberOfLines= 0;
    dataLabel.text = _longText;
    [scrollView addSubview:dataLabel];
}

实现的结果是这样的:

scroll_frame_only

这样实现的问题就在于,不知道UILabel占用的长度的时候,无法正确给出UIScrollView的contentSize。如果contentSize中的长度设置得太短,就会跟上图一样显示不完全(剩余的部分被截取为…)。如果设置得太长就会有多余的空白区域。

使用自动布局

那么如果希望使用自动布局,利用好UILabel的intrinsic content size。那应该怎么写呢?

第一版的代码如下:

- (void)setupWithAutoLayout1{
    UIScrollView *scrollView = [[UIScrollView alloc] init];
    scrollView.backgroundColor = [UIColor yellowColor];
    scrollView.delegate = self;
    [self.view addSubview:scrollView];
    [scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
    
    UILabel *dataLabel = [[UILabel alloc] init];
    dataLabel.numberOfLines= 0;
    dataLabel.text = _longText;
    [scrollView addSubview:dataLabel];
    [dataLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(scrollView).offset(16);
        make.top.equalTo(scrollView).offset(16+64);
        make.right.lessThanOrEqualTo(scrollView).offset(-16);
        make.bottom.equalTo(scrollView).offset(-16);
    }];
}

运行代码,得到的结果如图:

scroll_wrong

???

什么情况呢?没有任何内容显示在上面?查看Xcode的内容调试面板。看到scrollView的约束中有个紫色的感叹号图标,点击以后显示"Scrollable content size is ambiguous for UIScrollView",UIScrollView的contentSize是不确定的。

根据这个提示,我尝试给scrollView确定的scrollView,于是第二个版本的代码变成:

    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width,self.view.bounds.size.height-100)];
    scrollView.backgroundColor = [UIColor yellowColor];
    scrollView.contentSize = CGSizeMake(self.view.bounds.size.width,700);
    [self.view addSubview:scrollView];
    
    UILabel *dataLabel = [[UILabel alloc] init];
    dataLabel.numberOfLines= 0;
    dataLabel.text = _longText;
    [scrollView addSubview:dataLabel];
    [dataLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(scrollView).offset(16);
        make.top.equalTo(scrollView).offset(16+64);
        make.right.lessThanOrEqualTo(scrollView).offset(-16);
        make.bottom.equalTo(scrollView).offset(-16);
    }];

用frame的方式初始化scrollView并指定contentSize,仍然用自动布局的方式绘制dataLabel。

运行代码,发现结果是一样的。dataLabel中的文字并没有显示出来,也仍然有"Scrollable content size is ambiguous for UIScrollView"的警告。

这是为什么呢?我们还是从第一个版本的代码开始分析,为什会系统会认为scrollView的contentSize是不确定的呢?

首先我们给scrollView添加了一个edges的约束。相当于scrollView的left/right/top/bottom都和父视图相等。这里相当于确定了scrollView的frame了。

然后在scrollView中添加了一个dataLabel。

为dataLabel也设置了left/right/top/bottom四个约束,对于一般的视图而言,有这四个试图就意味着视图位置的唯一性了。但是UIScrollView是一种特殊的视图,它除了具有普通视图的frame属性以外,还具有内容区域。frame是UIScrollView中的可见部分,而UIScrollView中的其它部分都包含在其内容区域中。想象一下UITableView这种特殊的UIScrollView,第一行cell其实如果我们要设置布局约束的话,大概会像是这样:

cell1.top = tableView.contentView.top;

注意UIScrollView并没有contentView这个属性,这里只是我们想象出来的视图。

那么当我们在UIScrollView中的子视图中添加约束的时候,我们添加的约束是针对UIScrollView本身的可见区域呢,还是其内容区域呢?

以下是官方的解释(https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithScrollViews.html#//apple_ref/doc/uid/TP40010853-CH24-SW1):

  • Any constraints between the scroll view and objects outside the scroll view attach to the scroll view’s frame, just as with any other view.
  • For constraints between the scroll view and its content, the behavior varies depending on the attributes being constrained:
    • Constraints between the edges or margins of the scroll view and its content attach to the scroll view’s content area.
    • Constraints between the height, width, or centers attach to the scroll view’s frame.
  • You can also use constraints between the scroll view’s content and objects outside the scroll view to provide a fixed position for the scroll view’s content, making that content appear to float over the scroll view.

大概意思是:

对于UIScrollView和其它非子视图的约束,采用的方式和其它的视图类似,也就是采用其可见区域的left,right,top,bottom等;

对于UIScrollView和它的子视图的约束而言(上面的例子就是),left,right,top,bottom采用的是UIScrollView的内容区域,而width和height则任然是其可见区域的width和height。

你可以为UIScrollView中的子视图和UIScrollView外的视图添加一个固定位置的约束,这样可以达到让该子视图浮动在UIScrollView上面的效果。(想象一下UITableview的section header,当tableview 在滚动的时候,section header是固定在可见区域的顶部的)。

所以说为什么系统会警告说UIScrollView没有准确的content size呢?因为我们在dataLabel中添加的约束都是针对scrollView的内容区域而言的。看以下这个约束:

make.right.lessThanOrEqualTo(scrollView).offset(-16);

这个时候scrollView的内容区域的右边界还不知道呢,它可以是100,1000,10000,或者是0,所以对于一个不定宽度的UILabel而言,自然没办法计算出其应该占用的大小是多少。

那么应该如何修正,才能达到需求想要的效果呢?

第三版的代码如下:

- (void)setupWithAutoLayout2{
    UIScrollView *scrollView = [[UIScrollView alloc] init];
    scrollView.backgroundColor = [UIColor yellowColor];
    scrollView.delegate = self;
    [self.view addSubview:scrollView];
    [scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
    
    UILabel *dataLabel = [[UILabel alloc] init];
    dataLabel.numberOfLines= 0;
    dataLabel.text = _longText;
    [scrollView addSubview:dataLabel];
    [dataLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(scrollView).offset(16);
        make.top.equalTo(scrollView).offset(16+64);
        make.right.lessThanOrEqualTo(self.view).offset(-16);
        make.right.equalTo(scrollView).offset(16);
        make.bottom.equalTo(scrollView).offset(-16);
    }];
}

很简单,只要加多一个约束,让dataLabel的right属性跟self.view的right属性建立约束关系。这个时候由于dataLabel的right属性同时还跟scrollView的内容区域的right属性有约束关系,就可以间接地计算出scrollView的内容区域的right属性。而有了确定的右边界,dataLabel就可以根据font跟text计算出其字符串所占用的高度。因此dataLabel的bottom属性得以确定,又dataLabel的bottom属性和scrollView的内容区域的bottom属性有约束关系,因此确定了scrollView的内容区域中的right属性。对于UIScrollView而言,默认的contentOffset是(0,0)。也就是默认的(静止的时候)UIScrollView的内容区域的top和left都跟其可见区域是一致的。所以此时可以确定scrollView的top跟left。即整个scrollView的content size已经确定好了。就不会再出现警告。

运行结果如图:

scroll_right

那么第二版本中手动设置contentSize为什么不行呢?猜测是由于dataLabel设置约束的时候会改变scrollView的contentSize属性。将第二版的代码修改为:

- (void)setupWithFrameAndAutoLayout2{
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width,self.view.bounds.size.height-100)];
    scrollView.backgroundColor = [UIColor yellowColor];
    scrollView.contentSize = CGSizeMake(self.view.bounds.size.width,1);
    [self.view addSubview:scrollView];
    
    UILabel *dataLabel = [[UILabel alloc] init];
    dataLabel.numberOfLines= 0;
    dataLabel.text = _longText;
    [scrollView addSubview:dataLabel];
    [dataLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(scrollView).offset(16);
        make.top.equalTo(scrollView).offset(16+64);
        make.right.lessThanOrEqualTo(self.view).offset(-16);
        make.right.equalTo(scrollView).offset(16);
        make.bottom.equalTo(scrollView).offset(-16);
    }];
}

此时将content size的高度设置为1,但是其运行结果是正确的(和第三版的代码运行结果一样)。这个结果和我们的猜测一致。

需要注意的是,现在我们的界面是非常简单的,UIScrollView中只有一个UILabel,当UIScrollView中的子视图变得越来越多的时候,需要注意的地方就更多。假如有视图A,B,C,D...等是UIScrollView的直接子视图,当它们需要添加与UIScrollView的约束时,都要考虑到约束是和UIScrollView的内容区域关联而不是其可见区域。要考虑什么时候要借助UIScrollView外部的视图的属性建立约束。

UIScrollView的自动布局技巧

那有没有什么方法可以尽量让布局的代码显得更加清晰,减少出错的几率呢?

注意上面一直都提到“UIScrollView的内容区域”,子视图中的约束是和内容区域相关联的,但是实际上这个区域不在UIScrollView的属性内,那么是不是可以人为地给UIScrollView添加一个“内容区域”的视图呢?所有本来直接在UIScrollView下面的视图,都变成在额外添加的“内容区域”视图中,那么所有的约束都是和该视图关联,就不用再去考虑UIScrollView的特殊的地方了。这样做的话,只需要保证在添加“内容区域”视图的时候考虑一次与UIScrollView的约束关系就可以了。

修改后的代码如下:

- (void)setupViewsWithContentView{
    UIScrollView *scrollView = [[UIScrollView alloc] init];
    scrollView.backgroundColor = [UIColor yellowColor];
    scrollView.delegate = self;
    [self.view addSubview:scrollView];
    [scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
    
    UIView *contentView = [[UIView alloc] init];
    
    [scrollView addSubview:contentView];
    [contentView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(scrollView);
        make.width.equalTo(scrollView);
    }];
    contentView.backgroundColor = [UIColor lightGrayColor];
    
    UILabel *dataLabel = [[UILabel alloc] init];
    [contentView addSubview:dataLabel];
    [dataLabel mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(contentView).offset(16);
        make.left.equalTo(contentView).offset(16);
        make.right.lessThanOrEqualTo(contentView).offset(-16);
        make.bottom.equalTo(contentView.mas_bottom).offset(-16);
    }];
    dataLabel.numberOfLines = 0;
    dataLabel.text = _longText;
}

可以看到此时我们在添加dataLabel的时候就很方便了。只需要添加其与contentView的约束就可以了。

这里的关键点在于contentView与scrollView的约束的建立。首先让contentView的边界和scrollView的内容区域的边界相等。注意此时contentView的大小是不确定的,可以伸缩。那么根据我们的需求,我们只需要固定的宽度,然后让高度是可变的。那么我们添加宽度一个宽度的约束(根据官方文档,UIScrollView的width和height属性是其可见区域的属性)。这样contentView就只有高度是不确定的,那么我们通过dataLabel和scrollView的约束中计算出contentView的高度即可。

下面贴上官方文档对于设置UIScrollView自动布局约束的技巧:

  1. Add the scroll view to the scene.

  2. Draw constraints to define the scroll view’s size and position, as normal.

  3. Add a view to the scroll view. Set the view’s Xcode specific label to Content View.

  4. Pin the content view’s top, bottom, leading, and trailing edges to the scroll view’s corresponding edges. The content view now defines the scroll view’s content area.

    REMEMBER The content view does not have a fixed size at this point. It can stretch and grow to fit any views and controls you place inside it.

  5. (Optional) To disable horizontal scrolling, set the content view’s width equal to the scroll view’s width. The content view now fills the scroll view horizontally.

  6. (Optional) To disable vertical scrolling, set the content view’s height equal to the scroll view’s height. The content view now fills the scroll view horizontally.

  7. Lay out the scroll view’s content inside the content view. Use constraints to position the content inside the content view as normal.

    IMPORTANT: Your layout must fully define the size of the content view (except where defined in steps 5 and 6). To set the height based on the intrinsic size of your content, you must have an unbroken chain of constraints and views stretching from the content view’s top edge to its bottom edge. Similarly, to set the width, you must have an unbroken chain of constraints and views from the content view’s leading edge to its trailing edge.If your content does not have an intrinsic content size, you must add the appropriate size constraints, either to the content view or to the content.When the content view is taller than the scroll view, the scroll view enables vertical scrolling. When the content view is wider than the scroll view, the scroll view enables horizontal scrolling. Otherwise, scrolling is disabled by default.

大意为:

  1. 添加一个scroll view

  2. 像普通视图一样为scroll view添加位置和大小的约束

  3. 在scroll view中添加一个子视图(content view),给该视图添加一个指定的标签(这个标签只是为了更好地显示)

  4. 将content view的left,right,top,bottom和scroll view的边界建立相等约束。那么现在content view的边界就确定了scroll view的内容区域

    (注意此时content view还没有固定的大小,它可以根据你在其中设置的视图的伸缩大小)

  5. (可选)如果不需要水平滑动,将content view的宽度设置为和scoll view的宽度相等。

  6. (可选)如果不需要垂直滑动,将content view的高度设置为和scroll view的高度相等。

  7. 在content view中添加子视图,为子视图和content view添加约束。

重要: 你的布局必须能够决定content view的大小(除非在5和6中已经设置过了)。如果要基于你的内容的固有尺寸来决定高度,那么在content view的top跟bottom之间必须有一条不间断的约束链。类似地,对于宽度,必须要在left和right间有不间断的约束链。如果你在content view中添加的内容(子视图)不具有固有尺寸,那么你要显式地为content view或者其内容确定好合适的尺寸。当content view的高度大于scroll view的高度,那么scroll view支持垂直方向的滑动。当content view的宽度大于scroll view的宽度,那么scoll view支持水平方向上的滑动。否则,默认滑动是被禁止的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容