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的內容調試面板??吹絪crollView的約束中有個紫色的感嘆號圖標,點擊以后顯示"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還沒有固定的大小,它可以根據你在其中設置的視圖的伸縮大?。?/p>

  5. (可選)如果不需要水平滑動,將content view的寬度設置為和scoll view的寬度相等。

  6. (可選)如果不需要垂直滑動,將content view的高度設置為和scroll view的高度相等。

  7. 在content view中添加子視圖,為子視圖和content view添加約束。

重要: 你的布局必須能夠決定content view的大?。ǔ窃?和6中已經設置過了)。如果要基于你的內容的固有尺寸來決定高度,那么在content view的top跟bottom之間必須有一條不間斷的約束鏈。類似地,對于寬度,必須要在left和right間有不間斷的約束鏈。如果你在content view中添加的內容(子視圖)不具有固有尺寸,那么你要顯式地為content view或者其內容確定好合適的尺寸。當content view的高度大于scroll view的高度,那么scroll view支持垂直方向的滑動。當content view的寬度大于scroll view的寬度,那么scoll view支持水平方向上的滑動。否則,默認滑動是被禁止的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容