iOS 10: infinite loop of UICollectionView's layoutSubviews if one of its subview's adjustsFontSizeToFitWidth = true

** Note that this issue does NOT happen on ios 9 but on ios 10 only.


I have a collection view with cell called "MyCell". Each MyCell contains a MyView, and MyView contains a MyLabel. 2 seconds after the viewcontroller did load, I trigger a call "setNeedsLayout" to one of visibleCells of collectionView to refresh its MyLabel's content.


If my MyLabel's adjustsFontSizeToFitWidth is not set, it works ok. But if I assign adjustsFontSizeToFitWidth = true, app will hang up with an infinite number of calls to "layoutSubviews".


The sample code is as following (~120 lines). If I comment out line #18, it works ok but the font is not scaled as I expected. If I leave it as is, app will hang up and crash due to out-of-memory error.


Thanks for your help.


import UIKit
import SnapKit

class MyView: UIView {   
    var myText: String = "" {
        didSet {
            self.setNeedsLayout()
        }
    }
   
    let myLabel = UILabel()
   
    override init(frame: CGRect) {
        super.init(frame: frame)
       
        self.backgroundColor = UIColor.whiteColor()
       
        myLabel.adjustsFontSizeToFitWidth = true
        self.addSubview(myLabel)
       
        myLabel.snp_remakeConstraints { (make) in
            make.size.equalTo(CGSizeMake(200, 50))
            make.center.equalTo(self)
        }
    }
   
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
   
    override func layoutSubviews() {
        super.layoutSubviews()
       
        self.myLabel.text = self.myText
    }
}

class MyCell: UICollectionViewCell {
   
    let myView = MyView()
   
    override init(frame: CGRect) {
        super.init(frame: frame)
       
        self.backgroundColor = UIColor.darkGrayColor()
       
        self.addSubview(myView)
       
        myView.snp_remakeConstraints { (make) in
            make.center.equalTo(self)
            make.size.equalTo(CGSizeMake(210, 60))
        }
    }
   
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
   
    override func layoutSubviews() {
        super.layoutSubviews()
       
        self.myView.myText = "Hello!!!"
    }
}

class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
    lazy var myCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .Vertical
        layout.minimumInteritemSpacing = 1.0
        layout.minimumLineSpacing = 1.0
       
        let collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout)
        collectionView.dataSource = self
        collectionView.delegate = self       
        collectionView.registerClass(MyCell.self, forCellWithReuseIdentifier: "MyCell")
        return collectionView
    }()
   
    let myButton = UIButton()
   
    override func viewDidLoad() {
        super.viewDidLoad()
       
        self.view.addSubview(myCollectionView)
       
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(2 * NSEC_PER_MSEC)), dispatch_get_main_queue(), {
            self.myCollectionView.visibleCells().last?.setNeedsLayout()
        })
    }
   
    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }
   
    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 2
    }
   
    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath)
        return cell
    }
   
    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
        return CGSizeMake(self.view.bounds.width, self.view.bounds.height)
    }
   
    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
        return 0
    }
   
    func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
        return 0
    }
}

Replies

Assign label text value, in layoutSubviews(), only if the new value differs from the current label text value.

It is always a bad idea to update a UI element's content in layoutSubviews, since it can trigger further layout changes. Code should only change frames in this function, not contents.

Third party apps with custom

UIView
subclasses using Auto Layout that override
layoutSubviews
and dirty layout on
self
before calling super are at risk of triggering a layout feedback loop when they rebuild on iOS 10. When they are correctly sent subsequent
layoutSubviews
calls they must be sure to stop dirtying layout on self at some point (note that this call was skipped in release prior to iOS 10).