UICollectionViewCell Talking to Its UICollectionViewController - ios

Albeit potentially subjective, I was wondering how to go about having a custom UICollectionViewCell that when its UIButton is pressed, informs a custom UICollectionViewController of what to do.
My first thought was use a delegate in the CustomCell as follows:
class CustomCell: UICollectionViewCell {
var delegate: CustomCellDelegate?
static let reuseIdentifier = "CustomCell"
#IBOutlet weak private var button: UIButton! {
didSet {
button.addTarget(self, action: #selector(self.toggleButton), for: .touchUpInside)
}
}
#objc private func toggleButton() {
delegate?.didToggleButton()
}
}
where the class protocol for CustomCellDelegate is defined as:
protocol CustomCellDelegate: class {
func didToggleButton()
}
The UICollectionViewController then implements the didToggleButton function and assigns itself as the delegate to each cell as follows:
class CustomCollectionViewController: UICollectionViewController, CustomCellDelegate {
func didToggleButton() {
// do some stuff and then update the cells accordingly ...
collectionView?.reloadData()
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let customCell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCell.reuseIdentifier, for: indexPath) as? CustomCell else { fatalError("Unexpected indexPath") }
customCell.delegate = self
return customCell
}
}
Is this the correct way to go about this, or is there another way to to communicate between a UICollectionViewCell and its parent controller?
Thanks for any suggestions.

Yes, it's the right solution for sure. Your custom cells are blind and they don't know anything about your controller. They only fire delegate methods.
But, there is one more right solution and it's observation. Somebody prefers delegation, somebody prefers observation. You can use NotificationCenter to post your notifications about touches happening in your cells and make your controller an observer which reacts to these notifications.
// inside your cell
NotificationCenter.default.post(name: Notification.Name("ButtonPressed"), object: nil)
// inside your controller
NotificationCenter.default.addObserver(self, selector: #selector(someHandler), name: Notification.Name("ButtonPressed"), object: nil)
And your func someHandler() will handle the call when your controller (observer) catches posted events.
Also, there is KVO, but it's messy and isn't good for that particular case since you have multiple cells.
One more way to setup communication channel is binding. It can be both manually written or reactive (e.g., using ReactiveSwift).
For example, manual one:
// in your controller
cell.pressHandler = {
// do something
...
}
// in your cell
var pressHandler: (() -> Void)?
...
// when the button is pressed you execute that handler
pressHandler?()

Yes, delegation is optimal when a communication has to be done to only one object. In this case the parent UICollectionViewController
Other methods of communications are-
Notification: When we want to communicate multiple objects post a notification.
KVO: To know when a value/property has changed. But use carefully.

Related

How do I pass button action from a nested collectionView cell?

I have a MainCollectionView used for scrolling between items, inside of one of these cells I have another collectionView with cells. In that collection view, I have a button for each cell. My question is how do I pass action from my button to my MainCollectionView when it is tapped? I did create protocol for that button in the cell but I don't know how to let MainCollectionView know when my button is tapped. I can call action from my cell class but I think it is better to run it in Model which is my MainCollectionView. Below is my button protocol.
protocol ThanhCaHotTracksCellDelegate: class {
func handleSeeAllPressed()}
weak var delegate: ThanhCaHotTracksCellDelegate?
#objc func handleSeeAllButton(){
delegate?.handleSeeAllPressed()
}
LIke NSAdi said, you're on the right track, but the delegate pattern is a bit much overhead for just a single task like notifying about a button press.
I prefer using closures, because they're lightweight and helps to keep related code together.
Using Closures
This is what I'm always doing in UITableView. So this will work in UICollectionView too.
class MyTableViewCell: UITableViewCell {
var myButtonTapAction: ((MyTableViewCell) -> Void)?
#IBAction func myButtonTapped(_ sender: Any) {
myButtonTapAction?(self)
}
}
So when I dequeue my cell and cast it to MyTableViewCell I can set a custom action like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCellReuseIdentifier", for: indexPath) as! MyTableViewCell
cell.myButtonTapAction = { cell in
// Put your button action here
// With cell you have a strong reference to your cell, in case you need it
}
}
Using direct reference
When you're dequeueing your UICollectionView cell you can obtain a reference to your button by casting the cell to your cell's custom subclass.
Then just do the following
cell.button.addTarget(self, action: #selector(didTapButton(_:)), forControlEvents: .TouchUpInside)
And outside have a function:
#objc func didTapButton(_ sender: UIButton) {
// Handle button tap
}
Downside of this is that you have no direct access to your cell. You could use button.superview? but it's not a good idea since your view hierarchy could change...
You're on the right track.
Make sure MainCollectionView (or the class that contains) it implements ThanhCaHotTracksCellDelegate protocol.
Then assign the delegate as self.
Something like...
class ViewController: ThanhCaHotTracksCellDelegate {
override func viewDidLoad() {
super.viewDidLoad()
subCollectionView.delegate = self
}
}

