UICollectionViewCell reuse causing incorrect UISwitch state - ios

I am having trouble finding a solution for this issue.
I am using UISwitch inside UICollectionViewCell and I am passing a boolean variable to set switch on or off.
The condition is only one switch has to be ON at a time from all cells.
But When I turn one switch on another random switch's tint color changes that means its state changed.
By default switch status is ON in storyboard and even if I set it OFF nothing changes.
I couldn't figure out why is this happening.
Here is my code for cellForItemAtIndexPath
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AddEditItemPopupView.cellId, for: indexPath) as! DiscountCollectionViewCell
cell.delegate = self
let currentDiscount = allDiscounts[indexPath.item]
let shouldApplyDiscount = updatedDiscountId == currentDiscount.id
cell.updateCellWith(data: currentDiscount, applyDiscount: shouldApplyDiscount)
return cell
}
And code for cell class
func updateCellWith(data: DiscountModel, applyDiscount: Bool) {
let name = data.title.replacingOccurrences(of: "Discount ", with: "")
self.titleLabel.text = String(format: "%# (%.2f%%)", name, data.value)
self.switchApply.isOn = applyDiscount
self.switchApply.tag = data.id
}
Data source contains objects of DiscountModel which look like this:
{
id: Int!
title: String!
value: Double!
}
Switch value changed method inside cell class:
#IBAction func switchValueChanged(_ sender: UISwitch) {
if sender.isOn {
self.delegate?.switchValueDidChangeAt(index: sender.tag)
}
else{
self.delegate?.switchValueDidChangeAt(index: 0)
}
}
Delegate method inside view controller class:
func switchValueDidChangeAt(index: Int) {
self.updatedDiscountId = index
self.discountCollectionView.reloadData()
}

There are a few improvements I would suggest to your code;
Reloading the entire collection view is a bit of a shotgun
Since it is possible for there to be no discount applied, you should probably use an optional for your selected discount, rather than "0"
Using Tag is often problematic
I would use something like:
var currentDiscount: DiscountModel? = nil
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AddEditItemPopupView.cellId, for: indexPath) as! DiscountCollectionViewCell
cell.delegate = self
let item = allDiscounts[indexPath.item]
self.configure(cell, forItem: item)
return cell
}
func configure(_ cell: DiscountCollectionViewCell, forItem item: DiscountModel) {
cell.switchApply.isOn = false
let name = item.title.replacingOccurrences(of: "Discount ", with: "")
self.titleLabel.text = String(format: "%# (%.2f%%)", name, item.value)
guard let selectedDiscount = self.currentDiscount else {
return
}
cell.switchApply.isOn = selectedDiscount.id == item.id
}
func switchValueDidChangeIn(cell: DiscountCollectionViewCell, to value: Bool) {
if value {
if let indexPath = collectionView.indexPath(for: cell) {
self.currentDiscount = self.allDiscounts[indexPath.item]
}
} else {
self.currentDiscount = nil
}
for indexPath in collectionView.indexPathsForVisibleItems {
if let cell = collectionView.cellForItem(at: indexPath) {
self.configure(cell, forItem: self.allDiscounts[indexPath.item])
}
}
}
In your cell:
#IBAction func switchValueChanged(_ sender: UISwitch) {
self.delegate?.switchValueDidChangeIn(cell:self, to: sender.isOn)
}

Related

Is it wrong to add action to button in tableViewCell with tag?

