Prevent `didSelect…` for part of a UICollectionViewCell - ios

Summary
Can a UICollectionViewCell subclass prevent didSelectItemAt: indexPath being sent to the UICollectionViewDelegate for taps on some of its sub views, but to proceed as normal for others?
Use case
I have a UICollectionViewCell that represents a summary of an article. For most articles, when they are tapped, we navigate through to show the article.
However, some article summaries show an inline video preview. When the video preview is tapped, we should not navigate through, but when the other areas of the article summary are tapped (the headline), we should navigate through.
I'd like the article summary cell to be able to decide whether a tap on it should be considered as a selection.

You have to add tapGestureRecogniser on those subviews of cell on which you don't want delegate to get called.
tapGestureRecogniser selector method will get called when you will tap on those subview and gesture will not get passed to delegate.

What you need to do is to attach UITapGestureRecognizer to your view and monitor taps from it:
class MyViewController: UIViewController, UICollectionViewDataSource, MyCellDelegate {
var dataSource: [Article] = []
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ArticleCell", for: indexPath) as! MyCell
cell.article = dataSource[indexPath.row]
cell.delegate = self
return cell
}
func articleDidTap(_ article: Article) {
// do what you need
}
}
// your data model
struct Article {}
protocol MyCellDelegate: class {
func articleDidTap(_ article: Article)
}
class MyCell: UICollectionViewCell {
var article: Article! {
didSet {
// update your views here
}
}
weak var delegate: MyCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(MyCell.tap)))
}
#objc func tap() {
delegate?.articleDidTap(article)
}
}
This should work since your video view should overlap root view and prevent receiving taps from gesture recognizer.

Related

How to update a ULabel which is in a viewController from a custom UICollectionViewCell in swift [duplicate]

I have a custom UICollectionViewCell with a button inside it. When I tap the button, an event is fired inside that subclass. I want to then trigger an event on the UICollectionView itself, which I can handle in my view controller.
Pseudo-code:
class MyCell : UICollectionViewCell {
#IBAction func myButton_touchUpInside(_ sender: UIButton) {
// Do stuff, then propagate an event to the UICollectionView
Event.fire("cellUpdated")
}
}
class MyViewController : UIViewController {
#IBAction func collectionView_cellUpdated(_ sender: UICollectionView) {
// Update stuff in the view controller
// to reflect changes made in the collection view
}
}
Ideally, the event I define would appear alongside the default action outlets in the Interface Builder, allowing me to then drag it into my view controller code to create the above collectionView_cellUpdated function, similar to how #IBInspectable works in exposing custom properties.
Is there any way to implement a pattern like this in native Swift 3? Or if not, any libraries that make it possible?
I don't understand your question completely but from what I got, you can simply use a closure to pass the UIButton tap event back to the UIViewController.
Example:
1. Custom UICollectionViewCell
class MyCell: UICollectionViewCell
{
var tapHandler: (()->())?
#IBAction func myButton_touchUpInside(_ sender: UIButton)
{
tapHandler?()
}
}
2. MyViewController
class MyViewController: UIViewController, UICollectionViewDataSource
{
//YOUR CODE..
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCell
cell.tapHandler = {
//Here you can do your custom handling
}
return cell
}
}
Let me know if you face any issues.
Best thing to do is to make a custom protocol for your custom cell class
protocol CustomCellProtocolDelegate {
func custom(cell: YourCellClass, hadButton: UIButton, pressedWithInfo : [String:Any]?)
}
Make this cell class have this protocol as a peculiar delegate, and to trigger this delegate:
class YourCellClass: UICollectionViewCell {
var delegate : CustomCellProtocolDelegate?
var indexPath : IndexPath? //Good practice here to have an indexPath parameter
var yourButton = UIButton()
init(frame: CGRect) {
super.init(frame: frame)
yourButton.addTarget(self, selector: #selector(triggerButton(sender:)))
}
func triggerButton(sender: UIButton) {
if let d = self.delegate {
d.custom(cell: self, hadButton: sender, pressedWithInfo : /*Add info if you want*/)
}
}
}
In your controller, you conform it to the delegate, and you apply the delegate to each cell in cellForItem: atIndexPath:
class YourControllerThatHasTheCollectionView : UIViewController, CustomCellProtocolDelegate {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "identifier", for: indexPath) as! YourCellClass
cell.delegate = self
cell.indexPath = indexPath
return cell
}
func custom(cell: YourCellClass, hadButton: UIButton, pressedWithInfo : [String:Any]?) {
//Here you can process which button was selected, etc.. and apply your changes to your collectionview
}
}
Best practice is to pass the cell's indexPath parameter in the delegate method inside of pressedWithInfo. It saves you the trouble of calculating which cell actually was pressed; hence why i usually add an indexPath element to each of my UICollectionViewCell subclasses. Better yet, include the index inside the protocol method:
protocol CustomCellProtocolDelegate {
func custom(cell: YourCellClass, hadButton: UIButton, pressedAt: IndexPath, withInfo : [String:Any]?)
}
func triggerButton(sender: UIButton) {
if let d = self.delegate {
d.custom(cell: self, hadButton: sender, pressedAt: indexPath!, withInfo : /*Add info if you want*/)
}
}

