UICollectionView shouldShowMenuForItemAt Not Called - ios

I have a stock standard UICollectionView in a UIViewController that is its delegate. However the shouldShowMenuForItemAt func is not called for a long press. I have added a didSelectItemAt func which does get called on clicking a cell to make sure the delegate is indeed wired up correctly.
I also implemented the canPerformAction to return true and performAction in the delegate along with the canPerformAction and canBecomeFirstResponder to return true in my UICollectionViewCell subclass. None of these func's get called for a long press of a cell. Any suggestions?

The missing piece of the puzzle, which most people seem to miss, is that in order for menus to work (in a collection view or table view), the cell must implement the selector.
Here's a minimal example. Instruction: Make a new project using the Single View App template. Copy this code and paste it into ViewController.swift, so as to replace completely everything in that file. Run. Long press on a green square. Enjoy. (The menu item does nothing; the point is, you will see the menu item appear.)
import UIKit
class Cell : UICollectionViewCell {
#objc func f(_ : Any) {}
}
class ViewController: UIViewController {
let cellid = "cellid"
#nonobjc private let howdy = #selector(Cell.f)
override func viewDidLoad() {
super.viewDidLoad()
let cv = UICollectionView(frame: self.view.bounds, collectionViewLayout: UICollectionViewFlowLayout())
self.view.addSubview(cv)
cv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
cv.delegate = self
cv.dataSource = self
cv.register(Cell.self, forCellWithReuseIdentifier: cellid)
}
}
extension ViewController : UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
func collectionView(_ cv: UICollectionView, cellForItemAt ip: IndexPath) -> UICollectionViewCell {
let cell = cv.dequeueReusableCell(withReuseIdentifier: cellid, for: ip)
cell.backgroundColor = .green
return cell
}
func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
let mi = UIMenuItem(title:"Howdy", action:howdy)
UIMenuController.shared.menuItems = [mi]
return true
}
func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
return (action == howdy)
}
func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
}
}

Related

UICollectionView subclass with internal delegate doesn't get triggered when external delegate hasn't implemented the same method

