Changing the numberOfItemsInSection of a collectionView, from another collectionView didSelectSelectItemAt indexPath.

Hi, I have 2 separate collectionViews and I've been trying to change the numberOfItemsInSection of "collectionViewA" according to the didSelectItemAt indexPath of "collectionViewB". Let's say I have 2 items already setup in "collectionViewA" but if I select "collectionViewB" indexPath.row = 1, I would like to have 3 items in "collectionViewA", and if I select "collectionViewB" indexPath.row = 2, I would like to have 4 items in "collectionViewA". As far as I can see, this is possible but I couldn't find the relevant info online. Any idea on this please ?

Accepted Reply

Have you checked that you did not allow multiple cell selection in the collectionView ?


If so, the new selection (1) is added to the initial selection (0) ; creating the problem.

In viewDidLoad, you could set

collectionView.

allowsMultipleSelection = false


Another way to avoid, is to deselect all cells in DidSelect before resetting to the indexPath.

Here is a way to deseelct all, with a simple extension:

https://stackoverflow.com/questions/38430668/uicollectionview-how-to-deselect-all


extension UICollectionView {

  func deselectAllItems(animated: Bool) {
  guard let selectedItems = indexPathsForSelectedItems else { return }
  for indexPath in selectedItems { deselectItem(at: indexPath, animated: animated) }
  }
}

Replies

Are the collections in 2 different ViewControllers ?


- If both views are loaded (for instance in a common root navigation controller), you could use notification

- if they are not, you could use delegation or, simply, make their data sources global var in the app (may be in a singleton) and update dataSource A when you select in B ; then, in viewDidLoad for A, as you reload the collection, it will be updated.

- if they are in the same viewController, then the dataSources are already in the same class, so you can do the same.


So please explain more in detail the relationship between the 2 collections and between their ViewControllers.

Hi Claude, the collectionViews are in different viewControllers. The 1st viewControllers' collectionView is triggered by the next (2nd) viewController. I guess your 2nd advice is the most feasible one. I will get back to you with my results.

I have taken the Notification approach. "collectionViewEffects" triggering the "effectsMenuCollectionView". I'm having trouble with reaching (line 53) "effectsMenuImage". I'm not sure if I'm on the right track. Could you please give me any comments on that ?


import Foundation
import UIKit
import Photos

let collectionView0 = "collectionView0"
let collectionView1 = "collectionView1"

class PhotoViewController: UIViewController, UIScrollViewDelegate {

  let menuCollectionView0 = Notification.Name(rawValue: collectionView0)
  let menuCollectionView1 = Notification.Name(rawValue: collectionView1)

  deinit {
    NotificationCenter.default.removeObserver(self)
  }

override func viewDidLoad() {
    super.viewDidLoad()
    photoImageView.isUserInteractionEnabled = true
    imagePicker.delegate = self


    view.addSubview(collectionViewPreview)
    setupCollectionView()
    collectionViewPreview.isHidden = true

    view.addSubview(resetConfirmCollectionView)
    setup
ResetConfirmCollectionView()
    view.addSubview(effectsMenuCollectionView)
    setupEffectsMenuCollectionView()
    createObservers()
}

func createObservers() {

    NotificationCenter.default.addObserver(self, selector: #selector(updateCollectionView0(notification:)), name: menuCollectionView0, object: effectsMenuCollectionView)
}

@objc func updateCollectionView0(notification: NSNotification) {
    if notification.name == menuCollectionView0 {

      let cellCount = effectsMenuCollectionView.visibleCells.count
      let item = cellCount - 1
      let cellIndexPath = IndexPath(item: item, section: 0)
      effectsMenuCollectionView.insertItems(at: [cellIndexPath])

      let cell = effectsMenuCollectionView.cellForItem(at: cellIndexPath)
      let config = UIImage.SymbolConfiguration(pointSize: 30, weight: .semibold)
      guard cell == UIImage(systemName: "pencil", withConfiguration: config) else {
        print("SystemImage not available")
      }
      EffectsMenuCell.effectsMenuImage = cell

    }
  }

fileprivate func setupEffectsMenuCollectionView() {

    effectsMenuCollectionView.backgroundColor = .lightGray

    effectsMenuCollectionView.leadingAnchor.constraint(equalTo: photoView!.leadingAnchor).isActive = true
    effectsMenuCollectionView.trailingAnchor.constraint(equalTo: photoView!.trailingAnchor).isActive = true
    effectsMenuCollectionView.bottomAnchor.constraint(equalTo: photoView!.bottomAnchor).isActive = true
    effectsMenuCollectionView.heightAnchor.constraint(equalToConstant: 50).isActive = true

    effectsMenuCollectionView.clipsToBounds = true
    effectsMenuCollectionView.dataSource = self
    effectsMenuCollectionView.delegate = self
    effectsMenuCollectionView.isHidden = true

  }

let effectsMenuCollectionView: UICollectionView = {
  let spacing: CGFloat = 100
  let effectsMenuLayout = UICollectionViewFlowLayout()
  let effectsMenuCv = UICollectionView(frame: .zero, collectionViewLayout: effectsMenuLayout)
  effectsMenuCv.translatesAutoresizingMaskIntoConstraints = false
  effectsMenuLayout.sectionInset = UIEdgeInsets(top: spacing/5, left: spacing/2, bottom: spacing/5, right: spacing/2)
  effectsMenuLayout.minimumInteritemSpacing = spacing
  effectsMenuCv.register(EffectsMenuCell.self, forCellWithReuseIdentifier: "EffectsMenuCellID")
  return effectsMenuCv
}()

class EffectsMenuCell: UICollectionViewCell {

  let effectsMenuImageView: UIImageView = {
    let iv = UIImageView()
    iv.tintColor = .darkGray
    iv.translatesAutoresizingMaskIntoConstraints = false
    iv.contentMode = .top
    iv.clipsToBounds = true
    return iv
  }()

  var effectsMenuImage: UIImage? {
    get {
      effectsMenuImageView.image
    }
    set {
      effectsMenuImageView.image = newValue
    }
  }

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

    contentView.addSubview(effectsMenuImageView)

    effectsMenuImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: -10).isActive = true
    effectsMenuImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
    effectsMenuImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
    effectsMenuImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true

  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}
}