I have a UItableViewCell with a button inside it, I set the tag of the button and add the action of the button in my ViewController using the tag.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BillHistoryTableViewCell", for: indexPath) as! BillHistoryTableViewCell
let cellData = billHistories[indexPath.row]
cell.setup(with: cellData)
cell.retryButton.tag = indexPath.row
return cell
}
#IBAction func billHistoryRetryButtonDidTap(_ sender: UIButton) {
let index = sender.tag
if let id = billHistories[index].transactionInfo?.billUniqueID {
hidePayIdGeneralTextField()
billIdTextField.text = id.toNormalNumber()
inquiryGeneralBillRequest()
}
}
I want to know is it wrong for any reason? someone told me it is not good because it uses lots of memory to use tags.
Will it work? yes, but as mentioned above, this is not the best approach, I'd avoid using tags unless this is just for some POC. There are better approaches to handle it.
The first I'd suggest is using delegation to inform back to the controller, here's an example:
class BillHistoryTableViewController {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BillHistoryTableViewCell", for: indexPath) as! BillHistoryTableViewCell
let cellData = billHistories[indexPath.row]
cell.setup(with: cellData)
cell.index = indexPath.row
cell.delegate = self
return cell
}
}
extension BillHistoryTableViewController: BillHistoryTableViewCellDelegate {
func didTapButton(index: Int) {
print("tapped cell with index:\(index)")
if let id = billHistories[index].transactionInfo?.billUniqueID {
hidePayIdGeneralTextField()
billIdTextField.text = id.toNormalNumber()
inquiryGeneralBillRequest()
}
}
}
protocol BillHistoryTableViewCellDelegate: AnyObject {
func didTapButton(index: Int)
}
class BillHistoryTableViewCell: UITableViewCell {
weak var delegate: BillHistoryTableViewCellDelegate?
var cellData: CellData?
var index: Int?
func setup(with cellData: CellData) {
self.cellData = cellData
}
#IBAction func buttonPressed(_ sender: UIButton) {
guard let index = index else {
return
}
delegate?.didTapButton(index: index)
}
}
Another approach that I prefer lately is using Combine's PassThroughSubject, it requires less wiring and delegate definitions.
import Combine
class BillHistoryTableViewController {
var cancellable: AnyCancellable?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BillHistoryTableViewCell", for: indexPath) as! BillHistoryTableViewCell
let cellData = billHistories[indexPath.row]
cell.setup(with: cellData)
cell.index = indexPath.row
cancellable = cell.tappedButtonSubject.sink { [weak self] index in
guard let self = self else { return }
print("tapped cell with index:\(index)")
if let id = self.billHistories[index].transactionInfo?.billUniqueID {
self.hidePayIdGeneralTextField()
self.billIdTextField.text = id.toNormalNumber()
self.inquiryGeneralBillRequest()
}
}
return cell
}
}
class BillHistoryTableViewCell: UITableViewCell {
var tappedButtonSubject = PassthroughSubject<Int, Never>()
var cellData: CellData?
var index: Int?
func setup(with cellData: CellData) {
self.cellData = cellData
}
#IBAction func buttonPressed(_ sender: UIButton) {
guard let index = index else {
return
}
tappedButtonSubject.send(index)
}
}
You can make it even shorter by injecting the index with the cellData, e.g:
func setup(with cellData: CellData, index: Int) {
self.cellData = cellData
self.index = index
}
but from what I see in your example, you don't even need the index, you just need the CellData, so if we'll take the Combine examples these are the main small changes you'll have to make:
var tappedButtonSubject = PassthroughSubject<CellData, Never>()
tappedButtonSubject.send(cellData)
and observing it by:
cancellable = cell.tappedButtonSubject.sink { [weak self] cellData in
if let id = cellData.transactionInfo?.billUniqueID {
//
}
}

Sending a signal to collection view within tableview cell from a segment outlet in another tableview cell

