Search code examples
iosswiftxcodeinterface-builderibdesignable

IBDesignable with CollectionView crashes when add value to DataSource


I am working on a small UIView component that displays hashtags in a UICollectionView and for some reasons I needed to make it "editable" through the Interface Builder... Therefore, I made it IBDesignable.

Everything works except when in prepareForInterfaceBuilder I am trying to append a object to the datasource, so the list displays something when integrated somewhere in the storyboard.

Arriving to the line self.hashtags.append(hashtag), this crashes. If I remove this line, the view is rendered properly on the storyboard, if I put it again ---> crash... After all my debugging it seems that IB can't access this property or sees it nil.

I can't debug anything, Xcode doesn't let me debug the selected views in Storyboard > Editor (being on macOS Mojave causes bugs). My view doesn't have a .xib, and has both required init's

I also checked the logs in the Console.app and theres nothing. Here is the main error message given by the Storyboard :

Main.storyboard: error: IB Designables: Failed to render and update auto layout status for PublishViewController (tSg-9E-Vz3): The agent threw an exception.

Code (what matters)

@IBDesignable
class HashtagView: UIView {

    @IBOutlet weak var height: NSLayoutConstraint?

    var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
        return view
    }()

    open var hashtags: [HashTag] = []

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        setup()

        self.hashtags.append(HashTag(word: "it works")) < CRASH
        self.collectionView.reloadData()
        self.layoutSubviews()
    }
}

Setup function()

func setup() {
    self.backgroundColor = UIColor.clear
    self.backgroundColor = UIColor.Custom.ligthGray
    self.clipsToBounds = true
    self.layer.cornerRadius = self.cornerRadius

    let alignedFlowLayout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left, verticalAlignment: .top)
    alignedFlowLayout.minimumLineSpacing = 5.0
    alignedFlowLayout.minimumInteritemSpacing = 7.0
    alignedFlowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

    self.collectionView.collectionViewLayout = alignedFlowLayout
    self.collectionView.translatesAutoresizingMaskIntoConstraints = false

    self.collectionView.delegate = self
    self.collectionView.dataSource = self
    self.collectionView.backgroundColor = UIColor.clear
    self.collectionView.isScrollEnabled = false

    self.collectionView.register(UINib(nibName: "RemovableTagCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "RemovableTagCollectionViewCell")

    self.addSubview(self.collectionView)

    self.collectionView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
    self.collectionView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
    self.collectionView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
    self.collectionView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}

Solution

  • So the setup was definitely relevant. Apparently IB was crashing trying to load your cell for your UICollectionView. It could not load the Nib file. It did not have anything to do with the append it just crashed at the same time. I replicated your issue then created a cell with code and it all worked fine. Let me know if this helps and answers your question. All the required code is there.

    import UIKit
    
    struct HashTag {
        var word:String?
    }
    
    class HashCollectionViewCell: UICollectionViewCell {
    
        lazy var wordLabel : UILabel = {
            let lbl = UILabel(frame: self.bounds)
            lbl.font = UIFont.systemFont(ofSize: 17)
            lbl.textColor = .black
            return lbl
        }()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            self.addSubview(wordLabel)
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            self.addSubview(wordLabel)
        }
    
        func configureWithTag(tag:HashTag) {
            wordLabel.text = tag.word
        }
    
    }
    
    @IBDesignable
    class HashTagView: UIView {
    
        private var sizingLabel = UILabel(frame: .zero)
       lazy var collectionView: UICollectionView = {
            let layout = UICollectionViewFlowLayout()
            let view = UICollectionView(frame: self.bounds, collectionViewLayout: layout)
            view.autoresizingMask = [.flexibleWidth,.flexibleHeight]
            return view
        }()
    
        var hashtags: [HashTag] = [HashTag(word: "this works")]
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setup()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setup()
        }
    
    
        func setup(){
            sizingLabel.font = UIFont.systemFont(ofSize: 17)
    
            self.addSubview(collectionView)
            collectionView.frame = self.bounds
            collectionView.backgroundColor = self.backgroundColor
            collectionView.register(HashCollectionViewCell.self, forCellWithReuseIdentifier: "hashCell")
            self.addSubview(collectionView)
    
            for x in 0..<10{
                addHashTag(tag: "This is more \(x)")
            }
    
            collectionView.delegate = self
            collectionView.dataSource = self
        }
    
        func addHashTag(tag:String){
            let newHash = HashTag(word: tag)
            self.hashtags.append(newHash)
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            collectionView.frame = self.bounds
        }
    
    }
    
    extension HashTagView: UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout{
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return hashtags.count
        }
    
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "hashCell", for: indexPath) as? HashCollectionViewCell{
                let tag = self.hashtags[indexPath.item]
                cell.configureWithTag(tag: tag)
                return cell
            }
            return UICollectionViewCell()
        }
    
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            let tag = self.hashtags[indexPath.item]
            sizingLabel.text = tag.word
            sizingLabel.sizeToFit()
            return sizingLabel.frame.size
        }
    }
    

    Result:enter image description here

    2) You can load a nib into the programmatically created cell and it will work. An example would look like this instead for the part that is the programatic cell.

    Cell Code with View extension using nib inside cell:

    extension UIView {
        func loadNib() -> UIView {
            let bundle = Bundle(for: type(of: self))
            let nibName = type(of: self).description().components(separatedBy: ".").last!
            let nib = UINib(nibName: nibName, bundle: bundle)
            return nib.instantiate(withOwner: self, options: nil).first as! UIView
        }
    }
    
    class HashCollectionViewCell: UICollectionViewCell {
    
        var hashTagNib : HashTagNibView?
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setup()
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setup()
        }
    
        func setup(){
            if let htn = HashTagNibView().loadNib() as? HashTagNibView{
                hashTagNib = htn
                hashTagNib?.frame = self.bounds
                hashTagNib?.autoresizingMask = [.flexibleWidth,.flexibleHeight]
               self.addSubview(htn)
            }
    
        }
    
        func configureWithTag(tag:HashTag) {
            if let htn = hashTagNib{
                htn.hashTagButtonLabel.setTitle(tag.word, for: .normal)
            }
        }
    

    Nib setup would looks like this: nib

    This might give you more flexibility depending on what you are doing and the result is similar except in the nib example I used a UIButton. enter image description here