import Foundation
import UIKit
import Photos

class EffectsViewController: UIViewController {

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  
    if collectionView == collectionViewEffects {
      if indexPath.row == 0 {
        let name = Notification.Name(rawValue: collectionView0)
        NotificationCenter.default.post(name: name, object: effectsMenuCollectionView)
        effectsImageView!.image = pickedEffectsImage
      
        self.performSegue(withIdentifier: "EffectsPhotoSegue", sender: self)
     }
}
}

I do not understand lines 28-29. Isn't there something missing ?


I do not see how you transition between views with the segues. How do you go from collectionView0 to collectionView1?


Depending what you do, there is a risk that when you send notification, the other connectionView controller is not loaded and thus will miss the notification


If that occurs, another way would be to pass the relevant data in prepare for segue. Relevant data are the ones needed to update collection. Of course, you will need to declare a var in the destination VC class to hold this value to pass.

Line 28-29 is just a setup for another irrelevant collectionView. "collectionView0" is for "collectionViewEffects" "indexPath.row = 0", and "collectionView1" is for "collectionViewEffects" "indexPath.row = 1"


class EffectsViewController: UIViewController {

override func viewDidLoad() {
    super.viewDidLoad()
    effectsView.addSubview(collectionViewEffects)
    setupEffectsCollectionView()
    collectionViewEffects.isUserInteractionEnabled = true
  }

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "EffectsPhotoSegue" {
      if let previousViewController = segue.destination as? PhotoViewController {
        DispatchQueue.main.async {
       
          previousViewController.photoImageView.image = self.effectsImageView!.image
          previousViewController.filteredImage = self.effectsImage
          previousViewController.pickedImage = self.pickedEffectsImage
          previousViewController.effectsSegmentedControl()
          (previousViewController.menuStackView).isHidden = true
          resetConfirmCollectionView.isHidden = true
          effectsMenuCollectionView.isHidden = false
     
        }
      }
    }
  }

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  
    if collectionView == collectionViewEffects {
      if indexPath.row == 0 {
        let name = Notification.Name(rawValue: collectionView0)
        NotificationCenter.default.post(name: name, object: effectsMenuCollectionView)
        effectsImageView!.image = pickedEffectsImage
      
        self.performSegue(withIdentifier: "EffectsPhotoSegue", sender: self)
     }
     if indexPath.row == 1 {
        let name = Notification.Name(rawValue: collectionView1)
        NotificationCenter.default.post(name: name, object: effectsMenuCollectionView)
        effectsImageView!.image = pickedEffectsImage

        let imageViewHeight: CGFloat = effectsImageView!.image!.size.height
        let imageViewWidth: CGFloat = effectsImageView!.image!.size.width
       
        effectsImage = effects.cropToBounds(image: effectsImageView!.image!, width: Double(imageViewWidth), height: Double(imageViewHeight))
       
        self.performSegue(withIdentifier: "EffectsPhotoSegue", sender: self)
      }
    }
}

