Joe
Joe

Reputation: 3961

Passing data from a child VC back to a Collection View Cell

I have two ViewController: MainVC, and ChildVC. The MainVC includes a CollectionView with 5 cell. Each of these cell segues to the ChildVC. On this ChildVC, you can select different items which increases (or decreases) a counter on the ChildVC (the counter just reads "## selected".)

Basically, I just want this counter data on the ChildVC to be passed back onto a label of the respective MainVC cell that was tapped. For example: If user taps the second cell on the MainVC, selects 13 items on the ChildVC, then returns back to the MainVC, there will be a "13" in a label on the second cell. Then if the user taps the first cell, selects 5 items on the ChildVC, then returns back to the MainVC, there will be a "5" in a label on the first cell along with the "13" on second cell.

My progress:

I have decided that delegation is an appropriate solution for my requirements, as delegation makes it easy to pass data to/from VC's. I need assistance in passing data BACK from a ChildVC TO a CollectionView Cell.

My questions:

Update:

I have some progress below. But it's not working as intended.

In the below code, I have created both the ViewController (MainVC) and ChildVC. In the Child VC, there is a UISlider to emulate the selected counter. I would like this counter data passed back to the respective MainVC CollectionView Cells. What's happening now is the MainVC CollectionView gets a new cell added once I change the value of the slider! The 'Clear All Animals' btn needs to "zero out" the slider data for all the cells, but I haven't gotten that far yet..

View Controller (MainVC in my question above)

class ViewController: UIViewController {

    var allAnimals = AnimalData.getAllAnimals()
    @IBOutlet weak var mainCV: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()
        mainCV.dataSource = self

    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "AnimalSegue" {
            let childVC = segue.destination as! ChildVC
            childVC.delegate = self

            if let indexPath = self.mainCV.indexPath(for: sender as! AnimalCollectionViewCell) {
                let animalData = self.allAnimals[indexPath.item]
                childVC.animal = animalData
                childVC.indexPath = indexPath
            }

            childVC.allIndexPaths = getAllIndexPaths()

        }
    }

    func getAllIndexPaths() -> [IndexPath] {
        var indexPaths: [IndexPath] = []

        for i in 0..<mainCV.numberOfSections {
            for j in 0..<mainCV.numberOfItems(inSection: i) {
                indexPaths.append(IndexPath(item: j, section: i))
            }
        }
        return indexPaths
    }

}


extension ViewController: DataDelegate {
    func zeroOut(for animalObject: AnimalModel, at indexPath: [IndexPath]) {
        print("ZERO OUT")
        self.mainCV.reloadData()
    }

    func updatedData(for animalObject: AnimalModel, at indexPath: IndexPath ) {
        self.allAnimals[indexPath.item] = animalObject
        self.mainCV.reloadItems(at: [indexPath])
    }
}

extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return allAnimals.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AnimalCell", for: indexPath as IndexPath) as! AnimalCollectionViewCell
        let animal = allAnimals[indexPath.item]

        cell.animal = animal

        return cell
    }
}

ChildVC

class ChildVC: UIViewController {


    @IBOutlet weak var animalTitleLabel: UILabel!
    @IBOutlet weak var labelCounter: UILabel!
    @IBOutlet weak var sliderLabel: UISlider!

    var delegate: DataDelegate?
    var animal: AnimalModel?
    var indexPath: IndexPath?
    var allIndexPaths: [IndexPath]?

    override func viewDidLoad() {
        super.viewDidLoad()
        animalTitleLabel.text = animal?.name
        animalTitleLabel.textColor = animal?.color ?? .white
        sliderLabel.value = Float(animal?.amountCounter ?? 0)
        self.labelCounter.text = "\(Int(sliderLabel.value))"

    }

    @IBAction func closeButtonPressed(_ sender: UIButton) {
        if let delegate = self.delegate,
            let indexPath = self.indexPath,
            let animal = self.animal {
            delegate.updatedData(for: animal, at: indexPath)
        }
        self.dismiss(animated: true, completion: nil)
    }


    @IBAction func sliderChanged(_ sender: UISlider) {
        let newValue = Int(sender.value)
        labelCounter.text = "\(newValue)"
        self.animal?.amountCounter = newValue
    }

    @IBAction func clearAllBtnPressed(_ sender: UIButton) {
        if let delegate = self.delegate,
            let all = self.allIndexPaths,
            var animal = self.animal {
            animal.amountCounter = 0
            delegate.zeroOut(for: animal, at: all)
        }
        self.dismiss(animated: true, completion: nil)
    }
}

Animal Collection View Cell

class AnimalCollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var animalLabel: UILabel!
    @IBOutlet weak var counterLabel: UILabel!

    var animal: AnimalModel! {
        didSet {
            self.updateUI()
        }
    }

    func updateUI() {
        animalLabel.text = animal.name
        counterLabel.text = "\(animal.amountCounter)"
        self.backgroundColor = animal.color
    }

}

Data