How do I play videos inline in a UICollectionView Swift?

A UICollectionView will consist of a feed of videos. When the user is inline with a video, I would like it to play. With my current setup, several videos play at once (I suppose depending on the pagination) once they are loaded in to the collection view.
How do I play videos inline in a UICollectionView?
A cell in the UICollectionView feed will contain a UIView, which will hold the video player. This is the UIView's class PlayerViewClass:
import Foundation
import UIKit
import AVKit
import AVFoundation
class PlayerViewClass: UIView {
override static var layerClass: AnyClass {
return AVPlayerLayer.self
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
}
The Feed's collectionView cellForItemAt indexPath delegate method in the FeedViewController is as follows:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
if let cell = cell as? MyCollectionViewCell {
...
//Configuring the cell
...
//Video player
let avPlayer = AVPlayer(url: post.fullURL)
//Setting cell's player
cell.playerView.playerLayer.player = avPlayer
//TODO: Change so this is only executed when the user is inline.
cell.playerView.player?.play()
}
return cell
}
The cell MyCollectionViewCell has an IBOutlet linked to the playerView UIView:
class MyCollectionViewCell {
#IBOutlet weak var playerView: PlayerViewClass!
override func awakeFromNib() {
super.awakeFromNib()
//Setup, not relevant
}
}
I found the following GitHub repo, which shows the functionality that I would like to implement; however, I'm a little unsure of how to do this with my setup below.
Thanks so much!
You'll need to know which cells are visible.You get that "for free" via the UICollectionView API's visibleCells property. Here's the documentation.
Additionally, you'll need to do some bookkeeping when the UICollectionView scrolls. In reviewing the documentation for UICollectionView, you'll see it is a subclass of UIScrollView. UIScrollView comes with delegate methods that enable you to track this. Here's a "physics for poets" approach to what you could do to accomplish this task:
Let's say this is your view controller:
class YourViewController: UIViewController {
let collectionView = UICollectionView()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
}
}
Then, you'd implement your UICollectionViewDelegate and UICollectionViewDataSource methods here:
extension YourViewController: UICollectionViewDelegate, UICollectionViewDataSource {
// Bare bones implementation
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// TODO: need to implement
return 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// TODO: need to implem,ent
return UICollectionViewCell()
}
}
Finally, you'd implement UIScrollViewDelegate methods to detect beginning of scrolling and end of scrolling. Here's where you'd implement your logic for starting/stopping video:
extension YourViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
collectionView.visibleCells.forEach { cell in
// TODO: write logic to stop the video before it begins scrolling
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
collectionView.visibleCells.forEach { cell in
// TODO: write logic to start the video after it ends scrolling
}
}
}
You'll want to muck around with the timing of stopping/starting animations to see what looks good, so feel free to poke around the various UIScrollViewDelegate methods.
For a play a video in Inline you can refer to the MMPlayerView in gitHub it will be a helpful framework "https://cocoapods.org/pods/MMPlayerView"

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()
}
}

