swiftuicollectionviewuicollectionviewcelluicollectionviewlayoutuicollectionviewdelegate

Push a new UICollectionViewController from a UICollectionViewCell that is inside of multiple UICollectionVIews


I really tried my best to find a solution to this problem. I tried different solutions with delegate and protocols but my code did not work. Before complaining about my question please support me and let us work as a community. Tell me if you don't understand my question or need additional info. I still can change the question or add additional infos. I really need help. Thanks community.

Scenario I Everything is alright:

I created a UINavigationController which contains a UICollectionViewController called HomeController as rootViewController. The HomeController contains a custom UICollectionViewCell called CustomCell. If I click on one of the CustomCells the didSelectItem method of the HomeController class is executed. This method pushes a new UICollectionViewController to the UINavigationController. The code works fine. Here is the skeleton of my code:

class HomeController: UICollectionViewController{

 override func viewDidLoad() {
collectionView?.register(customCell.self, forCellWithReuseIdentifier: "cellId")

 }
 
 // Call methods to render the cell
  override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 2
    }
    
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) 
        return cell
    }
    
 
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: view.frame.width, height: 100)
    }
    

    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            let layout = UICollectionViewFlowLayout()
            let controller = UserController(collectionViewLayout: layout)
            navigationController?.pushViewController(controller, animated: true)
        }
    }

}


class customCell: UICollectionViewCell{
.....
}

I really need your help regarding Scenario II and III.

Scenario II:

I created a UINavigationController which contains a UICollectionViewConroller called MainController. MainController contains four different custom cells (CustomCell1, CustomCell2, CustomCell3, CustomCell4). The cells are horizontal scrollable and the width and height of each cell takes the entire collectionview. enter image description here

CustomCell1, CustomCell2, CustomCell3 and CustomCell4 contain a collectionView which takes the entires cells width and height

class CustomCell1: UICollectionViewCell, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout  {

//create a collectionView  
lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.backgroundColor = UIColor.white
        cv.dataSource = self
        cv.delegate = self
        return cv
    }()

.....

}

For example: The collectionView of CustomCell1 contains 3 cells. The first cell of the collectionView has two buttons Button1 and Button2. If I click Button2 I want that the content of cell2 and cell3 that are inside the same collectionView changes. QUESTION 1: How can you achieve this in that scenario ? A code example with my construct would be very very very helpful. enter image description here

As mentioned above CustomCell2 has also a collectionView that takes the entire width and height of Cell. The collectionView of CustomCell2 contains three CustomCells. The first and the second CustomCells have a collectionView (cell.indexPath == 0 cell.indexPath ==1).

The first collectionView (which is inside the first cell) has four customCells. If you click on one of those cells I want that a new UICollectionViewController be pushed to the UINavigationController. QUESTION 2: How can you do this using delegates and protocols?

The second collectionView (which is inside the second cell) has also four customCells. If you click on one of those customCells the task is not to push a new UICollectionView to the NavigationController but rather to change the content of cell3 QUESTION 3: How can I achieve this, can someone give me an example with my provided skeleton?

enter image description here