Did you try the 2 solutions, notification and passing data in prepare ?

I tried that and I've had the same problem because if I'm not mistaken, all the changes must take place within the "@objc func updateCollectionView0(notification: NSNotification)". I think need a way to reach

"let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "EffectsMenuCellID", for: indexPath) as! EffectsMenuCell" within @objc func block, then implement an adaptive layout size according to the number of items added and also didSelect for the new item added. I don't know this seems pretty advanced stuff for me.

There is a simpler way.


I hope I do not confuse between 0 and 1, as names are really not explicit

In "collectionView0" ( "collectionViewEffects") you select

Then you segue to "collectionView1" (hope that's it).


So, in collectionView1, add a property

var dataPassed : someType?

To hold whet you have to send from collectionView0 when you select

In prepare, you set

destVC.dataPassed = // Whatever value


Then in collectionView1, in viewDidLoad, you get this value to add to the datasource for the collection

reload should be automatic ;

if not, call collectionView1.reloadData()


Hope that's clear.

Following up on your guidance I think I'm on the right track. I have removed notifications and added the "dataPassed" variable. I also removed the "effectsMenuCollectionView", instead of that I'm using "resetConfirmCollectionView". Even though I'm tapping on "row = 1" in "collectionViewEffects", I can't trigger the breakpoint I set in line47. Do you see any error or adjustment to be made ?


class PhotoViewController: UIViewController, UIScrollViewDelegate {

var dataPassed: IndexPath?

override func viewDidLoad() {
    super.viewDidLoad()

     resetConfirmCollectionView.reloadData()
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "PhotoEffectsSegue" {
      if let nextViewController = segue.destination as? EffectsViewController {
        DispatchQueue.main.async {
          nextViewController.effectsImageView = self.photoImageView
          nextViewController.effectsImage = self.filteredImage
          nextViewController.pickedEffectsImage = self.pickedImage
        }
      }
    }
  }

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    if collectionView == resetConfirmCollectionView {
      let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ResetConfirmCellID", for: indexPath) as! ResetConfirmCell

      let config = UIImage.SymbolConfiguration(pointSize: 40, weight: .semibold)

      if indexPath.row == 0 {
 
        guard let cellImage = UIImage(systemName: "multiply", withConfiguration: config) else {
          print("SystemImage not available")
               return cell
        }
        cell.resetConfirmImage = cellImage
        return cell
      }
      if indexPath.row == 1 {
 
        guard let cellImage = UIImage(systemName: "checkmark", withConfiguration: config) else {
          print("SystemImage not available")
          return cell
        }
        cell.resetConfirmImage = cellImage
        return cell
      }
      if dataPassed!.row == 1 {
        let cellCount = resetConfirmCollectionView.visibleCells.count
        let item = cellCount - 1
        let cellIndexPath = IndexPath(item: item, section: 0)
        resetConfirmCollectionView.insertItems(at: [cellIndexPath])
        guard let cellImage = UIImage(systemName: "pencil", withConfiguration: config) else {
          print("SystemImage not available")
          return cell
        }
        cell.resetConfirmImage = cellImage
        return cell
      }
    }

class EffectsViewController: UIViewController {

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "EffectsPhotoSegue" {
      if let previousViewController = segue.destination as? PhotoViewController {
        DispatchQueue.main.async {
     
          previousViewController.dataPassed = self.collectionViewEffects.indexPathsForSelectedItems?.first
     
          previousViewController.photoImageView.image = self.effectsImageView!.image
          previousViewController.filteredImage = self.effectsImage
          previousViewController.pickedImage = self.pickedEffectsImage
          previousViewController.effectsSegmentedControl()
          (previousViewController.menuStackView).isHidden = true
          resetConfirmCollectionView.isHidden = false
          }
        }
    }
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

    if collectionView == collectionViewEffects {
      if indexPath.row == 0 {
  
     effectsImageView!.image = pickedEffectsImage
     
     self.performSegue(withIdentifier: "EffectsPhotoSegue", sender: self)
      }

     if indexPath.row == 1 {

     let imageViewHeight: CGFloat = effectsImageView!.image!.size.height
     let imageViewWidth: CGFloat = effectsImageView!.image!.size.width
       
        effectsImage = effects.cropToBounds(image: effectsImageView!.image!, width: Double(imageViewWidth), height: Double(imageViewHeight))
       
        self.performSegue(withIdentifier: "EffectsPhotoSegue", sender: self)
      }
    }
}

I looked at it rapidly.


On line 38 you test row and return if 1.


Add on line 39:

print("Passed on row 1")

to see if it is the reason why you never reach line 47.

The breakpoint hits when the "effectsViewController" is about to pop up and I receive the print statement. Then, when I tap in both of the "indexPath.row = 0" && "indexPath.row = 1", the breakpoint hits and I receive the print statement again.

Sorry, but I do not catch the point.

The breakpoint hits when the "effectsViewController" is about to pop up and I receive the print statement

about to popup after which action excatly ?


I tap in both of the "indexPath.row = 0" && "indexPath.row = 1

Do you mean you tap on row 0 and aat later time in row 1 and then, which print do you get.


You should differentiate the print messages to make it clear:

      if indexPath.row == 0 { 
        guard let cellImage = UIImage(systemName: "multiply", withConfiguration: config) else { 
          print("indexPath.row == 0. SystemImage not available") 
               return cell 
        }


      if indexPath.row == 1 { 
        guard let cellImage = UIImage(systemName: "checkmark", withConfiguration: config) else { 
          print("indexPath.row == 1. SystemImage not available") 
          return cell 
        }



      if dataPassed!.row == 1 {
        let cellCount = resetConfirmCollectionView.visibleCells.count
        let item = cellCount - 1
        let cellIndexPath = IndexPath(item: item, section: 0)
        resetConfirmCollectionView.insertItems(at: [cellIndexPath])
        guard let cellImage = UIImage(systemName: "pencil", withConfiguration: config) else {
          print("dataPassed!.row == 1. SystemImage not available")
          return cell
        }


So, what is the conclusion ? What does not work as expected ?

The problem at its' core is "previousViewController.dataPassed = self.collectionViewEffects.indexPathsForSelectedItems?.first" is sending out this info; "Optional([0, 1])" when I tap "indexPath.row = 1", I need to receive only 1 indexPath.row, the one that I tap. That's why "if dataPassed!.row == 1" never gets triggered.


class EffectsViewController: UIViewController {

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "EffectsPhotoSegue" {
      if let previousViewController = segue.destination as? PhotoViewController {
        previousViewController.dataPassed = self.collectionViewEffects.indexPathsForSelectedItems?.first
          }
     }
}
}

class PhotoViewController: UIViewController {

  var dataPassed: IndexPath?

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    if collectionView == resetConfirmCollectionView {
      let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ResetConfirmCellID", for: indexPath) as! ResetConfirmCell
     
      let config = UIImage.SymbolConfiguration(pointSize: 40, weight: .semibold)
     
      if indexPath.row == 0 {
       
        guard let cellImage = UIImage(systemName: "multiply", withConfiguration: config) else {
          print("SystemImage not available")
          return cell
        }
        cell.resetConfirmImage = cellImage
        return cell
      }
      if indexPath.row == 1 {
        print("Passed on row 1")
        guard let cellImage = UIImage(systemName: "checkmark", withConfiguration: config) else {
          print("SystemImage not available")
          return cell
        }
        cell.resetConfirmImage = cellImage
        return cell
      }
      if dataPassed!.row == 1 {
        let cellCount = resetConfirmCollectionView.visibleCells.count
        let item = cellCount - 1
        let cellIndexPath = IndexPath(item: item, section: 0)
        resetConfirmCollectionView.insertItems(at: [cellIndexPath])
        guard let cellImage = UIImage(systemName: "pencil", withConfiguration: config) else {
          print("SystemImage not available")
          return cell
        }
        cell.resetConfirmImage = cellImage
        return cell
      }
    }
}

Have you checked that you did not allow multiple cell selection in the collectionView ?


If so, the new selection (1) is added to the initial selection (0) ; creating the problem.

In viewDidLoad, you could set

collectionView.

allowsMultipleSelection = false


Another way to avoid, is to deselect all cells in DidSelect before resetting to the indexPath.

Here is a way to deseelct all, with a simple extension:

https://stackoverflow.com/questions/38430668/uicollectionview-how-to-deselect-all


extension UICollectionView {

  func deselectAllItems(animated: Bool) {
  guard let selectedItems = indexPathsForSelectedItems else { return }
  for indexPath in selectedItems { deselectItem(at: indexPath, animated: animated) }
  }
}

Still having the same issue "Optional([0, 1])", I decided to give up on this. Thank you very much for your time and effort.