Perform a segue selection from a uicollectionview that is embedded in a tableview cell?

Currently we have a uicollectionview that is embedded in a tableview cell. When the collection view cell is selected it's suppose to initiate a push segue to another view controller. The problem is there is no option to perform the segue on the cell. Is there a way around it? Here is the cell:
class CastCell : UITableViewCell {
var castPhotosArray: [CastData] = []
let extraImageReuseIdentifier = "castCollectCell"
let detailToPeopleSegueIdentifier = "detailToPeopleSegue"
var castID: NSNumber?
#IBOutlet weak var castCollectiontView: UICollectionView!
override func awakeFromNib() {
castCollectiontView.delegate = self
castCollectiontView.dataSource = self
}
}
extension CastCell: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return castPhotosArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = castCollectiontView.dequeueReusableCell(withReuseIdentifier: extraImageReuseIdentifier, for: indexPath) as! CastCollectionViewCell
cell.actorName.text = castPhotosArray[indexPath.row].name
return cell
}
}
extension CastCell: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.castID = castPhotosArray[indexPath.row].id
performSegue(withIdentifier: detailToPeopleSegueIdentifier, sender: self) //Use of unresolved identifier 'performSegue' error
}
}
extension CastCell {
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let peopleVC = segue.destination as! PeopleDetailViewController
peopleVC.id = self.castID
}
}
The problem is there is no option to perform the segue on the cell
There is no such thing as a "segue on a cell". A segue is from one view controller to another. performSegue is a UIViewController method. So you cannot say performSegue from within your CastCell class, because that means self.performSegue, and self is a UITableViewCell — which has no performSegue method.
The solution, therefore, is to get yourself a reference to the view controller that controls this scene, and call performSegue on that.
In a situation like yours, the way I like to get this reference is by walking up the responder chain. Thus:
var r : UIResponder! = self
repeat { r = r.next } while !(r is UIViewController)
(r as! UIViewController).performSegue(
withIdentifier: detailToPeopleSegueIdentifier, sender: self)
1: A clean method is to create a delegate protocol inside your UITableViewCell class and set the UIViewController as the responder.
2: Once UICollectionViewCell gets tapped, handle the taps inside the UITableViewCell and forward the tap to your UIViewController responder through delegatation.
3: Inside your UIViewController, you can act on the tap and perform/push/present whatever you want from there.
You want your UIViewController to know what is happening, and not call push/presents from "invisible" subclasses that should not handle those methods.
This way, you can also use the delegate protocol for future and other methods that you need to forward to your UIViewController if needed, clean and easy.

How to call an outside function from button in cell

I use a main class call newsFeedCointroller as UICollectionViewController.
1. In cell inside I have a newsfeed with a like button (to populate the cell I use a class called "FeedCell")
2. Out from cells (in mainview) I have a label (labelX) used for "splash message" with a function called "messageAnimated"
How can I call the "messageAnimated" function from the button inside the cells.
I want to to change the label text to for example: "you just liked it"...
Thanks for helping me
In your FeedCell you should declare a delegate (Read about delegate pattern here)
protocol FeedCellDelegate {
func didClickButtonLikeInFeedCell(cell: FeedCell)
}
In your cell implementation (suppose that you add target manually)
var delegate: FeedCellDelegate?
override func awakeFromNib() {
self.likeButton.addTarget(self, action: #selector(FeedCell.onClickButtonLike(_:)), forControlEvents: .TouchUpInside)
}
func onClickButtonLike(sender: UIButton) {
self.delegate?.didClickButtonLikeInFeedCell(self)
}
In your View controller
extension FeedViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("feedCell", forIndexPath: indexPath) as! FeedCell
// Do your setup.
// ...
// Then here, set the delegate
cell.delegate = self
return cell
}
// I don't care about other delegate functions, it's up to you.
}
extension FeedViewController: FeedCellDelegate {
func didClickButtonLikeInFeedCell(cell: FeedCell) {
// Do whatever you want to do when click the like button.
let indexPath = collectionView.indexPathForCell(cell)
print("Button like clicked from cell with indexPath \(indexPath)")
messageAnimated()
}
}

Resources