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];
}
實現的結果是這樣的:
這樣實現的問題就在于,不知道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);
}];
}
運行代碼,得到的結果如圖:
???
什么情況呢?沒有任何內容顯示在上面?查看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本身的可見區域呢,還是其內容區域呢?
- 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已經確定好了。就不會再出現警告。
運行結果如圖:
那么第二版本中手動設置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自動布局約束的技巧:
Add the scroll view to the scene.
Draw constraints to define the scroll view’s size and position, as normal.
Add a view to the scroll view. Set the view’s Xcode specific label to Content View.
-
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.
(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.
(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.
-
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.
大意為:
添加一個scroll view
像普通視圖一樣為scroll view添加位置和大小的約束
在scroll view中添加一個子視圖(content view),給該視圖添加一個指定的標簽(這個標簽只是為了更好地顯示)
-
將content view的left,right,top,bottom和scroll view的邊界建立相等約束。那么現在content view的邊界就確定了scroll view的內容區域
(注意此時content view還沒有固定的大小,它可以根據你在其中設置的視圖的伸縮大?。?/p>
(可選)如果不需要水平滑動,將content view的寬度設置為和scoll view的寬度相等。
(可選)如果不需要垂直滑動,將content view的高度設置為和scroll view的高度相等。
在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支持水平方向上的滑動。否則,默認滑動是被禁止的。