Solution

  • I'm gonna give you a hand, you asked really nicely, so here is an example of how it is done. I'll not answer questions 2 and 3 because it is basically the same as question 1 but with a few changes in the protocol. So I'll try to explain as best as I can your first question:

    Let me start with the full example that you can download: https://github.com/galots/CollectionView

    Now the explanation:

    You have a viewController that creates the top few cells, these are the 4 custom cells that you gave as the first picture. I guess this is clear for you:

    class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
    
        lazy var collectionView : UICollectionView = {
            let layout = UICollectionViewFlowLayout()
            layout.scrollDirection = .horizontal
            let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
            collectionView.backgroundColor = .white
            collectionView.dataSource = self
            collectionView.delegate = self
            return collectionView
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
    
            self.view.addSubview(collectionView)
            collectionView.anchor(top: self.view.safeAreaLayoutGuide.topAnchor, leading: self.view.leadingAnchor, bottom: self.view.bottomAnchor, trailing: self.view.trailingAnchor)
            collectionView.register(TopCell.self, forCellWithReuseIdentifier: "topCell")
        }
    
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return 1
        }
    
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "topCell", for: indexPath) as! TopCell
            return cell
        }
    
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            return CGSize(width: self.collectionView.frame.size.width, height: self.collectionView.frame.size.height)
        }
    
    }
    

    Now, Inside those cells you have other collectionViews that at the same time display different cells. So, so far so good. BaseCell is just a class that I made to avoid initializing the cells all the time.

    class BaseCell : UICollectionViewCell {
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupViews()
        }
    
        func setupViews() { }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
    }
    
    class TopCell: BaseCell, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, ButtonCellDelegate {
    
        var model = "Text"
    
        lazy var collectionView : UICollectionView = {
            let layout = UICollectionViewFlowLayout()
            let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
            collectionView.backgroundColor = .white
            collectionView.dataSource = self
            collectionView.delegate = self
            return collectionView
        }()
    
        override func setupViews() {
            super.setupViews()
            self.backgroundColor = .green
            self.addSubview(collectionView)
            collectionView.anchor(top: self.topAnchor, leading: self.leadingAnchor, bottom: self.bottomAnchor, trailing: self.trailingAnchor)
            collectionView.register(ButtonsCell.self, forCellWithReuseIdentifier: "buttonsCell")
            collectionView.register(InnerCollectionViewCell.self, forCellWithReuseIdentifier: "cvCell")
        }
    
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return 3
        }
    
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            if indexPath.item == 0 {
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "buttonsCell", for: indexPath) as! ButtonsCell
                cell.buttonCellDelegate = self
                return cell
            }
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cvCell", for: indexPath) as! InnerCollectionViewCell
            cell.model = self.model
            return cell
        }
    
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            return CGSize(width: self.frame.width, height: 150)
        }
    
        func didPressButton(sender: String) {
    
            switch sender {
            case "buttonOne":
                self.model = "Text"
                self.collectionView.reloadData()
            case "buttonTwo":
                self.model = "New Text"
                self.collectionView.reloadData()
            default:
                break
            }
    
    
        }
    }
    

    Now, here is where you are going to do most of the stuff.

    // Protocol for buttons
    
    protocol ButtonCellDelegate : class { func didPressButton (sender: String) }
    
    // Buttons Cell
    
    class ButtonsCell : BaseCell {
    
        weak var buttonCellDelegate : ButtonCellDelegate?
    
        let buttonOne : UIButton = {
            let button = UIButton(frame: .zero)
            button.setTitle("Button 1", for: .normal)
            button.setTitleColor(.black, for: .normal)
            return button
        }()
    
        let buttonTwo : UIButton = {
            let button = UIButton(frame: .zero)
            button.setTitle("Button 2", for: .normal)
            button.setTitleColor(.black, for: .normal)
            return button
        }()
    
        override func setupViews() {
            super.setupViews()
            self.addSubview(buttonOne)
            buttonOne.anchor(top: self.topAnchor, leading: self.leadingAnchor, bottom: self.bottomAnchor, trailing: nil, size: .init(width: self.frame.width / 2, height: 0))
            buttonOne.addTarget(self, action: #selector(buttonOnePressed), for: .touchUpInside)
            self.addSubview(buttonTwo)
            buttonTwo.anchor(top: self.topAnchor, leading: buttonOne.trailingAnchor, bottom: self.bottomAnchor, trailing: self.trailingAnchor)
            buttonTwo.addTarget(self, action: #selector(buttonTwoPressed), for: .touchUpInside)
        }
    
        @objc func buttonTwoPressed (sender: UIButton) {
            self.buttonCellDelegate?.didPressButton(sender: "buttonTwo")
        }
    
        @objc func buttonOnePressed (sender: UIButton) {
            self.buttonCellDelegate?.didPressButton(sender: "buttonOne")
        }
    }
    
    // Mark
    
    class InnerCollectionViewCell : BaseCell, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    
        var model : String? {
            didSet {
                self.collectionView.reloadData()
            }
        }
    
        lazy var collectionView : UICollectionView = {
            let layout = UICollectionViewFlowLayout()
            layout.scrollDirection = .horizontal
            let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
            collectionView.backgroundColor = .red
            collectionView.dataSource = self
            collectionView.delegate = self
            return collectionView
        }()
    
        override func setupViews() {
            super.setupViews()
            self.addSubview(collectionView)
            collectionView.anchor(top: self.topAnchor, leading: self.leadingAnchor, bottom: self.bottomAnchor, trailing: self.trailingAnchor)
            collectionView.register(InnerCollectionViewSubCell.self, forCellWithReuseIdentifier: "innerCell")
        }
    
        func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return 3
        }
    
        func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "innerCell", for: indexPath) as! InnerCollectionViewSubCell
            cell.model = self.model
            return cell
        }
    
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            return CGSize(width: 100, height: 100)
        }
    
    }
    
    // Mark
    
    class InnerCollectionViewSubCell : BaseCell {
    
        var model : String? {
            didSet { label.text = model }
        }
    
        let label : UILabel = {
            let label = UILabel(frame: .zero)
            label.textColor = .black
            label.textAlignment = .center
            return label
        }()
    
        override func setupViews() {
            super.setupViews()
            self.addSubview(label)
            label.anchor(top: self.topAnchor, leading: self.leadingAnchor, bottom: self.bottomAnchor, trailing: self.trailingAnchor)
        }
    }
    
    // Extensions
    
    extension UIView {
        func anchor(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets = .zero, size: CGSize = .zero) {
            translatesAutoresizingMaskIntoConstraints = false
            if let top = top { topAnchor.constraint(equalTo: top, constant: padding.top).isActive = true }
            if let leading = leading { leadingAnchor.constraint(equalTo: leading, constant: padding.left).isActive = true }
            if let bottom = bottom { bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom).isActive = true }
            if let trailing = trailing { trailingAnchor.constraint(equalTo: trailing, constant: -padding.right).isActive = true }
            if size.width != 0 { widthAnchor.constraint(equalToConstant: size.width).isActive = true }
            if size.height != 0 { heightAnchor.constraint(equalToConstant: size.height).isActive = true }
        }
    }
    

    There is a protocol for the ButtonsCell which can be conformed by the top cell to update the content of the other collectionViewCells. Whenever a button is pressed in the buttonsCell, the delegate will be called and the var model is updated, this model is also propagated to the inner cells because of reloadData() and because in the topCell cellForItem method I am setting the model of the innerCells to be the same as the model of the top cell. didSet in the inner cell just acts as an observer so that whenever the model is updated the UI of the cell should also be updated.

    Now, for questions 2 and 3, I guess if you have a look at the example it is basically the same implementation of delegates. You just need to make some changes to the protocol functions to add the functionality that you need and you can call the delegate in other places as well, for example in the didSelectItem method of the inner collectionViews.

    Hope this helps.