interact with button in UICollectionViewCell through main view controller - ios

I'm making an app that allows a user to "pin" certain elements in a collection view I have implemented in my home ViewController class. To pin an element, the user must access a button that is part of my WordCell (UICollectionViewCell) class. However, when I try to press the button from my home view controller, nothing happens.
Here is all the relevant code and screenshots:
The star on the right hand side is the button inside the CollectionViewCell that I want the user to be able to push through the home view.
Below is all the relevant code in my ViewController class. I am using a delegate to pass the cell that was pressed into my home class ViewController. I also plan on passing more data back and forth between the cell (UICollectionCellView) class and ViewController in the future.
extension ViewController: UICollectionViewDataSource{
//......
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let wordCell = collectionView.dequeueReusableCell(withReuseIdentifier: wordCellReuseID, for: indexPath) as! WordCell
wordCell.delegate = self
wordCell.configure(word: tempWords[indexPath.item])
return wordCell
}
//......
}
extension ViewController: WordCellDelegate{
func star(wasPressedOnCell: WordCell){
print("touched")
if(wasPressedOnCell.isStarred){ //if the button has already been starred, unstar it
wasPressedOnCell.starButton.setImage(UIImage(named: "unfilled_star.png"), for: .normal)
wasPressedOnCell.isStarred = false
}
else{ //else, star the button
wasPressedOnCell.starButton.setImage(UIImage(named: "filled_star.png"), for: .normal)
wasPressedOnCell.isStarred = true
}
}
}
Here is relevant code in my class that conforms to UICollectionCellView:
//delegate setup for home class
protocol WordCellDelegate: class{
func star(wasPressedOnCell cell: WordCell) //parameter: cell that was pressed
}
//........
//button setup
starButton = UIButton()
starButton.setImage(UIImage(named: "unfilled_star.png"), for: .normal)
starButton.translatesAutoresizingMaskIntoConstraints = false
starButton.addTarget(self, action: #selector(starred), for: .touchUpInside)
contentView.addSubview(starButton)
//......
//button objective function
#objc func starred(){
print("touched")
delegate?.star(wasPressedOnCell: self)
//starredTapAction?() //chained back to main view controller
}
However, when I try to press the star on my home view controller screen, the objc function inside my UICollectionCellView class is not called. I've read from previous posts that this is most likely due to a hierarchy of classes and which view controls which objects, but I haven't been able to find a solution to this issue yet. I'm not sure what needs to be changed so the button inside the collection cell can be pressed through the view of the collection.
Please let me know if you need any more information, and thank you for reading this post!

Please disable cell selection of UIcollection view and your button touch event will fire, basically at a time you can either use collection view did select method or button action method

