神奇的效果
有天一回到座位上,張皇失措的應(yīng)屆生同事就好像看到救星一樣把我抓過去:“倉(cāng)薯蚊夫,不好了,你看它這樣了E吵ⅰ这橙!”
我一看,從不說粗口的倉(cāng)薯也忍不住說了一句:“我……去导披,我做了這么多年 iOS 還從來沒遇見這樣的事屈扎。” 把領(lǐng)導(dǎo)也叫過來看撩匕。領(lǐng)導(dǎo)拿來玩了一會(huì)兒鹰晨,然后說:“哈哈哈,感覺真想要實(shí)現(xiàn)這個(gè)效果止毕,還不是那么容易呢……”
究竟是什么 bug 讓我們都這么不淡定呢模蜡?看下面的 gif 就知道了:
這個(gè)方塊形的 cell 就是一個(gè)平凡而普通的 collectionView 上平凡而普通的 collectionViewCell,很多地方都在用扁凛,用了一年多了忍疾,一直都長(zhǎng)這個(gè)樣子,從沒出任何問題谨朝。然而被我們的應(yīng)屆生同事不知道怎么一改卤妒,出現(xiàn)了這樣的效果:當(dāng) cell 滾動(dòng)到屏幕邊緣,即將離開屏幕的時(shí)候字币,它好像舍不得離開一樣则披,竟然把自己縮起來了……
要不要來幫我 debug
以下是能重現(xiàn) bug 的代碼,能在 iPhone 7 iOS 11 模擬器上重現(xiàn)洗出。為了只寫一個(gè)文件士复,我就把代碼最簡(jiǎn)化了,只要 60 行:
import UIKit
final class TestCell: UICollectionViewCell {
override init(frame: CGRect) {
let imageView = UIImageView(frame: .zero)
let metadataView = UIView(frame: .zero)
super.init(frame: frame)
imageView.backgroundColor = UIColor.red
metadataView.backgroundColor = UIColor.green
for view in [imageView, metadataView] {
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor).isActive = true
}
imageView.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor).isActive = true
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
metadataView.topAnchor.constraint(equalTo: imageView.bottomAnchor).isActive = true
metadataView.heightAnchor.constraint(equalToConstant: 25).isActive = true
metadataView.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor).isActive = true
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
final class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView!.contentInsetAdjustmentBehavior = .never
self.collectionView!.register(TestCell.self, forCellWithReuseIdentifier: "Cell")
}
// MARK: UICollectionViewDataSource
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let measurementCell = TestCell()
let width = (collectionView.bounds.size.width - 20) / 2.0
measurementCell.widthAnchor.constraint(equalToConstant: width).isActive = true
return CGSize(width: width, height: measurementCell.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height)
}
}
約束用的是系統(tǒng)原生的寫法翩活,可能大家平時(shí)用第三方庫(kù)用得多阱洪,原生寫法反而不熟悉了便贵。簡(jiǎn)單解釋下,假設(shè)紅色是圖片冗荸,綠色是描述吧:
圖片左邊承璃、右邊、上面約束到父 view俏竞,高度 = 寬度
描述左邊绸硕、右邊、下面約束到父 view魂毁,高度固定 25玻佩,頂部貼著圖片底部
代碼出來了,能看出是什么問題嗎席楚?
幾個(gè)猜測(cè)
Q:是不是 layout 出什么問題了咬崔!
A:用的是最簡(jiǎn)單的 UICollectionViewFlowLayout 啊…… 沒 override 任何東西。
Q:是不是 constraint 沖突烦秩?
A:你看我約束得有啥問題垮斯?明明不會(huì)有任何沖突耶。
Q:Cell size 算得不對(duì)吧只祠?
A:最普通的自動(dòng)計(jì)算…… 打 log 來看算得是對(duì)的兜蠕。而且,就算是出了問題抛寝,滾動(dòng)的時(shí)候也不會(huì)實(shí)時(shí)計(jì)算 size 啊…… 它可是一邊滾一邊縮啊……
Q:view.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor).isActive = true 這個(gè)self.layoutMarginsGuide.leadingAnchor是什么鬼熊杨,你就不能用self.leadingAnchor嗎?
A:你猜對(duì)了…… 因?yàn)橄胧∈赂?self.layoutMargins 所以約束到 layoutMarginsGuide盗舰,但確實(shí)如果改成約束到普通的self.leadingAnchor就不會(huì)有問題了晶府。
Q:這貨是不是只有什么特定情況才有的 bug,比如 iOS 11 或者 iPhoneX
A:沒錯(cuò)是 iOS 11 才有……任何手機(jī)都可以重現(xiàn)钻趋,但確實(shí)跟 iPhoneX 有點(diǎn)關(guān)系……
這下聰明的讀者猜出是什么問題了嗎川陆?:)
其實(shí)就是少了一行
要解決這個(gè)問題很簡(jiǎn)單,就是在 cell 的init方法里加一句
1self.insetsLayoutMarginsFromSafeArea = false
insetsLayoutMarginsFromSafeArea 這個(gè)屬性對(duì)于所有UIView默認(rèn)為YES(我覺得這點(diǎn)并不是太科學(xué))蛮位,當(dāng)它為YES的時(shí)候较沪,view 的 layoutMargins 會(huì)根據(jù) safeArea 進(jìn)行調(diào)整。這樣的話土至,即使把 layoutMargins 設(shè)置為一個(gè)固定值比如 layoutMargins = .zero购对,但是到了屏幕邊緣的時(shí)候,它的 margins 還是會(huì)逐漸變大陶因,本意應(yīng)該是為了讓子 view 自動(dòng)避開 iPhoneX 的劉海吧。這樣垂蜗,出現(xiàn)上面這個(gè)效果神奇的 bug也不足為怪了楷扬。
Layout Margins 的好處和坑
這么說的話解幽,其實(shí)應(yīng)該是個(gè)很常見的問題,為啥平常遇到的不多呢烘苹?我想還是因?yàn)槲覀兗s束到 layoutMarginsGuide 的情況比較少吧躲株。
layoutMargins 這套東西用來改 insets 是非常方便的。比如我寫一個(gè)用途很廣泛的東西镣衡,希望能支持使用者隨意改動(dòng)它的 insets霜定,如果我不用 layoutMargins 的話,我需要維護(hù) 4 個(gè) constraints:
// properties
var leadingInsetConstraint: NSLayoutConstraint!
var trailingInsetConstraint: NSLayoutConstraint!
var topConstraint: NSLayoutConstraint!
var bottomConstraint: NSLayoutConstraint!
// during init
self.leadingInsetConstraint = someView.leadingAnchor.constraint(equalTo: self.leadingAnchor)
self.leadingInsetConstraint.isActive = true
self.trailingInsetConstraint = someView.trailingAnchor.constraint(equalTo: self.trailingAnchor)
self.trailingInsetConstraint.isActive = true
self.topInsetConstraint = someView.topAnchor.constraint(equalTo: self.topAnchor)
self.topInsetConstraint.isActive = true
self.bottomInsetConstraint = someView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
self.bottomInsetConstraint.isActive = true
// configuration
self.leadingInsetConstraint.constant = inset.left // 假設(shè)我們不考慮阿拉伯語(yǔ)吧
self.trailingInsetConstraint.constant = inset.right
self.topInsetConstraint.constant = inset.top
self.bottomInsetConstraint.constant = inset.bottom
而如果我用layoutMagins這套東西廊鸥,上面這些代碼就可以簡(jiǎn)化很多了望浩,一個(gè)屬性都不用存:
// during init
self.leadingInsetConstraint = someView.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor)
self.trailingInsetConstraint = someView.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor)
self.topInsetConstraint = someView.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor)
self.bottomInsetConstraint = someView.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor)
// configuration
self.layoutMargins = insets
如果使用 directionalLayoutMargins,連阿拉伯語(yǔ)的情況都自動(dòng)處理好了惰说。
但它也有一些坑磨德,上面提到的就是其中之一。另外的我隨便列兩個(gè):
layoutMargins 的默認(rèn)值居然不是 0吆视。這一點(diǎn)讓我永遠(yuǎn)都不能理解蘋果的腦回路典挑,它的默認(rèn)值是 UIEdgeInsets(8,8,8,8)。也許 8 是某個(gè)蘋果工程師的幸運(yùn)數(shù)字吧……
沒有加進(jìn) view hierarchy 之前啦吧,布局可能無法正確使用 layout margins您觉。這一點(diǎn)就比較詭異,印象中以前就遇到需要先 addSubview 再設(shè) layoutMargins授滓,反過來就跟沒設(shè)一樣的神奇 bug琳水,也不知道最新版的系統(tǒng)修好了沒有了……