I have a segment outlet in a tableview cell in a VC. There are two indexes: 1 and 2.
When I click on 2, I want to tell the collection view within another tableviewcell to reload another view.
And when I click back to 1, I want the same collection view to reload again and display the original content.
Here are my View Controller Functions:
class MyProfileTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource,segment
{
//Variable selection to determine what is selected - 1 by default
var viewSelected = "1"
//Segment Function - viewSelected is used to tell VC what index it's on
func segmentSelected(tag: Int, type: String) {
if type == "1" {
print("1")
viewSelected = "1"
} else if type == "2" {
print("2")
viewSelected = "2"
}
}
//Cell For Row - tells tableviewcell to look at viewSelected
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = AboutTableView.dequeueReusableCell(withIdentifier: "ProfileSegmentTableViewCell", for: indexPath) as! ProfileSegmentTableViewCell
cell.segmentCell = self
return cell
} else {
let cell = AboutTableView.dequeueReusableCell(withIdentifier: "1_2Cell", for: indexPath) as! 1_2Cell
cell.viewSelected = viewSelected
return cell
}
Here is the Segment Control TableviewCell
//protocol used to delegate
protocol segment: UIViewController {
func segmentSelected(tag: Int, type: String)
}
class ProfileSegmentTableViewCell: UITableViewCell {
#IBOutlet weak var profileSegmentControl: UISegmentedControl!
var segmentCell: segment?
#IBAction func segmentPressed(_ sender: Any) {
profileSegmentControl.changeUnderlinePosition()
let Index = self.profileSegmentControl.selectedSegmentIndex
if Index == 0
{
segmentCell?.segmentSelected(tag: (sender as AnyObject).tag, type: "1")
)
} else {
segmentCell?.segmentSelected(tag: (sender as AnyObject).tag, type: "2")
}
}
CollectionView
//variable by default
var viewSelected = "1"
//viewDidLoad
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
cView.delegate = self
cView.dataSource = self
get {
self.cView.reloadData()
self.cView.layoutIfNeeded()
}
}
func get(_ completionHandler: #escaping () -> Void) {
getCount.removeAll()
if viewSelected = "1" {
print("1") } else {
print("2)
}
completionHandler()
}
Here's a very simple example of using a closure so your segmented-control cell can communicate with your table view controller.
Your cell class might look like this:
class ProfileSegmentTableViewCell: UITableViewCell {
#IBOutlet var profileSegmentControl: UISegmentedControl!
var callback: ((Int)->())?
#IBAction func segmentPressed(_ sender: Any) {
guard let segControl = sender as? UISegmentedControl else { return }
// tell the controller that the selected segment changed
callback?(segControl.selectedSegmentIndex)
}
}
When the user changes the selected segment, the cell uses the callback closure to inform the controller that a segment was selected.
Then, in your controller, you could have a var to track the currently selected segment index:
// track selected segment index
var currentIndex: Int = 0
and your cellForRowAt code would look like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
// first row - use cell with segemented control
let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileSegmentTableViewCell", for: indexPath) as! ProfileSegmentTableViewCell
// set the segemented control's selected index
cell.profileSegmentControl.selectedSegmentIndex = self.currentIndex
// set the callback closure
cell.callback = { [weak self] idx in
guard let self = self else {
return
}
// update the segment index tracker
self.currentIndex = idx
// reload row containing collection view
self.tableView.reloadRows(at: [IndexPath(row: 1, section: 0)], with: .automatic)
}
return cell
} else if indexPath.row == 1 {
// second row - use cell with collection view
let cell = tableView.dequeueReusableCell(withIdentifier: "1_2Cell", for: indexPath) as! My_1_2Cell
// tell the cell which segment index is selected
cell.setData(currentIndex)
return cell
}
// all other rows - use simple Basic cell
let cell = tableView.dequeueReusableCell(withIdentifier: "PlainCell", for: indexPath) as! PlainCell
cell.textLabel?.text = "Row \(indexPath.row)"
return cell
}
Here is a complete example you can run and examine: https://github.com/DonMag/ClosureExample
You can use NotificationCenter.default.addObserver... method and NotificationCenter.default.post..... Read about them. And don't forget to remove observers in deinit

Accessing indexPath in a collectionview

I have a collectionview cell which has an image on it and a button below it. Now when I click on this button, I want to load a tableviewCell which has on it the image from the collectionview. To achieve this, I did this initially..
func SellBtnTapped(_ sender: UIButton) {
let indexPath = collectionView?.indexPath(for: ((sender.superview?.superview) as! RecipeCollectionViewCell))
self.photoThumbnail.image = self.arrayOfURLImages[(indexPath?.row)!]
and photoThumbnail is defined like so...var photoThumbnail: UIImageView! But doing this gives a crash telling 'Unexpectedly found nil while unwrapping an optional value' So I tried this..
let point = sender.convert(CGPoint.zero, to: self.collectionView)
let myIndexPath = self.collectionView.indexPathForItem(at: point)
self.photoThumbnail.image = self.arrayOfURLImages[(myIndexPath?.row)!]
But again, the same crash of Unexpectedly found nil.... is happening. Any idea as to what could be the issue..?
EDIT:
This is the code for cellForItemAtIndex...
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath as IndexPath) as! RecipeCollectionViewCell
cell.sellButton.tag = indexPath.item
cell.sellButton.addTarget(self,action: #selector(SellBtnTapped(_:)),for: .touchUpInside)
return cell
}
It's because you alway get nil your indexPath.
Another approach is
in collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath method
set tag of your cell's button like
cell.myButton.tag = indexPath.item
And in SellBtnTapped method use below code for get indexPath
let indexPath = NSIndexPath(item: sender.tag, section: 0) // set section as you want
let cell = collectionView.cellForItem(at: indexPath as NSIndexPath) as! RecipeCollectionViewCell
Now by use of cell you can get image object that is on it or use self.arrayOfURLImages to get right image. and do your further stuff.
I prefer avoiding tags altogether. I wrote this a while ago and still find it useful.
extension UIView {
var superCollectionViewCell: UICollectionViewCell? {
if let cell = self as? UICollectionViewCell {
return cell
} else {
return superview?.superCollectionViewCell
}
}
var superCollectionView: UICollectionView? {
if let collectionView = self as? UICollectionView {
return collectionView
} else {
return superview?.superCollectionView
}
}
var indexPathOfSuperCollectionViewCell: IndexPath? {
guard let cell = superCollectionViewCell, let collectionView = superCollectionView else { return nil }
return collectionView.indexPath(for: cell)
}
}
This turns your action into
func SellBtnTapped(_ sender: UIButton) {
guard let indexPath = sender.indexPathOfSuperCollectionViewCell else {
print("button has no index path")
return
}
self.photoThumbnail.image = self.arrayOfURLImages[indexPath.row]
}

Get index of clicked UICollectionViewCell in UICollectionView Swift

How do I get the index of the "Sheep" I clicked on in a CollectionView made in Xcode with Swift for iOS?
class SheepsOverviewVC:
UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "class", for: indexPath) as! ClassesCollectionCell
if(sheeps.count > 0) {
cell.ClassImageView.image = UIImage(named: sheeps[indexPath.row] as! String)
cell.SheepName.text = names[indexPath.row] as? String
}
return cell
}
I created a Sent Event on the TouchDown via the Gui:
#IBAction func clickingSheep(_ sender: UIButton) {
print("This will show info about the Sheep")
print(sender)
}
But the response I get is from the second print:
<UIButton: 0x7f9a63021d20; frame = (50 50; 136 169); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x60800003d260>>
Probably there is some way to figure out which Sheep was clicked, but how do I get that information?
This is how it looks like (other namings then provided in the post):
One solution is to get the index path of the cell based on the button's location.
#IBAction func clickingSheep(_ sender: UIButton) {
let hitPoint = sender.convert(CGPoint.zero, to: collectionView)
if let indexPath = collectionView.indexPathForItem(at: hitPoint) {
// use indexPath to get needed data
}
}
You can set and check the button property "tag" (if you have the outlet set to the controller)
Here is another easy solution:
Have a new property for the callback.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "class", for: indexPath) as! ClassesCollectionCell
if(sheeps.count > 0) {
cell.ClassImageView.image = UIImage(named: sheeps[indexPath.row] as! String)
cell.SheepName.text = names[indexPath.row] as? String
}
cell.callBack = { [weak self] collectionViewCell in
let indexPath = collectionView.indexPath(for: collectionViewCell)
self?.doStuffFor(indexPath)
}
return cell
}
and on the cell you can have the ibaction
cell class
//...
var callBack : ((UICollectionViewCell?)->Void)?
//...
#IBAction func action(_ sender: UIButton) {
self.callBack?(self)
}

Reload Collection View in a Collection View Cell through delegation

I have a controller (A) with a Collection View that features 2 cell classes. One of them (B) contains another Collection View. After doing some research, I still cannot figure out how to update the cells in (B) from (A) or elsewhere to get what I want.
Issues
(B) does not reload properly when its button is pressed: the cell with whom the button was tied is still visible even though it is deleted from the userFriendRequests array in (A) in its delegate method. As a bonus I get a crash when I scroll to a new cell in (B) stating that "index is out of range" on the line cell.user = userFriendRequests[indexPath.row].
What I Have
Controller (A)
protocol UserFriendRequestsDelegate: class {
func didPressConfirmFriendButton(_ friendId: String?)
}
/...
fileprivate var userFriendRequests = [User]()
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if userFriendRequests.isEmpty == false {
switch indexPath.section {
case 0:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: friendRequestCellId, for: indexPath) as! UserFriendRequests
cell.userFriendRequests = userFriendRequests
cell.delegate = self
return cell
case 1:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! UserFriendCell
let user = users[indexPath.row]
cell.user = user
return cell
default:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! UserFriendCell
return cell
}
}
/...
extension AddFriendsController: UserFriendRequestsDelegate {
internal func didPressConfirmFriendButton(_ friendId: String?) {
guard let uid = FIRAuth.auth()?.currentUser?.uid, let friendId = friendId else {
return
}
let userRef = FIRDatabase.database().reference().child("users_friends").child(uid).child(friendId)
let friendRef = FIRDatabase.database().reference().child("users_friends").child(friendId).child(uid)
let value = ["status": "friend"]
userRef.updateChildValues(value) { (error, ref) in
if error != nil {
return
}
friendRef.updateChildValues(value, withCompletionBlock: { (error, ref) in
if error != nil {
return
}
self.setUpRequestsStatusesToConfirmed(uid, friendId: friendId)
DispatchQueue.main.async(execute: {
let index = self.currentUserFriendRequests.index(of: friendId)
self.currentUserFriendRequests.remove(at: index!)
for user in self.userFriendRequests {
if user.id == friendId {
self.userFriendRequests.remove(at: self.userFriendRequests.index(of: user)!)
}
}
self.attemptReloadOfCollectionView()
})
})
}
}
PS: self.attemptReloadOfCollectionView() is a func that simply invalidates a timer, sets it to 0.1 sec and then calls reloadData() on (A)'s Collection View.
CollectionViewCell (B)
weak var delegate: UserFriendRequestsDelegate?
var userFriendRequests = [User]()
/...
#objc fileprivate func confirmFriendButtonPressed(_ sender: UIButton) {
delegate?.didPressConfirmFriendButton(friendId)
}
/...
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return userFriendRequests.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: friendRequestCellId, for: indexPath) as! FriendRequestCell
cell.user = userFriendRequests[indexPath.row]
return cell
}
/...
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let firstName = userFriendRequests[indexPath.row].first_name, let lastName = userFriendRequests[indexPath.row].last_name, let id = userFriendRequests[indexPath.row].id else {
return
}
nameLabel.text = firstName + " " + lastName
friendId = id
confirmButton.addTarget(self, action: #selector(confirmFriendButtonPressed(_:)), for: .touchUpInside)
}
What I want to achieve
Update (B) when a User is removed from the userFriendRequests array in (A), this User being identified by his id passed by (B) through delegation.
Any good soul that might have an idea on how to tackle this issue ?
Thanks in advance for your help !

Resources