[譯]Auto Layout的最佳實踐
原文鏈接:https://medium.com/@NSomar/auto-layout-best-practices-for-minimum-pain-c130b2b1a0f6#.tqby1u8l4
Auto Layout是個很棒的工具,作為開發者,它可以讓我們保持神志清醒,還能讓我們這些懶人們在設置frame的時候遠離“神奇數字”。
但是任何技術都不是完美無缺的,我必須得說我花了太多的時間來debug那些缺失的約束條件,或者對于一些藏在層級結構深處的視圖,添加一個沖突的約束條件就會把整個布局毀掉,當這些事情發生的時候簡直是天崩地裂!
在debug了無數個小時的auto layout的問題后,我發現每次造成問題的都是我自己(或者是你自己!),而問題的解決辦法總是相同的:遵從auto layout的文檔和規則!
我會在這里把正確使用auto layout的最佳實踐說給你聽,這樣你就可以免除一些痛苦了。
UIView的子類應該實現intrinsicContentSize方法
每個UIView的子類都應該實現intrinsicContentSize,并且返回它認為合適的大小。
假設我們新建了一個AwesomeView,而且我們知道這個view的默認尺寸是300x20,我們會這么寫:
</span>-(CGSize)intrinsicContentSize { return CGSizeMake(300, 20); }
如果我們不知道view的寬度,我們會用UIViewNoIntrinsicMetric來代替:
- (CGSize)intrinsicContentSize { return CGSizeMake(UIViewNoIntrinsicMetric, 20); }
UIView基類的updateConstraints實現會調用intrinsicContentSize,它會使用返回的尺寸來給AwesomeView添加約束條件。
根據上面例子中的(300,20)尺寸,會添加下面的約束條件:
</span><NSContentSizeLayoutConstraint:0x7fef48d52580 H:[AwesomeView:0x7fef48ead7f0(300)] Hug:250 CompressionResistance:750>, <NSContentSizeLayoutConstraint:0x7fef48d4d110 V:[AwesomeView:0x7fef48ead7f0(20)] Hug:250 CompressionResistance:750>
添加的約束條件比較特殊,它們是NSContentSizeLayoutConstraint類型的,這個類是個私有類。這些約束條件的優先級范圍是0-1000,“包住限制”(譯者注:hug consistance,使其在“內容大小”的基礎上不能繼續變大)的優先級是250,“撐住限制”(compression resistance,撐住使其在在其“內容大小”的基礎上不能繼續變小)的優先級是750,使用的常量等于通過intrinsicContentSize返回的值。
請注意,UIView基類實現updateConstraints只有在它第一次執行的時候才會添加intrinsicContentSize約束。
UIView的子類絕不應該給自身的尺寸添加約束
每個view都會負責給它的superview設置約束,但是view絕不應該設置它自己的約束條件,不管是對于自身的約束(比如說 NSLayoutAttributeWidth和 NSLayoutAttributeHeight),還是相對于superview的約束。
如果一個view想指定自己的高度或者寬度,它應該通過實現 intrinsicContentSize來達到目的。
這是個糟糕的例子:
</span>- (instancetype)init { self = [super init]; if (self) { [self addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:0 constant:100]]; [self addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:0 constant:100]]; } return self; }
這個view在通過給自身添加約束來設置自己的寬度和高度,那么如果現在它的superview也在試圖指定這些數值會發生什么呢?
//Some place in the superview [awesome addConstraint:[NSLayoutConstraint constraintWithItem:awesome attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:0 constant:200]]; [awesome addConstraint:[NSLayoutConstraint constraintWithItem:awesome attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:0 constant:200]];
嘭!
Unable to simultaneously satisfy constraints. … property translatesAutoresizingMaskIntoConstraints) ( “<NSLayoutConstraint:0x7ff3b16c2ae0 H:[AwesomeView:0x7ff3b16bfa00(100)]>”, “<NSLayoutConstraint:0x7ff3b16c2330 H:[AwesomeView:0x7ff3b16bfa00(200)]>” ) Will attempt to recover by breaking constraint <NSLayoutConstraint:0x7ff3b16c2330 H:[AwesomeView:0x7ff3b16bfa00(200)]> …
AwesomeView添加的寬度/高度是100/100,而它的superview也添加了寬度/高度,但是是200/200,這樣autoLayout就不知道該選擇哪個約束條件了,因為它們的優先級都一樣。
一種解決辦法是這樣的,將AwesomeView自身的優先級降低一點。
</span>[self addConstraint:({ NSLayoutConstraint *constraint; constraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:0 constant:100]; constraint.priority = 800; constraint; })];
這樣autoLayout就可以做出選擇了,因為它自身添加的優先級比較低,它就可以選擇superview添加的約束了。
然而,盡管這樣可以解決問題,但正確的方式是通過 intrinsicContentSize來指定它的高度。
UIView的子類絕不應該給它的superview添加約束
和上面的原因一樣,子視圖絕不應該給他的父視圖添加約束。子視圖的位置是由父視圖決定的。
像這么做很糟糕:
</span>- (void)didMoveToSuperview { [super didMoveToSuperview]; [self.superview addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]]; }
太糟糕了,它不能離開相對于它的父視圖的位置。所以,當父視圖想把Awesome放到另一個位置上會怎么樣?是的,就會拋出來另一個Unable to simultaneously satisfy constraints的問題。
updateConstraints是用來更新約束條件的
顧名思義,updateConstraints只是被用來更新需要的約束的。一個正確的實現 updateConstraints的方式應該長這個樣子:
</span>- (instancetype)init { … init stuff … _labelCenterYConstraints = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]; [self addConstraint:_labelCenterYConstraints]; label.text = @”I Am truly awesome!”; … }
(void)updateConstraints { self.labelCenterYConstraints.constant = self.labelVerticalDisplacement; [super updateConstraints]; }</pre>
然后在需要的地方:awesome.labelVerticalDisplacement = 40; [awesome setNeedsUpdateConstraints];
調用 setNeedsUpdateConstraints會使autoLayout重新計算布局,因此會調用 updateConstraints,從而讀取新的label狀態并更新約束。
在上面的例子中,你本可以只更新 _labelCenterYConstraints這個約束,如果你的視圖暴露出約束,或者如果你可以簡單獲得一個約束,那就直接設置約束的常量好了,不必使用updateConstraints。所以,上面的代碼也可以這么實現:
awesome.labelCenterYConstraints.constant = 40;</pre>
一個非常糟糕的 updateConstraints的實現會長這個樣子:
</span>- (void)updateConstraints { [self removeConstraints:self.constraints]; /* create the constraint here */ [super updateConstraints]; }
這么做是非常錯誤的,因為:
- 系統調用 updateConstraints很多次,因此移除或者重新創建可能會校驗約束的合理性。
- [self removeConstraints:self.constraints]; 會移除包括xib或storyboard創建的所有約束條件,你該怎么重新創建這些約束?(趕緊說你不能!)
- 上面的updateConstraints實現會覆蓋掉intrinsicContentSize的效果,因為你在調用[super updateConstraints];后移除了系統添加的約束條件。
- updateConstraints應該被用來創建約束條件一次,然后僅僅移除掉失效的約束。它絕不該是一個移除所有約束再把每個傳過來的布局添加上的地方。(感謝Alexis提供以下補充。)
正確的實現方式是這樣:
- (void)updateConstraints { if (!didSetConstraints) { didSetConstraints = YES; //create the constraint here }
//Update the constraints if needed [super updateConstraints]; }</pre>
在上面的代碼中,創建約束的動作只會執行一次,然后在接下來的updateConstraints調用中,只會對這些已創建的約束的常量進行修改。
我一直在追尋真理!所以如果你有很多更好的實踐經驗,請在推ter上和我分享。
來自:http://www.calios.gq/2015/12/14/[譯]Auto-Layout的最佳實踐-——-止疼片/