I'm working on a 3rd party framework for swift so i cannot use the delegate methods of UICollectionViewDelegate myself but I do need them for some custom logic.
Tried multiple approaches to make it work, including method swizzling but in the end I felt like it was too hacky for what i'm doing.
Now i'm subclassing UICollectionView and setting the delegate to an internal (my) delegate.
This works well except for when the UIViewController hasn't implemented the method.
right now my code looks like this:
fileprivate class UICollectionViewDelegateInternal: NSObject, UICollectionViewDelegate {
var userDelegate: UICollectionViewDelegate?
override func responds(to aSelector: Selector!) -> Bool {
return super.responds(to: aSelector) || userDelegate?.responds(to: aSelector) == true
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if userDelegate?.responds(to: aSelector) == true {
return userDelegate
}
return super.forwardingTarget(for: aSelector)
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let collection = collectionView as! CustomCollectionView
collection.didEnd(item: indexPath.item)
userDelegate?.collectionView?(collectionView, didEndDisplaying: cell, forItemAt: indexPath)
}
}
class CustomCollectionView: UICollectionView {
private let internalDelegate: UICollectionViewDelegateInternal = UICollectionViewDelegateInternal()
required init?(coder: NSCoder) {
super.init(coder: coder)
super.delegate = internalDelegate
}
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
super.delegate = internalDelegate
}
func didEnd(item: Int) {
print("internal - didEndDisplaying: \(item)")
}
override var delegate: UICollectionViewDelegate? {
get {
return internalDelegate.userDelegate
}
set {
self.internalDelegate.userDelegate = newValue
super.delegate = nil
super.delegate = self.internalDelegate
}
}
}
In the ViewController I just have a simple set up with the delegate method for didEndDisplaying not implemented
Is it possible to listen to didEndDisplaying without the ViewController having it implemented?
Edit 1:
Here's the code of the ViewController to make it a little more clear what i'm doing
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CustomCollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
1000
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = .blue
return cell
}
// func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// print("view controller - did end displaying: \(indexPath.item)")
// }
}
the didEndDisplaying of CustomCollectionView is only triggered when i uncomment the didEndDisplaying method in the ViewController.
what i'm looking for is to have the didEndDisplaying of CustomCollectionView also triggered if the didEndDisplaying method in the ViewController is NOT implemented.
hope it's a little more clear now
Edit 2:
Figured out that the code above had some mistakes which made the reproduction not work as I intended. updated the code above.
also made a github page to make it easier to reproduce here:
https://github.com/mees-vdb/InternalCollectionView-Delegate
I did a little reading-up on this approach, and it seems like it should work - but, obviously, it doesn't.
Played around a little bit, and this might be a solution for you.
I made very few changes to your existing code (grabbed it from GitHub - if you want to add me as a Collaborator I can push a new branch [same DonMag ID on GitHub]).
First, I implemented didSelectItemAt to make it a little easier to debug (only one event at a time).
ViewController class
class ViewController: UIViewController {
#IBOutlet weak var collectionView: CustomCollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
1000
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = .blue
return cell
}
// DonMag - comment / un-comment these methods
// to see the difference
//func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// print("view controller - did end displaying: \(indexPath.item)")
//}
//func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// print("view controller - didSelectItemAt", indexPath)
//}
}
UICollectionViewDelegateInternal class
fileprivate class UICollectionViewDelegateInternal: NSObject, UICollectionViewDelegate {
var userDelegate: UICollectionViewDelegate?
override func responds(to aSelector: Selector!) -> Bool {
return super.responds(to: aSelector) || userDelegate?.responds(to: aSelector) == true
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if userDelegate?.responds(to: aSelector) == true {
return userDelegate
}
return super.forwardingTarget(for: aSelector)
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let collection = collectionView as! CustomCollectionView
collection.didEnd(item: indexPath.item)
userDelegate?.collectionView?(collectionView, didEndDisplaying: cell, forItemAt: indexPath)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let collection = collectionView as! CustomCollectionView
collection.didSel(p: indexPath)
userDelegate?.collectionView?(collectionView, didSelectItemAt: indexPath)
}
}
CustomCollectionView class
// DonMag - conform to UICollectionViewDelegate
class CustomCollectionView: UICollectionView, UICollectionViewDelegate {
private let internalDelegate: UICollectionViewDelegateInternal = UICollectionViewDelegateInternal()
required init?(coder: NSCoder) {
super.init(coder: coder)
super.delegate = internalDelegate
}
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
super.delegate = internalDelegate
}
func didEnd(item: Int) {
print("internal - didEndDisplaying: \(item)")
}
func didSel(p: IndexPath) {
print("internal - didSelectItemAt", p)
}
// DonMag - these will NEVER be called,
// whether or not they're implemented in
// UICollectionViewDelegateInternal and/or ViewController
// but, when implemented here,
// it allows (enables?) them to be called in UICollectionViewDelegateInternal
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
print("CustomCollectionView - didEndDisplaying", indexPath)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("CustomCollectionView - didSelectItemAt", indexPath)
}
override var delegate: UICollectionViewDelegate? {
get {
// DonMag - return self instead of internalDelegate.userDelegate
return self
//return internalDelegate.userDelegate
}
set {
self.internalDelegate.userDelegate = newValue
super.delegate = nil
super.delegate = self.internalDelegate
}
}
}

I am trying to pass a value from a UICollectionView Item to a UIButton in Swift 4

I have a UICollectionViewCell. This Cell is registered in another UICollectionViewCell, which in turn is registered in viewDidLoad of a UIViewController called FilterCollectionViewController. When the first cell is selected it should pass a value of type String to a UIButton. This UIButton is located in FilterCollectionViewController
In the FilterCollectionViewController I made a variable called colorName
var colorName: String!{
didSet {
print(colorName)
}
}
in the same class there is a UIButton.
#objc func addButtonFunc(sender:UIButton){
print(colorName)
}
In FilterCollectionViewController I declare a UICollectionView. Within this collectionView there is a "second" UICollectionView, which has the function:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let filterVC = FilterCollectionViewController()
filterVC.colorName = "blau"
}
When clicking on a cell from the second UICollectionView I can print the colorName value, from the didSet (in the main FilterCollectionViewController). However, I want it to be passed to the UIButton. When this UIButton is clicked it should process this string further. However the print method results in nil
You should not create a new instance of FilterCollectionViewController if you are in the same class, other things look fine,
Create a variable like,
var colorName: String {
didSet {
print(colorName)
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
colorName = "blau"
}
Then in addButton action,
#objc func addButton(sender: UIButton){
print(colorName)
}
First you have to go to storyboard and set identifier "yourViewId" to
FilterCollectionViewController
then
in this method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let viewController = self.storyboard.instantiateViewController(withIdentifier:"yourViewId") as! FilterCollectionViewController
viewController.colorName = "red"
self.present(viewController, sender:nil)
}
I found the answer to the problem.
In the parent FilterCollectionViewController I made a variable colorName like
var colorName: String!
in the second UIVieControllerCell I declared a variable of the Parent ViewController Class
var colorName: FilterCollectionViewController?
Thereby in the second UICollectionView at the function didSelectItemsAtI added the following code
colorName?.colorName = "blau"
This had to be declared again in the first UICollectionview, the parent of the second UICollectionView and a child of FilterCollectionViewController. The declaration was in the function cellForItemAt
cell.colorName = self
And that is it. The color name is updated when clicking the button.
Here is the abbreviated Code:
class FilterCollectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var colorName: String!
override func viewDidLoad() {
super.viewDidLoad()
//...register UICollectionview declare button etc.
}
#objc func addButtonFunc(sender:UIButton){
print(colorName)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellID", for: indexPath) as! FilterCell
cell.colorName = self
return cell
}
And in the FilterCell class:
var colorName: FilterCollectionViewController?
//Class is initialised with function, in the function another UUICollectionView is registered.
//...functions like numberOfItemsInSection and cellForItemAt are declared
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
colorName?.colorName = "blau"
}