struct AnimalData {
    static func getAllAnimals() -> [AnimalModel] {
        return [
            AnimalModel(name: "Cats", amountCounter: 0, color: UIColor.red),
            AnimalModel(name: "Dogs", amountCounter: 0, color: UIColor.blue),
            AnimalModel(name: "Fish", amountCounter: 0, color: UIColor.green),
            AnimalModel(name: "Goats", amountCounter: 0, color: UIColor.yellow),
            AnimalModel(name: "Lizards", amountCounter: 0, color: UIColor.cyan),
            AnimalModel(name: "Birds", amountCounter: 0, color: UIColor.purple)
        ]
    }
}

Delegate

protocol DataDelegate {
    func updatedData(for animalObject: AnimalModel, at: IndexPath)
    func zeroOut(for animalObject: AnimalModel, at: [IndexPath])
}

Screenshots below of what is happening. See how Dogs is being added as another cell with the value of 23? What should happen is the 0 should change to a 23 on the second blue Dogs cell. I don't understand updating the data source and reloading the correct cells??

Wrong functionality

How do i simply pass back the slider data into the cell that was originally tapped?

Any help is appreciated

Upvotes: 1

Views: 1198

Answers (2)

Paulw11
Paulw11

Reputation: 114826

You have the right idea with your delegation, but you need to be able to provide context back to your delegate; ie. what animal was being updated? To do this, either MainVC needs to keep a property of the item that is being updated, or this information needs to be provided to the ChildVC so that it can provide the information back to the MainVC. I will use the latter approach.

Protocol

protocol DataDelegate {
    func updatedData(for animalObject: AnimalModel, at: IndexPath)
    func clearAll()
}

MainVC

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "AnimalSegue" {

        let childVC = segue.destination as! ChildVC
        childVC.delegate = self

        if let indexPath = self.mainCV.indexPath(for: sender as! AnimalCollectionViewCell) {
            let animalData = self.allAnimals[indexPath.item]
            childVC.animal = animalData
            childVC.indexPath = indexPath
        }

    }
}

extension ViewController: DataDelegate {
    func updatedData(for animalObject: AnimalModel, at indexPath: IndexPath ) {
        self.allAnimals[indexPath.item] = animalObject            
        self.mainCV.reloadItems(at: [indexPath])
    }

    func clearAll() {
       for index in 0..<self.allAnimals.count {
          self.allAnimals[index].count =0
       }
       self.mainCV.reloadData()
}

ChildVC

class ChildVC: UIViewController {


    @IBOutlet weak var animalTitleLabel: UILabel!
    @IBOutlet weak var labelCounter: UILabel!
    @IBOutlet weak var sliderLabel: UISlider!

    var delegate: DataDelegate?  
    var animal: AnimalModel? 
    var indexPath: IndexPath?

    override func viewDidLoad() {
        super.viewDidLoad()

        animalTitleLabel.text = animal?.name
        animalTitleLabel.textColor = animal?.color ?? .white
        sliderLabel.value = animal?.count ?? 0
        self.labelCounter.text = "\(Int(sliderLabel.value))"

    }

    @IBAction func closeButtonPressed(_ sender: UIButton) {
        if let delegate = self.delegate, 
           let indexPath = self.indexPath,
           let animal = self.animal {
            delegate.updatedData(for: animal, at: indexPath)
        }
        self.dismiss(animated: true, completion: nil)
    }


    @IBAction func sliderChanged(_ sender: UISlider) {
        let newValue = Int(sender.value)
        labelCounter.text = "\(newValue)"
        self.animal.count = newValue
    }

    @IBAction func clearAllBtnPressed(_ sender: UIButton) {
        delegate.clearAll()
    }

}

Updated

I have updated my answer to show how you could implement the clear all. In this case there is no reason to have the ChildVC update the data model; it simply needs to invoke a delegate method to let the MainVC know that it should update the model and refresh the collection view.

I think that this gives a hint as to why the ChildVC is the wrong place for the "clear all" button; if the code feels a bit clunky then the user experience may be a bit clunky too. The clear all button should just be on your MainVC - it doesn't make sense for a button on a animal-specific view to be affecting other animals. It also isn't "discoverable"; I don't find out about the clear all until I select an animal. I realise that this is just a "learning app" but user experience is an important part of iOS app development and so it is never too early to consider it; it can also impact the way you design your code as you can see here.

Upvotes: 2

Vollan
Vollan

Reputation: 1915

So this is a very incomplete display, but i believe this solution is something that you are lookin for

class mainVC {

    var counters: [Int] = [0,0,0,0,0]


    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(forIndexPath: indexPath) as CustomCell
        cell.counterLabel = counters[indexPath.item]
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let childVC =  ChildVC()
        childVC.finishedSelecting = { counter in
            self.counters.insert(counter, at: indexPath.item)
            childVC.dismiss()
            self.theCollectionView.reloadItems(at: [indexPath])
            //Or
            self.theCollectionView.reloadData()
        }

         present(childVC, animated: true)

    }
}

class childVC {

    var finishedSelecting: ((Int) -> ())?
    var counter = 5
    @objc func finishedButtonPressed() {
        finishedSelecting?(counter)

    }

    func count() {
        counter+=1
    }
}

Upvotes: 0

Related Questions