how to notify the controller when something in a uicollectionviewcell is pressed

I have an extension of UICollectionViewCell class. When something is pressed in this cell, I am trying to notify the Controller. I am not quite sure if the protocol delegate pattern is the way to go about it. I am not sure how to use it in this case. I have the following class outside my extension of UICollectionViewCell class.
protocol bundleThreadsDelegate: class {
func bundleThreadsDidSelect(_ viewController: UIViewController)
}
And I have the following property:
public weak var delegate: bundleThreadsDelegate? in my extension.
I am not quite sure where to go on from Here. Please help.
You said "when something is pressed in this cell", not when the cell itself is pressed, so I assume you may want multiple actionable items in your cell. If that's what you really mean then in your UICollectionViewCell, you could simply add a UIButton (no need for delegates as mentioned here because everything can happen within the same view controller—use delegates when communicating between different objects):
class MyCollectionViewCell: UICollectionViewCell {
let someButton = UIButton()
...
}
When you create the UICollectionView, to make it easiest, set the view controller it's in as the data source:
let myCollection = UICollectionView(frame: .zero, collectionViewLayout: MyCollectionViewFlowLayout())
myCollection.dataSource = self
...
Then in your data source, which would be in something like MyViewController, give the button a target:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let path = indexPath.item
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myCell", for: indexPath) as! MyCollectionViewCell
cell.someButton.addTarget(self, action: #selector(someButtonAction(_:)), for: .touchUpInside)
return cell
}
And make sure that the action method is also in MyViewController along with the data source.
#objc func someButtonAction(_ sender: UIButton) {
print("My collection view cell was tapped")
}
Now you can have multiple buttons within one collection view cell that do different things. You can also pass in arguments from the cell to the button action for further customization.
However, if you want action when the entire cell is pressed, use the delegate method already mentioned (or make the entire cell a UIButton which is not as elegant but that's open to interpretation).
Use this CollectionView Delegate method to notify the ViewController when your cell is selected by a user.
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath)
Chris is correct, assuming you're only interested in the whole cell being selected. If you have a button or something within your cell and it's that press event you're interested in then yeah, you could use a delegate.
As an aside, protocols usually start with an uppercase letter, i.e. BundleThreadsDelegate rather than bundleThreadsDelegate, but that's up to you. Your general approach could be something like this (note this is pseudo code):
protocol YourProtocol {
func didPressYourButton()
}
class YourCell {
#IBOutlet weak var yourButton: UIButton!
public weak var yourButtonDelegate: YourProtocol?
func awakeFromNib() {
super.awakeFromNib()
yourButton.addTarget(self, action: #selector(didPressYourButton), for: .touchUpInside)
}
func didPressYourButton() {
yourButtonDelegate?.didPressYourButton()
}
}
And then in your view controller's cellForRowAt function:
let cell = ...
cell.yourButtonDelegate = self
Then conform to the protocol and implement the method in your view controller:
extension YourViewController: YourProtocol {
func didPressYourButton() {
doAllTheThings()
}
}

Delegate Method to UItableViewCell Swift

I have a Social Network Feed in form UItableView which has a cell. Now each cell has an image that animates when an even is triggered. Now, This event is in form of a string, will be triggered at every cell. the options for the event are defined in another class(of type NSObject).
My issue:
I constructed a protocol delegate method in table view, which will be called whenever the event is triggered for each cell. Then, I define this function in UITableViewCell Class, since my the image will be animating on that.
All is working well but I am unable to figure out how to assign the delegate of TableView class to cell class. What I mean is, how can I use UITableView.delegate = self in cellView class. I have tried using a static variable, but it doesn't work.
I have been playing around the protocols for a while now but really unable to figure out a solution to this.
I hope I am clear. If not, I will provide with an example in the comments. I am sorry, This is a confidential project and I cant reveal all details.
If I understand you correctly, you are trying to make each of your cells conform to a protocol that belongs to their UITableView? If this is the case then this cannot be done. The Delegation design pattern is a one to one relationship, i.e only one of your UITableViewCells would be able to conform to the UITableView's delegate.
Delegation is a simple and powerful pattern in which one object in a program acts on behalf of, or in coordination with, another object. The delegating object keeps a reference to the other object—the delegate—and at the appropriate time sends a message to it. The message informs the delegate of an event that the delegating object is about to handle or has just handled. The delegate may respond to the message by updating the appearance or state of itself or other objects in the application, and in some cases it can return a value that affects how an impending event is handled. The main value of delegation is that it allows you to easily customize the behavior of several objects in one central object.
Quote from the Apple Docs
I would suggest that your UITableViewCell should call a block (Objective-C) or a closure (Swift) whenever your specified event is triggered to achieve what you are looking for. Set up this closure in your tableView:cellForRowAtIndexPath function.
EXAMPLE
TableViewController
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCellID", for: indexPath) as! MyTableViewCell
cell.eventClosure = {
//Do something once the event has been triggered.
}
return cell
}
TableViewCell
func eventTriggered()
{
//Call the closure now we have a triggered event.
eventClosure()
}
If I correctly understood your question, maybe this could help:
class ViewController: UIViewController, YourCustomTableDelegate {
#IBOutlet weak var tableView: YourCustomTableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.customTableDelegate = self
}
// table delegate method
func shouldAnimateCell(at indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.animate(...)
}
}
}
Try something like this:
Define your delegate protocol:
protocol CustomCellDelegate: class {
func animationStarted()
func animationFinished()
}
Define your CustomCell. Extremely important to define a weak delegate reference, so your classes won't retain each other.
class CustomCell: UITableViewCell {
// Don't unwrap in case the cell is enqueued!
weak var delegate: CustomCellDelegate?
/* Some initialization of the cell */
func performAnimation() {
delegate?.animationStarted()
UIView.animate(withDuration: 0.5, animations: {
/* Do some cool animation */
}) { finished in
self.delegate?.animationFinished()
}
}
}
Define your view controller. assign delegate inside tableView:cellForRowAt.
class ViewController: UITableViewDelegate, UITableViewDataSource {
/* Some view controller customization */
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: CustomCell.self)) as? CustomCell
cell.delegate = self
cell.performAnimation()
return cell
}
}

