有天一回到座位上,張皇失措的應屆生同事就好像看到救星同樣把我抓過去:「倉薯倉薯,很差了,你看它這樣了!!」bash
我一看,從不說粗口的倉薯也忍不住說了一句:「我……去,我作了這麼多年 iOS 還歷來沒碰見這樣的事。」 把領導也叫過來看。領導拿來玩了一下子,而後說:「哈哈哈,感受真想要實現這個效果,還不是那麼容易呢……」ide
到底是什麼 bug 讓咱們都這麼不淡定呢?看下面的 gif 就知道了:佈局
這個方塊形的 cell 就是一個平凡而普通的 collectionView 上平凡而普通的 collectionViewCell,不少地方都在用,用了一年多了,一直都長這個樣子,從沒出任何問題。然而被咱們的應屆生同事不知道怎麼一改,出現了這樣的效果:當 cell 滾動到屏幕邊緣,即將離開屏幕的時候,它好像捨不得離開同樣,居然把本身縮起來了……ui
如下是能重現 bug 的代碼,能在 iPhone 7 iOS 11 模擬器上重現。爲了只寫一個文件,我就把代碼最簡化了,只要 60 行:spa
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)
}
}
複製代碼
約束用的是系統原生的寫法,可能你們平時用第三方庫用得多,原生寫法反而不熟悉了。簡單解釋下,假設紅色是圖片,綠色是描述吧:debug
代碼出來了,能看出是什麼問題嗎?code
Q:是否是 layout 出什麼問題了! A:用的是最簡單的 UICollectionViewFlowLayout 啊…… 沒 override 任何東西。cdn
Q:是否是 constraint 衝突? A:你看我約束得有啥問題?明明不會有任何衝突耶。blog
Q:Cell size 算得不對吧? A:最普通的自動計算…… 打 log 來看算得是對的。並且,就算是出了問題,滾動的時候也不會實時計算 size 啊…… 它但是一邊滾一邊縮啊……圖片
Q:view.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor).isActive = true
這個self.layoutMarginsGuide.leadingAnchor
是什麼鬼,你就不能用self.leadingAnchor
嗎? A:你猜對了…… 由於想省事改 self.layoutMargins
因此約束到 layoutMarginsGuide
,但確實若是改爲約束到普通的self.leadingAnchor
就不會有問題了。
Q:這貨是否是隻有什麼特定狀況纔有的 bug,好比 iOS 11 或者 iPhoneX A:沒錯是 iOS 11 纔有……任何手機均可以重現,但確實跟 iPhoneX 有點關係……
這下聰明的讀者猜出是什麼問題了嗎?:)
要解決這個問題很簡單,就是在 cell 的init
方法里加一句
self.insetsLayoutMarginsFromSafeArea = false
複製代碼
insetsLayoutMarginsFromSafeArea
這個屬性對於全部UIView
默認爲YES
(我以爲這點並非太科學),當它爲YES
的時候,view 的 layoutMargins
會根據 safeArea 進行調整。這樣的話,即便把 layoutMargins
設置爲一個固定值好比 layoutMargins = .zero
,可是到了屏幕邊緣的時候,它的 margins 仍是會逐漸變大,本意應該是爲了讓子 view 自動避開 iPhoneX 的劉海吧。這樣,出現上面這個效果神奇的 bug也不足爲怪了。
這麼說的話,其實應該是個很常見的問題,爲啥日常遇到的很少呢?我想仍是由於咱們約束到 layoutMarginsGuide
的狀況比較少吧。
layoutMargins 這套東西用來改 insets 是很是方便的。好比我寫一個用途很普遍的東西,但願能支持使用者隨意改動它的 insets,若是我不用 layoutMargins 的話,我須要維護 4 個 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 // 假設咱們不考慮阿拉伯語吧
self.trailingInsetConstraint.constant = inset.right
self.topInsetConstraint.constant = inset.top
self.bottomInsetConstraint.constant = inset.bottom
複製代碼
而若是我用layoutMagins
這套東西,上面這些代碼就能夠簡化不少了,一個屬性都不用存:
// 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
,連阿拉伯語的狀況都自動處理好了。
但它也有一些坑,上面提到的就是其中之一。另外的我隨便列兩個: