Perform Segue from UICollectionViewCell Button different from Cell Click - ios

I have a UICollectionViewCell, with a UIButton. And I have two different actions. The first one, when the user presses the cell, it will segue to another view withdidSelectITemAt; the second one, when the users presses the UIButton inside the cell.
My problem is that on Swift Code of MyCollectionViewCell, I cant perform a segue, When I write the Code:
self.performSegue(withIdentifier: "toStoreFromMyDiscounts", sender: nil)
it says the error:
Value of Type MyCollectionViewCell has no member performSegue.
I also cannot write prepareForSegue, it doesn't auto complete.
How can I create a segue from a cell, that is different from click the cell itself?

You can not call performSegue from your UICollectionViewCell subclass, because there is no interface declared on UICollectionViewCell like that.
The reason why it is working didSelectItemAtIndexPath() is because i suppose the delegate of your UICollectionView is a UIViewController subclass, what has the function called performSegueWithIdentifier:()`.
You need to notify your UIViewController when the button was clicked in your UICollectionViewCell, for what you have various possibilities, like KVO or using delegate.
Here is a little code sniplet, how to use KVO. This solution is great, as long as you do not care, in which cell was the button pressed.
import UIKit
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var button: UIButton!
}
class CollectionViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
}
extension CollectionViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: CollectionViewCell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
// Add your `UIViewController` subclass, `CollectionViewController`, as the target of the button
// Check out the documentation of addTarget(:) https://developer.apple.com/reference/uikit/uicontrol/1618259-addtarget
cell.button.addTarget(self, action: #selector(buttonTappedInCollectionViewCell), for: .touchUpInside)
return cell
}
func buttonTappedInCollectionViewCell(sender: UIButton) {
self.performSegue(withIdentifier: "toStoreFromMyDiscounts", sender: nil)
}
}
EDIT:
If you care, in which cell the touch event has happend, use the delegate pattern.
import UIKit
protocol CollectionViewCellDelegate: class {
// Declare a delegate function holding a reference to `UICollectionViewCell` instance
func collectionViewCell(_ cell: UICollectionViewCell, buttonTapped: UIButton)
}
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var button: UIButton!
// Add a delegate property to your UICollectionViewCell subclass
weak var delegate: CollectionViewCellDelegate?
#IBAction func buttonTapped(sender: UIButton) {
// Add the resposibility of detecting the button touch to the cell, and call the delegate when it is tapped adding `self` as the `UICollectionViewCell`
self.delegate?.collectionViewCell(self, buttonTapped: button)
}
}
class CollectionViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
}
extension CollectionViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: CollectionViewCell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
// Asssign the delegate to the viewController
cell.delegate = self
return cell
}
}
// Make `CollectionViewController` confrom to the delegate
extension CollectionViewController: CollectionViewCellDelegate {
func collectionViewCell(_ cell: UICollectionViewCell, buttonTapped: UIButton) {
// You have the cell where the touch event happend, you can get the indexPath like the below
let indexPath = self.collectionView.indexPath(for: cell)
// Call `performSegue`
self.performSegue(withIdentifier: "toStoreFromMyDiscounts", sender: nil)
}
}

Here's an elegant solution that only requires a few lines of code:
Create a custom UICollectionViewCell subclass
Using storyboards, define an IBAction for the "Touch Up Inside" event of your button
Define a closure
Call the closure from the IBAction
Swift 4+ code
class MyCustomCell: UICollectionViewCell {
static let reuseIdentifier = "MyCustomCell"
#IBAction func onAddToCartPressed(_ sender: Any) {
addButtonTapAction?()
}
var addButtonTapAction : (()->())?
}
Next, implement the logic you want to execute inside the closure in your
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCustomCell.reuseIdentifier, for: indexPath) as? MyCustomCell else {
fatalError("Unexpected Index Path")
}
// Configure the cell
// ...
cell.addButtonTapAction = {
// implement your logic here, e.g. call preformSegue()
self.performSegue(withIdentifier: "your segue", sender: self)
}
return cell
}
You can use this approach also with table view controllers.

Another solution that also works like a charm:
extension YOURViewController : UICollectionViewDataSource
{
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "YOURCell", for: indexPath) as! YOURCollectionViewCell
cell.butTapped = {
[weak self] (YOURCollectionViewCell) -> Void in
// do your actions when button tapped
}
}
return cell
}
class YOURCollectionViewCell: UICollectionViewCell
{
var butQRTapped: ((YOURCollectionViewCell) -> Void)?
#IBAction func deleteButtonTapped(_ sender: AnyObject) {
butTapped?(self)
}
}

Related

Hint on how to create & pass data via segue when collectionview cell is pressed

I have a UICollectionView embedded in a UITableViewCell & I want to perform a segue when a UICollectionViewCell is pressed & pass the data represented by that cell to the destination ViewController for detailed information.
Here is the code for the embedded UICollectionView inside the UITableViewCell
#IBOutlet weak var EventCollection: UICollectionView!
var events = [Events]()
override func awakeFromNib() {
super.awakeFromNib()
EventCollection.delegate = self
EventCollection.dataSource = self
}
extension PopularCell: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return events.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = EventCollection.dequeueReusableCell(withReuseIdentifier: "EventCell", for: indexPath) as! EventCell
let event = events[indexPath.row]
print("Event Name:\(event.event_name)")
cell.event = event
cell.tag = indexPath.row
return cell
}
How do I perform & prepare a segue in the main ViewController when a UICollectionViewCell is pressed so as to pass the data contained by that cell to the destination ViewController
Here are the steps that you need to do.
As you said, your CollectionView is inside TableView. So your TableView delegates/DataSources binded with the MainViewController. CollectionView delegates/DataSources binded with the TableViewCell.
Now create a protocol to know user has clicked on collectionView.
protocol MyProtocol : class {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
}
In TableViewCell, you need to call delegate like this,
class MyTableCell : UITableViewCell, UICollectionViewDelegate {
weak var delegate : MyProtocol?
:
:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let delegate = delegate {
delegate.collectionView(collectionView, didSelectItemAt: indexPath)
}
}
}
Now your MainViewController must have to conform this protocol,
class MainViewController :UIViewController, MyProtocol {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// Do segue here ....
}
}
Note : Make sure to bind delegate with your MainViewController i.e. in TableviewCellForRow have cell.delegate = self
Add the following code with in a new file named UIView_Ext
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
In func didSelectItem(At indexPath: IndexPath) method , write the following code
self.parentViewController?.performSegue(withIdentifier: "Identifer", sender: "Your Data in place of this string")

UICollectionView Can't use IBOutlets in ViewController.swift [duplicate]

This question already has answers here:
Outlets cannot be connected to repeating content iOS
(9 answers)
Closed 4 years ago.
I followed the attached guide to creating static UICollectionView but now I would like to add buttons to each cell and change the text on the buttons, for example. I can not do this and get the error "UIButton is invalid. Outlets cannot be connected to repeating content." How can I fix this issue and use IBOutlets with objects in cells without leaving the ViewController?
If I need to leave ViewController please describe the process with a lot of detail as I am a beginner and am not too knowledgeable on the different view classes.
Thank you!!
Instead of the outlet between the button and the view controller, you should create a subclass of UICollectionViewCell, and add your IBOutlets on that class.
class MyCollectionViewCell: UICollectionViewCell {
#IBOutlet var myButton: UIButton!
}
Then, in Interface Builder, set this subclass to be the class of your cells (in the Identity inspector pane).
You should then be able to create the outlet connection from your button to your cell.
I hope this is clear enough. If not, please let me know!
Example code
class MyCollectionViewCell: UICollectionViewCell {
#IBOutlet var myButton: UIButton!
}
class MyViewController: UIViewController, UICollectionViewDataSource {
#IBOutlet var myCollectionView: UICollectionView!
private var isMyButtonEnabled = true
// Other view controller code
func disableMyButton() {
self.isMyButtonEnabled = false
self.myCollectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = ... as! MyCollectionViewCell // Get cell
// Other cell setup
cell.myButton.isEnabled = self.isMyButtonEnabled
return cell
}
}
Define class like following for your collection view:
class MyCollectionCell : UICollectionViewCell {
#IBOutlet weak var likeButton: UIButton?
}
Create xib for collection cell and use above custom class for collection view.
Now in your view controller define collection view and implement following delegates UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout.
class ViewController: UIViewController, UICollectionViewDataSource,
UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
#IBOutlet var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "MyCollectionViewCell", bundle: nil)
collectionView?.registerNib(nib, forCellWithReuseIdentifier: "myCell")
}
//UICollectionViewDelegateFlowLayout methods
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat
{
return 4;
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat
{
return 1;
}
//UICollectionViewDatasource methods
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int
{
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell =
collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as MyCollectionCell
cell.likeButton.setTitle("myTitle", for: .normal)
cell.likeButton.tag = indexPath.row
cell.likeButton.addTarget(self, action: #selector(mainButton:), forControlEvents: .TouchUpInside)
return cell
}
#IBAction func mainButton(sender: UIButton) {
println(sender)
// use button tag to find out which button is clicked.
}
}
In above code important method is func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell where you set tag to your button and then use that tag to find out which button is pressed and use that id to find out data source or action you want to perform.

Make sure only 1 cell has an active state in a UICollectionView

I have an UICollectionView in which I want only want 1 cell to be active. With active I mean: the last cell that has been clicked (or the very first cell when to collection view lays out). When a user clicks a non-active cell, I want to reset the old active cell to a non-active state. I am having trouble doing this. This is because visibleCells, a property of collection view, only returns the cells on screen but not the cells in memory. This is my current way to locate an active cell and reset the state to non active.
This scenario can happen, causing multiple active cells: A user scroll slightly down so that the current active cell is not visible anymore, taps on a random cell and scroll up. The problem is that the old active cell stays in memory, although it is not visible: cellForItemAt(_:) does not gets called for that cell. Bad news is that visibleCells also do not find the old active cell. How can I find it? The function willDisplay cell also does not work.
An example project can be cloned directly into xCode: https://github.com/Jasperav/CollectionViewActiveIndex.
This is the code in the example project:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CollectionView!
static var activeIndex = 0
override func viewDidLoad() {
super.viewDidLoad()
collectionView.go()
}
}
class Cell: UICollectionViewCell {
#IBOutlet weak var button: MyButton!
}
class CollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {
func go() {
delegate = self
dataSource = self
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 500
}
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
if indexPath.row == ViewController.activeIndex {
cell.button.setTitle("active", for: .normal)
} else {
cell.button.setTitle("not active", for: .normal)
}
cell.button.addTarget(self, action: #selector(touchUpInside(_:)), for: .touchUpInside)
return cell
}
#objc private func touchUpInside(_ sender: UIButton){
let hitPoint = sender.convert(CGPoint.zero, to: self)
guard let indexPath = indexPathForItem(at: hitPoint), let cell = cellForItem(at: indexPath) as? Cell else { return }
// This is the problem. It does not finds the current active cell
// if it is just out of bounds. Because it is in memory, cellForItemAt: does not gets called
if let oldCell = (visibleCells as! [Cell]).first(where: { $0.button.titleLabel!.text == "active" }) {
oldCell.button.setTitle("not active", for: .normal)
}
cell.button.setTitle("active", for: .normal)
ViewController.activeIndex = indexPath.row
}
}
To recover from this glitch you can try in cellForRowAt
cell.button.tag = indexPath.row
when the button is clicked set
ViewController.activeIndex = sender.tag
self.reloadData()
You can use the isSelected property of the UIColectionViewCell. You can set an active layout to your cell if it is selected. The selection mechanism is implemented by default in the UIColectionViewCell. If you want to select/activate more than one cell you can set the property allowsMultipleSelection to true.
Basically this approach will look like this:
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.go()
}
func activeIndex()->Int?{
if let selectedItems = self.collectionView.indexPathsForSelectedItems {
if selectedItems.count > 0{
return selectedItems[0].row
}
}
return nil
}
}
class Cell: UICollectionViewCell {
#IBOutlet weak var myLabel: UILabel!
override var isSelected: Bool{
didSet{
if self.isSelected
{
myLabel.text = "active"
}
else
{
myLabel.text = "not active"
}
}
}
}
class CollectionView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource {
func go() {
delegate = self
dataSource = self
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 500
}
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
return cell
}
}

iOS (Swift): UICollectionView for a given UICollectionViewCell

I have a UICollectionViewCell subclass that is contained within a UICollectionView as follows:
class MyCell: UICollectionViewCell {
}
class MyViewController: UIViewController, UICollectionViewDataSource {
#IBOutlet public var collectionView: UICollectionView!
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as? MyCell else { fatalError("Unexpected indexPath") }
return cell
}
}
Is there any way to access the collectionView instance defined in MyViewController from within MyCell?
Thanks for any help.
You can try
class MyCell: UICollectionViewCell, UICollectionViewDataSource {
weak var outerCollectionView: UICollectionView!
}
and set it in cellForRowAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as? MyCell else { fatalError("Unexpected indexPath") }
cell.outerCollectionView = collectionView
return cell
}
//
Or you can try to cast the superView like this
let collectionView = self.superView as! UICollectionView
Try something like below
protocol MyCellDelegate {
func reloadData()
}
class MyCell: UICollectionViewCell {
var delegate: MyCellDelegate!
}
class MyViewController: UIViewController {
//lets say this is your view controller and datasource
}
extension MyViewController: UICollectionViewDataSource {
//Methods are not implemeted here
//But you can set the delegate of the cell in cellForRowAtIndex method which is your view controller
}
extension MyViewController: MyCellDelegate {
func reloadData() {
//Here you reload the collection view
}
}
Following suggestions in the comments:
extension UICollectionViewCell {
/// The collection view that presents this cell.
weak var collectionView: UICollectionView! {
return self.superview as! UICollectionView
}
}

Singleton Class not updating properly

I've got a collectionViewController and a normal viewcontroller. When a cell is tapped it goes to the VC and sets the label to the cell tapped. The variable for this is in a singleton class.
The issue I'm having is that the first time you tap a cell and go to the VC the label doesn't say anything (console prints correct data though). Then you go back to the collectionView and tap a different cell, the label in the view now shows the cell you tapped previously.
I tried cleaning the build folder etc. but didn't do anything. I also tried another method - let CVC = CollectionViewController() then lbl.text = CVC.cellTapped (create var first) but that didnt work either.
SharingManager.swift
class SharingManager {
var cellChoose = String()
static let sharedInstance = SharingManager()
}
CollectionViewController.swift
class CollectionViewController: UICollectionViewController {
let sm = SharingManager.sharedInstance
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
// Configure the cell
let lbl = cell.viewWithTag(1) as! UILabel
lbl.text = String(indexPath.row)
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
sm.cellChoose = "cell\(indexPath.row)"
print(sm.cellChoose)
}
}
VC2.swift (the viewcontroller tapping a cell takes you to)
class VC2: UIViewController {
let sm2 = SharingManager.sharedInstance
#IBOutlet weak var lbl2: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.lbl2.text = sm2.cellChoose
//print(sm2.cellChoose)
}
Since you're navigating via a segue, it's possible your new ViewController is loaded before didSelectItem is called. Moving lbl2.text = sm2.cellChoose to viewWillAppear will fix your issue.
The "correct" way to do this with segues is handle this in prepareForSegue, but what you have now will work.

Resources