寫在前面
iOS的的布局機制「auto layout」不是一個新概念,它早在iOS 6中就推出來了,當下距離iOS 9正式版面世已不遠矣,我卻對它的了解還比較初級。
我之前對auto layout機制的理解非常粗淺,幾乎把它和「constraint」對等。對它的使用有這么幾個階段:
Storyboard中通過拖拽設置constraints;
學習VFL(Visual Format Language)語法使用代碼設置constraints;
使用大殺器Masonry。
P.S: iOS的VFL語法實在太羅嗦了,又臭又長且可讀性差難于調試,無法忍受,Masonry正是解決這一痛點的第三方庫,非常好用,學習成本非常低。
近期因為項目,我需要實現一個能夠自適應文本自動調整高度的table view cell。網上有相關豐富的資源(category、博客等),思路都非常簡單,比如這篇:動態計算UITableViewCell高度詳解。但即便如此,我對有些東西理解起來還有障礙,譬如systemLayoutSizeFittingSize:和sizeThatFits:之類的,想到我都將近有大半年的開發經驗了,居然還無法理解這么簡單的東西,不能忍!
導致這種結果的主要原因有倆:一是之前項目比較簡單,涉及auto layout相關的知識無非是add/update/remove constraints;二是自己太輕浮,把auto layout想得太簡單;
通過對各種資訊梳理,大概搞明白了自己最大的問題是對auto layout相關的各種API不熟悉或者完全陌生,這導致了無法在一些實際問題中使用正確的策略解決問題。本文的著重點正是結合各種資料加上自己的理解對這些API進行分析。
本文涉及的API包括:
sizeThatFits:和sizeToFit
systemLayoutSizeFittingSize:
intrinsicContentSize
P.S: 這幾個API都是與size相關,該如何使用它們呢?這曾讓筆者一時非常困惑。以上這幾個API都是UIView的實例方法,除此之外,本文還涉及一些屬性,譬如preferredMaxLayoutWidth。
iOS布局機制大概分這么幾個層次:
frame layout
autoresizing
auto layout
frame layout
frame layout最簡單直接,簡單來說,即通過設置view的frame屬性值進而控制view的位置(相對于superview的位置)和大小。
autoresizing
autoresizing和frame layout一樣,從一開始存在,它算是后者的補充,基于autoresizing機制,能夠讓subview和superview維持一定的布局關系,譬如讓subview的大小適應superview的大小,隨著后者的改變而改變。
站在代碼接口的角度來看,autoresizing主要體現在幾個屬性上,包括(但不限于):
translatesAutoresizingMaskIntoConstraints
autoresizingMask
第一個屬性標識view是否愿意被autoresize;
第二個屬性是一個枚舉值,決定了當superview的size改變時,subview應該做出什么樣的調整;
關于autoresizing的更詳細使用說明,參考:自動布局之autoresizingMask使用詳解。
auto layout
autoresizing存在的不足是非常顯著的,通過autoresizingMask的可選枚舉值可以看出:基于autoresizing機制,我們只能讓view在superview的大小改變時做些調整;而無法處理兄弟view之間的關系,譬如處理與兄弟view的間隔;更無法反向處理,譬如讓superview依據subview的大小進行調整。
Auto Layout是隨著iOS 6推出來的,關于它的介紹,官方文檔《Auto Layout Guide》的描述非常精煉:
Auto Layout is a system that lets you lay out your app’s user interface by creating a mathematical description of the relationships between the elements. You define these relationships in terms of constraints either on individual elements, or between sets of elements. Using Auto Layout, you can create a dynamic and versatile interface that responds appropriately to changes in screen size, device orientation, and localization.
簡單來說,它是一種基于約束的布局系統,可以根據你在元素(對象)上設置的約束自動調整元素(對象)的位置和大小。
值得一提的是,對于某個view的布局方式,autoresizing和auto layout只能二選一,簡單來說,若要對某個view采用auto layout布局,則需要設置其translatesAutoresizingMaskIntoConstraints屬性值為NO。
intrinsicContentSize方法
在介紹intrinsicContentSize方法之前,先來看一個應用場景:
場景一:某個UILabel用于顯示單行文本,讓其能夠自適應文本,即根據文本自動調整其大小。
讓UILabel自適應文本,在auto layout之前,一般做法是先給定字體,進而計算文本內容所占據的寬度width和高度height,然后使用得來的width和height設置其frame屬性值。
但是使用auto layout非常簡單,如下:
@interfaceViewController(){
UILabel*testLabel;
}
- (void)viewDidLoad {
[superviewDidLoad];
self.view.backgroundColor = [UIColorwhiteColor];
testLabel = ({
UILabel*label? ? ? ? = [[UILabelalloc] init];
label.textAlignment? =NSTextAlignmentCenter;
label.font? ? ? ? ? ? = [UIFontsystemFontOfSize:14.0];
label.textColor? ? ? = [UIColorwhiteColor];
label.backgroundColor = [UIColorlightGrayColor];
label;
});
[self.view addSubview:testLabel];
// 使用Masonry添加constraints
[testLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).offset(40);
make.left.equalTo(self.view.mas_left).offset(10);
}];
testLabel.text =@"天地玄黃 宇宙洪荒 日月盈昃 辰宿列張";
}
- (void)viewDidAppear:(BOOL)animated {
[superviewDidAppear:animated];
NSLog(@"testLabel.origin = %@",NSStringFromCGPoint(testLabel.frame.origin));
NSLog(@"testLabel.size = %@",NSStringFromCGSize(testLabel.frame.size));
// print: "testLabel.origin = {10, 40}"
// print: "testLabel.size = {236.5, 17}"
}
效果如下:
問題來了,auto layout system知道testLabel的size呢?
OK,就此引入APIintrinsicContentSize。
intrinsicContentSize是UIView的基礎方法(Available in iOS 6.0 and later),UIView Class References對它的描述如下:
Returns the natural size for the receiving view, considering only properties of the view itself.
Return Value
A size indicating the natural size for the receiving view based on its intrinsic properties.
Discussion
Custom views typically have content that they display of which the layout system is unaware. Overriding this method allows a custom view to communicate to the layout system what size it would like to be based on its content. This intrinsic size must be independent of the content frame, because there’s no way to dynamically communicate a changed width to the layout system based on a changed height, for example.
If a custom view has no intrinsic size for a given dimension, it can return UIViewNoIntrinsicMetric for that dimension.
「intrinsic content size」在中文世界里常被譯作:「固有內容大小」,簡單來說,它被用來告訴auto layout system應該給它分配多大的size。
所以呢,在上文代碼(場景一的Solution)中,根據我的理解,layout工作流程是這樣的:在layout時,auto layout system會去回調testLabel的實例方法intrinsicContentSize,該方法能夠根據「文本內容+字體」計算出content的size,進而根據此size對testLabel進行布局。
為了驗證這個說法,對上述代碼做些改變。
首先定義一個繼承自UILabel的類ZWLabel,重寫ZWLabel的intrinsicContentSize方法,如下:
@interfaceZWLabel:UILabel
@end
@implementationZWLabel
- (CGSize)intrinsicContentSize {
CGSizesize = [superintrinsicContentSize];
size.width? +=20;
size.height +=20;
returnsize;
}
@end
然后讓上文的testLabel從UILabel的實例改為ZWLabel的實例,其余不變:
testLabel = ({
ZWLabel *label = [[ZWLabel alloc] init];
//...
label;
});
效果如下:
效果明顯,本示例較為直觀說明了intrinsicContentSize這個API的作用了,這個API是為auto layout system的callback提供的!
P.S:筆者剛開始對intrinsicContentSize這個API的用法感到非常疑惑,在我的理解里,它有兩種可能:
Auto Layout System會根據content為view設置一個合適的size,開發者有時需要知道這個size,因此可以通過intrinsicContentSize獲取;
Auto Layout System在layout時,不知道該為view分配多大的size,因此回調view的intrinsicContentSize方法,該方法會給auto layout system一個合適的size,system根據此size對view的大小進行設置;
現在看來,第二種理解更靠譜!
對于上文所用到的UILabel,想必Cocoa在實現intrinsicContentSize方法時已經根據text屬性值和font屬性值進行了計算。那是不是每個原生view都實現了intrinsicContentSize呢?
NO!《Auto Layout Guide》在談論「intrinsic content size」時,總會與另外一個詞語「leaf-level views」相關聯,譬如:
Intrinsic Content Size
Leaf-level views such as buttons typically know more about what size they should be than does the code that is positioning them. This is communicated through theintrinsic content size, which tells the layout system that a view contains some content that it doesn’t natively understand, and indicates how large that content is, intrinsically.
「leaf-level views」指的是那種一般不包含任何subview的view,譬如UILabel、UIButton等,這類的view往往能夠直接計算出content(譬如UILabel的text、UIButton的title,UIImageView的image)的大小。
但是有些view不包含content,譬如UIView,這種view被認為「has no intrinsic size」,它們的intrinsicContentSize返回的值是(-1,-1)。
P.S: 官方文檔中說的是:UIView’s default implementation is to return (UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric),而UIViewNoIntrinsicMetric等于-1,為什么是-1而不是0,我猜是0是一個有效的width/height,而-1不是,更容易區分處理。
還有一種view雖然包含content,但是intrinsicContentSize返回值也是(-1,-1),這類view往往是UIScrollView的子類,譬如UITextView,它們是可滾動的,因此auto layout system在對這類view進行布局時總會存在一些未定因素,Cocoa干脆讓這些view的intrinsicContentSize返回(-1,-1)。
preferredMaxLayoutWidth屬性
基于上述場景一,我們來分析更復雜一點的UILabel自適應問題。
場景二:某個UILabel用于顯示多行文本,讓其能夠自適應文本,即根據文本自動調整其大小;
對于單行文本UILabel,UILabel的intrinsicContentSize在計算content size時比較容易;但對于多行文本的UILabel,同樣的content,譬如「天地玄黃宇宙洪荒」這八個字,擺放方式可以是1x8,可以是2x4,可以是4x2,auto layout system該如何處理呢?UILabel的屬性preferredMaxLayoutWidth正是用來應對這個問題的。
UILabel Class References對它的描述如下:
The preferred maximum width (in points) for a multiline label.
Discussion
This property affects the size of the label when layout constraints are applied to it. During layout, if the text extends beyond the width specified by this property, the additional text is flowed to one or more new lines, thereby increasing the height of the label.
preferredMaxLayoutWidth的作用顧名思義,用來限制UILabel content size的最大寬度值。如下代碼:
testLabel = ({
UILabel*label? ? ? ? ? ? ? ? = [[UILabelalloc] init];
label.textAlignment? ? ? ? ? =NSTextAlignmentCenter;
label.font? ? ? ? ? ? ? ? ? ? = [UIFontsystemFontOfSize:14.0];
label.textColor? ? ? ? ? ? ? = [UIColorwhiteColor];
label.numberOfLines? ? ? ? ? =0;// mark
label.preferredMaxLayoutWidth =100;// mark
label.backgroundColor? ? ? ? = [UIColorlightGrayColor];
label;
});
[self.view addSubview:testLabel];
// 使用Masonry添加constraints
[testLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).offset(40);
make.left.equalTo(self.view.mas_left).offset(10);
}];
testLabel.text =@"天地玄黃 宇宙洪荒 日月盈昃 辰宿列張";
效果如下:
那么最后testLabel的width是不是就是preferredMaxLayoutWidth的屬性值呢?No,最終testLabel的屬性值小于等于preferredMaxLayoutWidth的屬性值。
sizeThatFits:方法和sizeToFit方法
上文已經提到,UITextView繼承自UIScrollView,是可以滾動的,它的intrinsicContentSize方法返回值是(-1,-1),auto layout system在處理UITextView對象時,為其設置的size是(0,0)。如此看來,似乎UITextView無法體驗到auto layout帶來的好處了。
繼續結合應用場景引出sizeThatFits:方法和sizeToFit方法。
場景三:某個UITextView用于顯示文本,讓其能夠自適應文本,即根據文本自動調整其大小;
既然UITextView的content計算方法intrinsicContentSize無法向auto layout system傳遞我們想要傳達的值,我們就應該另想別的方法。
好在iOS有直接的接口可供我們使用。
先談sizeThatFits:方法,UIView Class References對它的描述如下:
Asks the view to calculate and return the size that best fits the specified size.
Return Value
A new size that fits the receiver’s subviews.
Discussion
The default implementation of this method returns the existing size of the view. Subclasses can override this method to return a custom value based on the desired layout of any subviews. For example, a UISwitch object returns a fixed size value that represents the standard size of a switch view, and a UIImageView object returns the size of the image it is currently displaying.
簡單來說,調用sizeThatFits:方法意味著「根據文本計算最適合UITextView的size」。從功能來講,sizeThatFits:和intrinsicContentSize方法比較類似,都是用來計算view的size的。筆者曾一度對二者的關系非常疑惑,甚至覺得二者存在相互調用的關系。后來通過驗證發現不是這么回事兒,后文會通過示例說明。
對于顯示多行文本的UILabel,為了方便intrinsicContentSize方法更方便計算content size,需要指定preferredMaxLayoutWidth屬性值;對于UITextView的sizeThatFits:,似乎有類似的需求,畢竟UITextView也可能會顯示多行啊,這樣說來,UITextView也有一個preferredMaxLayoutWidth屬性?
No!preferredMaxLayoutWidth屬性是iOS 6才引入的,sizeThatFits:方法則早得多,況且,UITextView是可以滾動的,哪怕文本不會全部呈現出來,但也可以通過左右或者上下滾動瀏覽所有內容;傳給sizeThatFits:的參數(假設為size)是CGSize類型,size.width的功能和UILabel的preferredMaxLayoutWidth差不多,指定了UITextView區域的最大寬度,size.height則指定了UITextView區域的最大高度;可能有人問,若傳給sizeThatFits:的size小于UITextView的text面積怎么辦,豈不是有些內容無法顯示出來?傻啊,可以滾啊!
值得一提的是,調用sizeThatFits:并不改變view的size,它只是讓view根據已有content和給定size計算出最合適的view.size。
那么sizeToFit方法是干嘛的呢?很簡單:
calls sizeThatFits: with current view bounds and changes bounds size.
P.S:有點不太理解,這個「current view」指的是啥?self?還是superview?
P.P.S:經過驗證,這里的「current view」指的是self。簡單來說,sizeToFit等價于:
// calls sizeThatFits
CGSizesize = [selfsizeThatFits:self.bounds.size];
// change bounds size
CGRectbounds =self.bounds;
bounds.size.width = size.width;
bounds.size.height = size.width;
self.bounds = bounds;
P.S:值得一提的是,經過測試發現,當調用sizeThatFits:的size=(width, height),當width/height的值為0時,width/height似乎就被認為是無窮大!
systemLayoutSizeFittingSize:方法
首先來看一個應用場景。
場景四:某個UIView,寬度等于屏幕寬度,包含兩個UILabel,兩個label都可能顯示多行文本,要求結合auto layout讓UIView大小能夠自適應subviews。
Easy,給出如下代碼:
- (void)viewDidLoad {
[superviewDidLoad];
self.view.backgroundColor = [UIColorlightGrayColor];
bgView = ({
UIView*view? ? ? ? = [[UIViewalloc] init];
view.backgroundColor = [UIColorgrayColor];
view;
});
[self.view addSubview:bgView];
label1 = ({
UILabel*label? ? ? ? ? ? ? ? = [[UILabelalloc] init];
label.font? ? ? ? ? ? ? ? ? ? = [UIFontsystemFontOfSize:14.0];
label.preferredMaxLayoutWidth =self.view.frame.size.width-20;
label.numberOfLines? ? ? ? ? =0;
label.textColor? ? ? ? ? ? ? = [UIColorwhiteColor];
label.backgroundColor? ? ? ? = [UIColorpurpleColor];
label;
});
[bgView addSubview:label1];
label2 = ({
UILabel*label? ? ? ? ? ? ? ? = [[UILabelalloc] init];
label.font? ? ? ? ? ? ? ? ? ? = [UIFontsystemFontOfSize:18.0];
label.preferredMaxLayoutWidth =self.view.frame.size.width-20;
label.numberOfLines? ? ? ? ? =0;
label.textColor? ? ? ? ? ? ? = [UIColorwhiteColor];
label.backgroundColor? ? ? ? = [UIColorredColor];
label;
});
[bgView addSubview:label2];
// 添加約束(基于Masonry)
[bgView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view.mas_left);
make.top.equalTo(self.view.mas_top).offset(10);
make.width.equalTo(self.view.mas_width);
}];
[label1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(bgView.mas_left).offset(10);
make.right.lessThanOrEqualTo(bgView.mas_right).offset(-10);
make.top.equalTo(bgView.mas_top).offset(10);
}];
[label2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(label1.mas_left);
make.right.lessThanOrEqualTo(bgView.mas_right).offset(-10);
make.top.equalTo(label1.mas_bottom).offset(10);
make.bottom.equalTo(bgView.mas_bottom).offset(-10);
}];
label1.text =@"天地玄黃 宇宙洪荒 日月盈昃 辰宿列張 寒來暑往 秋收冬藏 閏余成歲 律呂調陽";
label2.text =@"天地玄黃 宇宙洪荒 日月盈昃 辰宿列張 寒來暑往 秋收冬藏 閏余成歲 律呂調陽";
}
代碼看得有些枯燥,簡單來說,bgView(UIView)中嵌入兩個能顯示多行文本的label1(UILabel)和label2(UILabel),設置約束如下:
代碼運行后的顯示效果:
代碼中除了添加各種各樣的constraints,沒有任何設置frame的代碼,顯然都是基于auto layout的。
那么問題來了,理解label1和label2的布局沒啥子問題,因為它們的intrinsicContentSize方法會將content size告訴auto layout system,進而后者會為它們的size設置對應值;但對于bgView,它可是一個UIView對象,它的intrinsicContentSize回調方法的返回值為(-1,-1),那么auto layout system是如何為它設置合適的size的呢?
根據我的理解,auto layout system在處理某個view的size時,參考值包括:
自身的intrinsicContentSize方法返回值;
subviews的intrinsicContentSize方法返回值;
自身和subviews的constraints;
OK,根據筆者理解,結合上圖,我認為auto layout system是這樣計算一下bgView的size的:
width=max{10+size1.width+10, 10+size2.width+10, size3.width}
height=max{10+size1.height+10+size2.height+10, size3.height}
我們在viewDidAppear:方法中將相關值打印出來瞧瞧看:
- (void)viewDidAppear:(BOOL)animated {
[superviewDidAppear:animated];
CGSizesize1 = [label1 intrinsicContentSize];
CGSizesize2 = [label2 intrinsicContentSize];
CGSizesize3 = [bgView intrinsicContentSize];
NSLog(@"size1 = %@",NSStringFromCGSize(size1));// print: "size1 = {300, 33.5}"
NSLog(@"size2 = %@",NSStringFromCGSize(size2));// print: "size2 = {290.5, 64.5}"
NSLog(@"size3 = %@",NSStringFromCGSize(size3));// print: "size3 = {-1, -1}"
CGSizebgViewSize = bgView.frame.size;
NSLog(@"bgViewSize = %@",NSStringFromCGSize(bgViewSize));// print: "bgViewSize = {320, 128}"
}
完全吻合我理解的auto layout size計算公式。
P.S:然而,我知道,事實往往并沒有這么簡單,當處理自定義View時,當constraints設置不完整或者沖突時,事情總會變得復雜起來,也總會得到意想不到的結果。但,暫且就這么理解吧!
啰里啰嗦寫了這么多,還沒引出systemLayoutSizeFittingSize:方法…
OK,再來看另外一個應用場景。
場景五:某個UIView,寬度等于屏幕寬度,包含一個UILabel和一個UITextView,二者都可能顯示多行文本,要求結合auto layout讓UIView大小能夠自適應subviews。
在場景四代碼基礎上將label2改為UITextView對象textView1,如下:
- (void)viewDidLoad {
[superviewDidLoad];
self.view.backgroundColor = [UIColorlightGrayColor];
bgView = ({
UIView*view? ? ? ? = [[UIViewalloc] init];
view.backgroundColor = [UIColorgrayColor];
view;
});
[self.view addSubview:bgView];
label1 = ({
UILabel*label? ? ? ? ? ? ? ? = [[UILabelalloc] init];
label.font? ? ? ? ? ? ? ? ? ? = [UIFontsystemFontOfSize:14.0];
label.preferredMaxLayoutWidth =self.view.frame.size.width-20;
label.numberOfLines? ? ? ? ? =0;
label.textColor? ? ? ? ? ? ? = [UIColorwhiteColor];
label.backgroundColor? ? ? ? = [UIColorpurpleColor];
label;
});
[bgView addSubview:label1];
textView1 = ({
UITextView*label? ? = [[UITextViewalloc] init];
label.font? ? ? ? ? ? = [UIFontsystemFontOfSize:18.0];
label.textColor? ? ? = [UIColorwhiteColor];
label.backgroundColor = [UIColorredColor];
label;
});
[bgView addSubview:textView1];
// 添加約束(基于Masonry)
[bgView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view.mas_left);
make.top.equalTo(self.view.mas_top).offset(10);
make.width.equalTo(self.view.mas_width);
}];
[label1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(bgView.mas_left).offset(10);
make.right.lessThanOrEqualTo(bgView.mas_right).offset(-10);
make.top.equalTo(bgView.mas_top).offset(10);
}];
[textView1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(label1.mas_left);
make.right.lessThanOrEqualTo(bgView.mas_right).offset(-10);
make.top.equalTo(label1.mas_bottom).offset(10);
make.bottom.equalTo(bgView.mas_bottom).offset(-10);
}];
label1.text =@"天地玄黃 宇宙洪荒 日月盈昃 辰宿列張 寒來暑往 秋收冬藏 閏余成歲 律呂調陽";
textView1.text =@"天地玄黃 宇宙洪荒 日月盈昃 辰宿列張 寒來暑往 秋收冬藏 閏余成歲 律呂調陽";
}
運行效果如下:
這顯然不是我們想要的結果,至少存在這么兩個問題:
textView1不見了;
bgView的大小不是我們想要的;
為什么會有出現這樣的問題呢?
首先正如上文所提到的那樣,textView1是UITextView的對象,而UITextView是UIScrollView的子類,它的intrinsicContentSize:方法的返回值是(-1,-1),這意味著textView1對bgView的auto layout size沒有產生影響。
那textView1為什么不見了呢?或者說,為什么textView1的size為CGSizeZero呢?根據我對auto layout system的理解,auto layout system在處理view的size時,受三個因素影響:
自身的intrinsicContentSize方法返回值;
subviews的intrinsicContentSize方法返回值;
自身和subviews的constraints;
對于textView1,前兩個因素可以忽略掉,簡而言之,textView1的size由它自身的constraints決定。而根據上述代碼的約束可以計算出textView1的height:
height = bgView.height-10-label1.height-10-10;
而bgView.height=10+label1.height+10+10;意味著textView1的height值為0;當然就看不到了textView1了。
因此,若想要讓textView1正常可見,至少有這么一種策略:直接為textView1添加約束設置width和height;
簡單來說,override回調方法viewWillLayoutSubviews,如下:
- (void)viewWillLayoutSubviews {
[superviewWillLayoutSubviews];
CGSizetextViewFitSize = [textView1 sizeThatFits:CGSizeMake(self.view.frame.size.width-20,0)];
[textView1 mas_updateConstraints:^(MASConstraintMaker *make) {
make.width.equalTo([NSNumbernumberWithFloat:textViewFitSize.width]);
make.height.equalTo([NSNumbernumberWithFloat:textViewFitSize.height]);
}];
}
這種做法是有效的,運行效果如下:
若將這種場景切換到table view cell中會如何呢?簡單來說,如果將上述的bgView換成UITableViewCell(或其子類)對象又會如何呢?
在table view中,我們可以使用- (CGFloat)tableView:heightForRowAtIndexPath:接口,該回調方法會返回CGFloat值,該值指示了對應cell的高度;假設auto layout system為cell分配的size是autoSize,在處理返回值時額外加上textViewFitSize.height即可。
但問題是,我們如何獲取這個autoSize的值呢?畢竟此時cell還未布局完成啊,直接讀取cell.frame.size肯定是不行的。
systemLayoutSizeFittingSize:方法正是用于處理這個問題的。
UIView Class References對該方法描述如下:
Returns the size of the view that satisfies the constraints it holds.
Return Value
The size of the view that satisfies the constraints it holds.
Discussion
Determines the best size of the view considering all constraints it holds and those of its subviews.
我是這么理解systemLayoutSizeFittingSize:的:對于使用auto layout機制布局的view,auto layout system會在布局過程中綜合各種約束的考慮為之設置一個size,在布局完成后,該size的值即為view.frame.size的值;這包含的另外一層意思,即在布局完成前,我們是不能通過view.frame.size準確獲取view的size的。但有時候,我們需要在auto layout system對view完成布局前就知道它的size,systemLayoutSizeFittingSize:方法正是能夠滿足這種要求的API。systemLayoutSizeFittingSize:方法會根據其constraints返回一個合適的size值。
systemLayoutSizeFittingSize:方法可傳入一個參數,目前有兩個值可以傳入:
UILayoutFittingCompressedSize : The option to use the smallest possible size.
UILayoutFittingExpandedSize : The option to use the largest possible size.
值得一提的是,在使用[view systemLayoutSizeFittingSize:]時,要注意盡量確保view的constraints的完整性,這樣參數UILayoutFittingCompressedSize和UILayoutFittingExpandedSize得到的結果是一樣的。否則,舉個例子,若view的right屬性沒有設置,則這兩個參數得到systemLayoutSizeFittingSize:返回值size是不一樣的,前者size.width=0,后者size.width=1000。
P.S:這純屬個人使用體驗。
至于systemLayoutSizeFittingSize:的使用場景,動態計算UITableViewCell高度詳解非常值得參考!
對比幾種API
在剛開始接觸這幾個API時感到非常困惑。分不清intrinsicContentSize、sizeThatFits:以及systemLayoutSizeFittingSize:的區別。經過這么將近一天的折騰,現在大概有了基本的判斷。
首先說intrinsicContentSize,它的最主要作用是告訴auto layout system一些信息,可以認為它是后者的回調方法,auto layout system在對view進行布局時會參考這個回調方法的返回值;一般很少像CGSize size = [view intrinsicContentSize]去使用intrinsicContentSizeAPI。
再來看sizeThatFits:和systemLayoutSizeFittingSize:,它們倆非常相似,都是為開發者直接服務的API(而不是回調方法)。所不同的是,sizeThatFits:是auto layout之前就存在的,一般在leaf-level views中用得比較多,在計算size過程中,它可不會考慮constraints神馬的;對于systemLayoutSizeFittingSize:,它是隨著auto layout(iOS 6)引入的,用于在view完成布局前獲取size值,如果view的constraints確保了完整性和正確性,通常它的返回值就是view完成布局之后的view.frame.size的值。
它們之前存在相互調用的關系嗎?經過測試發現,三者之前沒有直接的調用關系。但是能得出這樣的結論:intrinsicContentSize的返回值會直接影響systemLayoutSizeFittingSize:的返回值。至于底層是如何處理的不得而知。