UICollectionView with buttons

I am using a UICollectionView and I am thinking how to add buttons for each collection view cell and I don't want all buttons to do the same actions. I need to change buttons actions. I don't know how to do that.
import UIKit
class FirstARViewController: UIViewController , UICollectionViewDelegate , UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var collectionView: UICollectionView!
var imagescv = ["cv1","cv2","cv3","cv4","cv5"]
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imagescv.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! cellimagesar
cell.layer.cornerRadius = 5
cell.layer.borderWidth = 1
cell.myImages.image = UIImage(named: imagescv [indexPath.row])
cell.myImages.contentMode = .scaleToFill
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 182, height: 290)
}
}
Assign the tag and action to the button inside the collectionviewcell.
Assign indexPath.item as button tag is the best approach to identify the particular button from the number of cells.
All you need to do within cellForItemAt delegate method.
cell.btnCounter.tag = indexPath.item
cell.btnCounter.addTarget(self, action: #selector(self.buttonClicked), for: .touchUpInside)
And now you just need to handle the event as below
func buttonClicked(_ sender: UIButton) {
//Here sender.tag will give you the tapped Button index from the cell
//You can identify the button from the tag
}

UIMenuController not showing custom action on UICollectionViewController subclass

I am trying to display a custom action using UIMenuController on a UICollectionViewController subclass, and even though the cut, copy and paste actions appears as expected, for some reason my custom action doesn't.
I followed a lot of references from the web but none of them makes it work, here the code:
class CollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
fileprivate var items = [MyClass]()
// MARK: - UICollectionViewDataSource
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellIdentifier", for: indexPath)
/* update cell properties */
return cell
}
// MARK: - UICollectionViewDelegate
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: itemSize, height: itemSize)
}
override func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
return true
}
override func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
/* Do Something */
}
override func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
return true
}
public func menuAction(_ sender: UIMenuItem) {
/* Action method*/
}
}
Tried to add the menu item as follows:
let menuItem = UIMenuItem(title: SFLocalization.localizedString("Common-remove"), action: #selector(CollectionViewController.menuAction(_:)))
let menuController = UIMenuController.shared
// menuController.menuItems?.append(menuItem)
menuController.menuItems = [menuItem]
on both viewDidLoad and collectionView(_ collectionView:, shouldShowMenuForItemAt) -> Bool
Any ideas?
Omer - check out this link: http://dev.glide.me/2013/05/custom-item-in-uimenucontroller-of.html
Basically, moving these methods:
(BOOL)canPerformAction:(SEL)action withSender:(id)sender {
(BOOL)canBecomeFirstResponder {
... to the CollectionView cell subclass works. You then have to pass this selector back up to the cell delegate.
With this, I was able to get my custom menu to appear!

Swift open new UIViewController

I'm trying to open new UIViewController when I click on an item inside UICollectionView, but using this code my app crashes and Xcode and simulator restarts so I can't even see where is the problem.
import UIKit
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.register(CustomCell.self, forCellWithReuseIdentifier: "cellId")
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! CustomCell
cell.imageViewGame.image = UIImage(named: imageArray[indexPath.row])
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width / 2, height: view.frame.width / 2)
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
performSegue(withIdentifier: "postController", sender: title[indexPath.row])
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "postController" {
let postController = segue.destination as! PostController
postController.title = sender as! String
}
}
}
Does anyone know from this code why my app crashes? If it's important, I'm not using storyboard.
Try this:
Make you don't forgot to declare postController identifier in your segue
You just can present it modally with:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let postController = PostController()
postController.title = title[indexPath.row]
present(postController, animated: true, completion: nil)
}

Resources