I had the exact same issue less than 2 weeks ago; I am still not sure about the root cause but I know the fix.
Move this line --
starButton.addTarget(self, action: #selector(starred), for: .touchUpInside)
inside your cell configuration method --
WordCell.configure (word: )
Add --
starButton.isEnabled = true
starButton.isUserInteractionEnabled = true
under your --
//button setup
only if the tap is not registering (ie if you can't see the button getting tapped)
It's really about setting the button target under cellForItemAt(); that'll do the trick.

Related

buttons and labels resetting when scrolling through collection view

I have a collection view in which each cell possess the ability to be interacted with by the user. Each cell has a like button and a number of likes label. When the button is pressed, the button should turn cyan, and the label (which holds the number of likes) should increment. This setup currently works. However, when I scroll through the collection view and scroll back, the button reverts to its original color (white) and the label decrements down to its original value. I have heard of an ostensibly helpful method called prepareForReuse(), but perhaps I'm not using it correctly. Here is my code:
Here is the array which holds all the cells
var objects = [LikableObject]()
Here is the class definition for these objects
class LikableObject {
var numOfLikes: Int?
var isLikedByUser: Bool?
init(numOfLikes: Int, isLikedByUser: Bool) {
self.numOfLikes = numOfLikes
self.isLikedByUser = isLikedByUser
}
}
Mind you, there is more functionality present in this object, but they are irrelevant for the purposes of this question. One important thing to be noted is that the data for each cell are grabbed using an API. I'm using Alamofire to make requests to an API that will bring back the information for the numOfLikes and isLikedByUser properties for each cell.
Here is how I load up each cell using the collection view's delegate method:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ObjectCell", for: indexPath) as! ObjectCell
cell.configureCell(
isLikedByUser: objects[indexPath.row].isLikedByUser!,
numOfLikes: objects[indexPath.row].numOfLikes!,
)
return cell
}
The ObjectCell class has these three fields:
var isLikedByUser: Bool?
#IBOutlet weak var numOfLikes: UILabel!
#IBOutlet weak var likeBtn: UIButton!
And that configureCell() method, which belongs to the cell class, is here:
public func configureCell(numOfLikes: Int, isLikedByUser: Bool) {
self.isLikedByUser = isLikedByUser
self.numOfLikes.text = String(numOfLikes)
if isLikedByUser {
self.likeBtn.setFATitleColor(color: UIColor.cyan, forState: .normal)
} else {
self.likeBtn.setFATitleColor(color: UIColor.white, forState: .normal)
}
}
And lastly, the prepareForReuse() method is here:
override func prepareForReuse() {
if isLikedByUser! {
self.likeBtn.setTitleColor(UIColor.cyan, for: .normal)
} else {
self.likeBtn.setTitleColor(UIColor.white, for: .normal)
}
}
This doesn't work. And even if it did, I still don't know a way to keep the numOfLikes label from decrementing, or if it should anyway. I'm speculating that a big part of this problem is that I'm not using the prepareForReuse() method correctly... Any help is appreciated, thank you.
prepareForReuse is not the place to modify the cell, as the name states, you "only" have to prepare it for reuse. if you changed something (for example isHidden property of a view), you just have to change them back to initial state.
What you should do though, you can implement didSet for isLikedByUser inside the cell, and apply your modifications to likeBtn in there. (this is of-course the fast solution)
Long solution: It's an anti-pattern that your cell has a property named isLikedByUser, TableViewCell is a View and in all architectures, Views should be as dumb as they can about business logic. the right way is to apply these modifications in configure-cell method which is implemented in ViewController.
If you feel you'll reuse this cell in different viewControllers a lot, at least defined it by a protocol and talk to your cell through that protocol. This way you'll have a more reusable and maintainable code.
Currently all of this is good , the only missing part is cell reusing , you have to reflect the changes in the number of likes to your model array
class ObjectCell:UICollectionViewCell {
var myObject:LikableObject!
}
In cellForRowAt
cell.myObject = objects[indexPath.row]
Now inside cell custom class you have the object reflect any change to it , sure you can use delegate / callback or any observation technique
The prepareForResuse isn't needed here.
You do need to update the model underlying the tableview. One way to verify this is with mock data that is pre-liked and see if that data displays properly.

Proper way to get indexes of a UITableViewCell in response to a button click which sits in a tableview within tableview?

Screenshot of the app:
I have an 'x' button to delete a TableViewCell which is in a table within a table. On click of the button, I would like to remove the cell, so I need to know the 2 indexes of the button click, the row if the first table view, and then within that tableview the row of the cell which the button was clicked. All I have is the sender.
So to be a bit clearer, in the screenshot, if someone clicks the x under the ford fiesta, I need to get indexpath 0 for the "subtableview" and 1 for the tableview, and that way I know to delete this element from the table datasource.
I do it successfully by doing:
var cell = sender.superview
while (cell != nil) && !((cell?.isKind(of: CustomCell.self))!) {
cell = cell?.superview
}
let tbl = cell?.superview as! UITableView
let indexPath = tbl.indexPath(for: (cell as? UITableViewCell)!
)
The stupid thing is I have to do it twice, once to find the index of the cell within the "sub"tableview, and then again to find the index of the "subtableview" within the tableview.
Is there a better way to do this? Isnt there a way to get the buttonClick to get the didSelectRowAt to fire and add the sender object to it (so I know that a button was clicked as opposed to the cell being selected)?
EDIT I forgot to mention that the first tableview opens and closes on click, so the main tableview has 2 different cell types, one closed (so no nested tableview) and then onselect of a row from that tableview, the cell is replaced with a detailed cell which has another tableview inside it, thats why sectioned tableview isnt a solution (to the best of my knowledge, I'm new here)
One way to do it is to use closures. You set up your cell with a closure and then call it. Pretty much like this:
class CellWithClosure: UITableViewCell {
var button: UIButton = UIButton()
var closureForButton: (Void) -> Void
func setupCell(closureForButton: #escaping (Void) -> Void) {
self.closureForButton = closureForButton
button.addTarget(self, action: #selector(buttonAction), for: UIControlEvents.touchUpInside)
}
#objc func buttonAction() {
closureForButton()
}
}

Tapping Multiple Buttons in Custom UITableViewCell Swift

I have a custom UITableViewCell subclass where I am putting two buttons in the cell, I want both to be clickable, but so far I cannot even get any kind of click to trigger (besides row selection, but I disabled that on my UITableViewController subclass). Basically When one of two buttons is selected, it should remove those two buttons and update what is in the cell with a list of selectable choices (also buttons). I am doing everything programmatically (no IB).
My TableViewCell with Initial Two Buttons
I have looked around a lot and haven't found anything that handles more than ONE button in a tableViewCell. Currently I've been trying to add targets to my buttons in my UITableViewCell's awakeFromNib():
for button in initialChoiceButtons{
button.isUserInteractionEnabled = true
button.addTarget(self, action: #selector(initialChoicePressed(sender:)), for: UIControlEvents.touchUpInside)
}
One thing I've tried is in my tableView in cellForRowAt for my custom cell is to bring my buttons to the front of the cell:
for button in (cell as! FormDropDownTableViewCell).initialChoiceButtons{
cell.bringSubview(toFront: button)
}
I'm really stumped and feel like this should easy. I'm on the verge of just using a stackView inside of scrollview for everything...
Ok so I figured out a somewhat clean way to separate button taps in my tableviewcell, by creating a delegate protocol with a function that I'll call from my target in my tableviewcontroller for every button in my tableviewcell.
protocol UIButtonSelectorDelegate{
func handleTap(button:DelegatingButton) //will select different functions based on DelegatingButton.actionType
}
class DelegatingButton:UIButton{
var selectorDelegate:UIButtonSelectorDelegate?
var actionType:String = "default"
}
in my FormDropDownTableViewCell I conform to the UIButtonSelectedDelegate and implement handleTap like so:
func handleTap(button:DelegatingButton){
switch button.actionType{
case "initialChoiceSelect":
initialChoicePressed(initialChoice:button) //specific method for certain button.actionType also in my FormDropDownTableViewCell
case "cardTypeSelect":
cardTypeSelected(selectedCardType:button)
default:
break
}
}
Now I add the target-actions for every button in cellForRowAt in my tableviewcontroller like so:
button.addTarget(self, action: #selector(handleButtonTaps), for: UIControlEvents.touchUpInside)
and the handleButtonTaps func in the tableviewcontroller is simple:
func handleButtonTaps(sender: DelegatingButton){
sender.selectorDelegate?.handleTap(button: sender)
}
Enjoyed Talking to myself =P ..

How to pass value on UIButton click to delegate inside collectionViewCell?

I am learning iOS programming for last few days, so I am new to this tech. I am building simple app where I'm using collection view which has EventCell inside it. Each EventCell has one UIButton and EventCell has uniq value(ID coming from JOSN API response). I need to pass that value to delegate which call new ViewController. I have setup the delegate method which is working correctly, just finding the solution for how to pass value on button click
PS: I am not using storyboard
**EventCell.swift**
lazy var leaderboardButton: UIButton = {
let leaderboardBtn = UIButton(type: .system)
leaderboardBtn.setTitle("LeaderBoard", for: .normal)
leaderboardBtn.addTarget(self, action: #selector(handleLeaderBoardClick), for: .touchUpInside)
leaderboardBtn.tintColor = .white
return leaderboardBtn
}()
weak var delegate: HomeControllerDelegate?
func handleLeaderBoardClick() {
// need to get uniq value and pass here....
delegate?.clickOnLeaderBoard()
}
If you need to pass that ID value to delegate object which is in your case is your ViewController - then you need to modify clickOnLeaderboard function from the HomeControllerDelegate protocol.
Modify it and pass your EventCell's ID as an additional argument to it.
delegate?.clickOnLeaderBoard(cellID)
Also, don't forget to update function signature in view controller class.
In func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) overload
add this:
cell.yourButton.tag = indexPath.row
cell.yourButton.addTarget(self, action: #selector(buttonClicked(sender:)), for: .touchUpInside)
you should have buttonClicked(sender:) in your UIViewController.
buttonClicked(sender:){
tag = sender.tag
// you can use this tag to change your view controller
}
Change your function to include a parameter:
func handleLeaderBoardClick(_ sender: UIButton) {
// need to get uniq value and pass here....
delegate?.clickOnLeaderBoard()
}
Update the selector:
leaderboardBtn.addTarget(self, action: #selector(handleLeaderBoardClick(_:)), for: .touchUpInside)
Now you can perform a comparison between sender and leaderboardButton to see if they're the same.

Swift 3: UITapGestureRecognizer Not Firing

I'm not sure why but my UITapGestureRecognizer is not firing correctly. Well in fact it is not firing at all. I am trying to get a function to run on the tap of an image.
Info about the setup:
Not using storyboards, loading everything programmatically
The view that I am loading this on also has a UICollectionView on it
ok so for the code:
The UIImageView is declared like so:
let backButtonIcon: UIImageView = {
let bbi = UIImageView()
bbi.image = UIImage(named: "backIcon")
bbi.translatesAutoresizingMaskIntoConstraints = false
bbi.layer.zPosition = 2
return bbi
}()
I then added that to the view:
view.addSubview(backButtonIcon)
to add the tap functionality to the UIImageView I am using:
let tapBackButton = UITapGestureRecognizer(target: self, action: #selector(backButtonPressed))
backButtonIcon.isUserInteractionEnabled = true
backButtonIcon.addGestureRecognizer(tapBackButton)
and lastly, here is the function that I am trying to run, just a simple print at the moment:
func backButtonPressed(sender: UITapGestureRecognizer) {
print("Go Back Pressed")
}
Update:
I have already tried adding:
bbi.isUserInteractionEnabled = true
and also:
backButtonIcon.isUserInteractionEnabled = true
Update 2:
I created another blank view and used the code exactly as it is here and it worked.
Could it be something to do with the UICollectionView?
I made the UICollectionView zPosition -1 and the UIImageView zPosition at 1 but that alas that also did not work.
Image views ignore user events by default. Normally, you use image views only to present visual content in your interface. If you want an image view to handle user interactions as well, change the value of its isUserInteractionEnabled property to true. After doing that, you can attach gesture recognizers or use any other event handling techniques to respond to touch events or other user-initiated events.
source: https://developer.apple.com/reference/uikit/uiimageview
I think the issue is with the UICollectionView, which is composed of UICollectionViewCells that already have a tap enabled by definition.
I'm looking through a very old app (Swift 1.x) I wrote for my personal usage (as in it never saw the light of day) and here's what I seemed to do - you probably need to do some variation of this:
Subclassed UICollectionViewCell and added a UILabel and UIImageView as subviews to it, populating them (along with a NSURL for the tap action).
Declared (and adopted) a specific UIViewController to also be my UICollectionView delegate (also for my purposes it was a SFSafariViewController delegate).
In that VC's viewDidLoad() I did set up a long press (for removing) and double tap (for reloading) gestures, but did not set up a tap gesture - it isn't needed.
In that VC's viewDidLoad() I loaded (and populated) the cells.
Here's the thing I think you are getting stuck on, but without more code I'm not sure:
Coded against:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItemAtIndexPath(indexPath) as? BookmarkCell {
selectedBookmarkSection = indexPath.section
selectedBookmarkRow = indexPath.row
sfc = SFSafariViewController(URL: cell.targetUrl!, entersReaderIfAvailable:true)
sfc.delegate = self
presentViewController(sfc, animated: true, completion: nil)
} else {
// Error indexPath is not on screen: this should never happen.
}
}
Please be kind, as this was something I wrote back in July 2014! (I update the collectionView(didSelectItemAt:indexPath:) for Swift 3.x but that's it.)
TR;DR: I think - particularly if your tap is seen outside of a UICollectionView - that this may get you working.

Resources