UICollectionView cell is nil - swift

I'm working on a collection view that gets populated with images that I have on Firebase. Everything works fine, but when I try to perform segue I get "unexpectedly found nil while unwrapping an Optional value" for this line:
if let indexPath = self.collectionView?.indexPath(for: sender as! UICollectionViewCell){}
I have seen many working examples here in SO and apparently they all work fine with that line.
Here is the rest of the relevant code:
//grab Firebase objects in viewdidload and put them into productsArray
var productsArray = [ProductsModel]()
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return productsArray.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let imageView = cell.viewWithTag(1) as! UIImageView
//imageView.image = imageArray[indexPath.row]
let getUrl = productsArray[indexPath.row].productImg
imageView.loadUsingCache(getUrl!)
imageView.layer.borderColor = UIColor.lightGray.cgColor
imageView.layer.borderWidth = 1
imageView.layer.cornerRadius = 0
return cell
}
//NAVIGATION
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
performSegue(withIdentifier: "itemSegue", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "itemSegue"){
let destinationController = segue.destination as! ItemViewController
if let indexPath = self.collectionView?.indexPath(for: sender as! UICollectionViewCell){
destinationController.getProduct = productsArray[indexPath.row]
}
}
}
Also, I double checked everything is connected and set in the storyboard.
Thanks in advance!

In the following function, you send the sender parameter as nil:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
performSegue(withIdentifier: "itemSegue", sender: nil)
}
Then in the following function, you receive the sender parameter and try to cast it (sender as! UICollectionViewCell). This parameter will always be nil.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "itemSegue"){
let destinationController = segue.destination as! ItemViewController
if let indexPath = self.collectionView?.indexPath(for: sender as! UICollectionViewCell){
destinationController.getProduct = productsArray[indexPath.row]
}
}
}
If you don't want it to be nil, then don't invoke the performSegue(withIdentifier:sender:) function with a nil sender. Send a valid object. In this case, it seems you want sender to be of type UICollectionViewCell. So send the cell in the performSegue function.
Edit: Nirav D makes a good point in mentioning that there is no reason to send the cell since it just gets converted back into indexPath anyway. We could solve this entire issue by doing:
performSegue(withIdentifier: "itemSegue":, sender: indexPath)
and:
if let indexPath = sender as? IndexPath {
destinationController.getProduct = productsArray[indexPath.row]
}

Related

Swift: having trouble getting segue to show details from selecting item in a view collection

I am trying to set a View Collection to segue to a item's details when the item's image is clicked on. Here is the code I have. However, when I try, I get the error, Thread 1: Fatal error: Index out of range, next to this line of code: destinationController.shop = shopping[indexPaths[0].row]
Here is my full code for the collection view controller. Can anyone tell me what I am doing wrong?
import UIKit
private let reuseIdentifier = "Cell"
class shopCollectionViewController: UICollectionViewController {
#IBAction func unwindToMain(segue: UIStoryboardSegue){
}
var shopping: [Shop] = [Shop(image:"blueTshirt", name:"blueTshirt", price: 10), Shop(image:"blackTshirt", name:"blackTshirt", price: 10), Shop(image:"lightblueTshirt", name:"lightblueTshirt", price: 10), Shop(image:"whiteTshirt", name:"whiteTshirt", price: 10)]
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Do any additional setup after loading the view.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
}
*/
// MARK: UICollectionViewDataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
return shopping.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "dataCell", for: indexPath) as! shopCollectionViewCell
// Configure the cell
let item = shopping[indexPath.row]
cell.itemImage.image = UIImage(named: item.image)
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
performSegue(withIdentifier: "showDetail", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPaths = collectionView.indexPathsForSelectedItems{
let destinationController = segue.destination as! itemDetailsViewController
destinationController.shop = shopping[indexPaths[0].row]
collectionView.deselectItem(at: indexPaths[0], animated: false)
}
}
When you use collectionView maybe you should use indexPath.item instead of indexPath.row
A more reliable way than indexPathsForSelectedItems is to pass the index path while calling performSegue.
And deselect the cell right inside didSelectItemAt
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
performSegue(withIdentifier: "showDetail", sender: indexPath)
collectionView.deselectItem(at: indexPath, animated: false)
}
And replace prepare(for with
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
let indexPath = sender as! IndexPath
let destinationController = segue.destination as! itemDetailsViewController
destinationController.shop = shopping[indexPath.item]
}
}

Trouble Passing Variable Info to First VC Using Closure

I am trying to pass the data of which name was selected by the user in my ShipViewController to my ProfileViewController. I tried using closures to do so, but the title of the button in ProfileViewController (which presents the modal popover to ShipViewController) isn't changing to the name the user selects in ShipViewController.
Should it not be String --> () or is the way I instantiate my view controller incorrect?
(ShipViewController)
var completionHandler:((String) -> ())?
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "shipCell", for: indexPath) as! ShipViewCell
if selectedIndex == indexPath.row {
let result = completionHandler?(shipNames[selectedIndex!])
self.dismiss(animated: true, completion: nil)
}
}
(In viewDidLoad of ProfileViewController)
let vc = storyboard?.instantiateViewController(withIdentifier: "ShipViewController") as! ShipViewController
vc.completionHandler = { (text) -> ()in
print(text)
self.shipButton.setTitle(text, for: .normal)
}
Dismiss ShipViewController in didSelectItemAt
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let result = completionHandler?(shipNames[indexPath.item])
self.dismiss(animated: true, completion: nil)
}
In ProfileViewController Don't assign to completionHandler in viewDidLoad
Assign to completion handler in prepare for segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showShip" {
if let vc = segue.destination as? ShipViewController {
vc.completionHandler = { (text) -> ()in
print(text)
self.shipButton.setTitle(text, for: .normal)
}
}
}
}

Use perform segue into a collection view

i need to pass my data into collection view with index path.
i'm trying to use this code but i don't know why i have 2 times the result, one time it's ok and the second one is nil.
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.performSegue(withIdentifier: "showDetails", sender: indexPath)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let cell = sender as? UICollectionViewCell,
let indexPath = self.collectionView.indexPath(for: cell) {
let secondViewController = segue.destination as! CollectionDetailViewController //Cast with your DestinationController
//Now simply set the title property of vc
secondViewController.people = person[indexPath.row]
}
}
You are getting 2 calls to prepare(for:sender:) because your segue is wired from your UICollectionViewCell. This will call prepare(for:sender:) for you, so you don't need this code:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.performSegue(withIdentifier: "showDetails", sender: indexPath)
}
Delete it, and you should be good to go.

Prepare for segue not passing data

I'm working on a Swift project using firebase and I have a UICollectionViewController that has all the categories, and I need to pass the category Id to the next view, but It passes an empty string
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let category = cnameArray[indexPath.row]
let catId = category.caId
func prepare(for segue: UIStoryboardSegue, sender: Any?){
if segue.identifier == "CatCity" {
let cityView = segue.destination as? CityViewController
cityView?.catId = catId!
} }
self.performSegue(withIdentifier: "CatCity", sender: self)
}
and in my CityViewController I have
var catId = String()
when I put a break point at the last line of the prepare func it gets hit and the console gets the value correctly while sitting a breakpoint at CityViewController gives me an empty var.
and I checked the code and my segue name and everything looks fine!
Correct setup
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let category = cnameArray[indexPath.row]
self.performSegue(withIdentifier: "CatCity", sender: category.caId)
}
func prepare(for segue: UIStoryboardSegue, sender: Any?){
if segue.identifier == "CatCity" {
if let cityView = segue.destination as? CityViewController {
cityView.catId = sender as! String
}
}
}

How to send indexpath when performing segue on collectionViewCell?

I have a collection view controller with similar cells and I want each cell open next viewController with specific content. I think I must send indexpath.row with the segue but in this part I face an error:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let newViewController = segue.destination as? myCollectionViewController {
newViewController.passedValue = indexPath.row // exactly here
}
}
There are a couple of things wrong in your code. indexPath.row variable is not declared in your prepare function. To pass the selected cell info to another view controller you first need to add a global variable in your class.
var selectedCell = 0
Next implement the function didSelectRowAtIndexPath in your class. This function is called every time you select a cell. In this function get the cell info and assign it to selectedCell var
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
selectedCell = indexPath.row
}
Now in your prepare method use this var value and pass it on to the next controller
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "YourIdentifier" {
let VC = segue.destination as! YourViewController
VC.passed = selectedCell
}
}
Hope this helps.
In the above code:
newViewController.passedValue = indexPath.row // exactly here
what is the value of indexPath here? Is is an instance variable?
Instead of using segue, use UICollectionViewDelegate method:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
//Push your controller here
}