Subclass UIButton to save Data?

I was wondering if there is any problem with extending UIButton in order to have a ref of my Data?
For instance I have a table view and whenever user clicks on a cell I need to provide the user with the data for that specific cell.
Data is kept inside an array and array index was saved in UIButton tag but as my array gets updated, wrong indexes will be provided. So what i was trying to do was to extend a uibutton and then have a variable which holds my model.
This idea works fine but as Im not really experienced in swift I wanted to know what are the drawbacks and problems of doing such a thing.
You don't need to save the index as Button's tag. Subclassing the UIButton as Sneak pointed out in comment is clearly a very bad idea. On top of that saving your model in a UIComponent is disasters design.
You can do it multiple ways. One that I find neat :
Step 1:
Create a Class for your Custom Cell. Lets say MyCollectionViewCell. Because your Cell has a button inside it, you should create IBAction of button inside the Cell.
class MyCollectionViewCell: UICollectionViewCell {
#IBAction func myButtonTapped(_ sender: Any) {
}
}
Step 2:
Lets declare a protocol that we will use to communicate with tableView/CollectionView's dataSource.
protocol MyCollectionViewCellProtocol : NSObjectProtocol {
func buttonTapped(for cell : MyCollectionViewCell)
}
Step 3:
Lets create a property in our MyCollectionViewCell
weak var delegate : MyCollectionViewCellProtocol? = nil
After step 3 your MyCollectionViewCell class should look like
protocol MyCollectionViewCellProtocol : NSObjectProtocol {
func buttonTapped(for cell : MyCollectionViewCell)
}
class MyCollectionViewCell: UICollectionViewCell {
weak var delegate : MyCollectionViewCellProtocol? = nil
#IBAction func myButtonTapped(_ sender: Any) {
self.delegate?.buttonTapped(for: self)
}
}
Step 4:
In your tableView's CellForRowAtIndexPath or CollectionView's sizeForItemAtIndexPath confirm pass ViewController as delegate to cell.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell : MyCollectionViewCell = //deque your cell
(cell as! MyCollectionViewCell).delegate = self
}
Step 5:
Now make your ViewController confirm to protocol
class YourViewController: UIViewController,MyCollectionViewCellProtocol {
Step 6:
Finally implement method in your VC
func buttonTapped(for cell: MyCollectionViewCell) {
let indexPath = self.collectionView?.indexPath(for: cell)
//access array now
}
P.S:
Sorry though I know you are using TableView, in a hurry I have written code for CollectionView but the delegates are pretty same :) I hope you will be able to understand the logic

Perform Segue from Collection View Cell

import UIKit
class ActionCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var myLabel: UILabel!
#IBOutlet weak var actionGIF: UIImageView!
#IBAction func actionPressed(sender: AnyObject) {
print(myLabel.text)
Global.actionButtonIndex = myLabel.text!.toInt()! - 1
print(actionGIF.image)
ActionViewController.performSegueWithIdentifier("showActionPreview", sender: nil)
}
}
I am trying to perform a Segue after User Clicking on One of the Cell in my Collection View. Can't seem to do that using performSegueWithIdentifier. App Screenshot
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.
Instance method performSegue is not available from a UICollectionViewCell:
Since an UICollectionViewCell is not an UIViewController, you can not use performSegue(withIdentifier:sender:) from it. You may prefer use delegates to notify your parent view controller and then, performSegue from there.
Take a look at the details of this answer. The question is slightly different but the solution lies in the same pattern.
Have you set the segue identifier by exactly named "showActionPreview". Moreover, ensure that your segue linked from your parent view controller to your destination view controller in storyboard. Hope this would be help.

Resources