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 !
Related
Even if you click on someone else's profile, only users who are currently logged in are displayed. A few similar questions have been asked, but
I ask because the language and environment are different.
To solve the problem, the displayed ID is printed, but the printed ID is displayed as a different user. So maybe there's a problem with the profile controller, right? Or is there a problem somewhere else?
FeedCellDelegate:
protocol FeedCellDelegate: class {
func cell(_ cell: FeedCell, wantsToShowCommentsFor post: Post)
func cell(_ cell: FeedCell, didLike post: Post)
func cell(_ cell: FeedCell, wantsToShowProfileFor uid: String)
}
ProfileController:
extension ProfileController {
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return posts.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! ProfileCell
cell.viewModel = PostViewModel(post: posts[indexPath.row])
return cell
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerIdentifier, for: indexPath) as! ProfileHeader
header.delegate = self
header.viewModel = ProfileHeaderViewModel(user: user)
return header
}
}
Feed controller:
func cell(_ cell: FeedCell, wantsToShowProfileFor uid: String) {
UserService.fetchUser(withUid: uid) { user in
print("id check \(uid)")
let controller = ProfileController(user: user)
self.navigationController?.pushViewController(controller, animated: true)
}
UserService(firebase)
static func fetchUser(withUid uid: String, completion: #escaping(User) -> Void) {
guard let uid = Auth.auth().currentUser?.uid else { return }
Collection_Users.document(uid).getDocument { snapshot, error in
guard let dictionary = snapshot?.data() else { return }
let user = User(dictionary: dictionary)
completion(user)
}
}
FeedCell:
#objc func showUserProfile() {
guard let viewModel = viewModel else { return }
print("check UID : \(viewModel.post.ownerUid)")
delegate?.cell(self, wantsToShowProfileFor: viewModel.post.ownerUid)
}
In your fetchUser function you always do:
guard let uid = Auth.auth().currentUser?.uid else { return }
Collection_Users.document(uid).getDocument { snapshot, error in
So you always get the user document for Auth.auth().currentUser?.uid, regardless of what else happened in the app.
If you wan to load the document for a different user, you'll have to pass the UID of the user that was clicked on to fetchUser and use that UID in the call to document.
I'm trying to use the library ExpandableCell to add collapsable table view cells to my app. I'm using the latest version of the library which is 1.3.0.
Below is the full code.
import UIKit
import ExpandableCell
class ViewController: UIViewController {
#IBOutlet weak var tableView: ExpandableTableView!
private var passengers = [Passenger]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = UIView(frame: .zero)
tableView.expandableDelegate = self
passengers = [
Passenger(id: 1, name: "Mark", trips: [Trip(id: 1, route: "NY to NJ")]),
Passenger(id: 2, name: "Jesica", trips: [Trip(id: 1, route: "NY to NJ"), Trip(id: 2, route: "LA to LV")]),
Passenger(id: 3, name: "Brian", trips: [Trip(id: 2, route: "Kansas City to Denver")])
]
tableView.reloadData()
}
}
extension ViewController: ExpandableDelegate {
func expandableTableView(_ expandableTableView: ExpandableTableView, numberOfRowsInSection section: Int) -> Int {
return passengers.count
}
func expandableTableView(_ expandableTableView: ExpandableTableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: PassengerCell.reuseIdentifier, for: indexPath) as! PassengerCell
let passenger = passengers[indexPath.row]
cell.textLabel?.text = passenger.name
cell.detailTextLabel?.text = "\(passenger.trips?.count ?? 0) trips"
return cell
}
func expandableTableView(_ expandableTableView: ExpandableTableView, expandedCellsForRowAt indexPath: IndexPath) -> [UITableViewCell]? {
let cell = tableView.dequeueReusableCell(withIdentifier: TripCell.reuseIdentifier, for: indexPath) as! TripCell
let passenger = passengers[indexPath.row]
if let trips = passenger.trips {
var cells = [TripCell]()
for trip in trips {
cell.textLabel?.text = trip.route
cells.append(cell)
}
return cells
} else {
return nil
}
}
func expandableTableView(_ expandableTableView: ExpandableTableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 60
}
func expandableTableView(_ expandableTableView: ExpandableTableView, heightsForExpandedRowAt indexPath: IndexPath) -> [CGFloat]? {
let count = passengers[indexPath.row].trips?.count ?? 0
let heightArray = [CGFloat](repeating: 50, count: count)
return heightArray
}
func expandableTableView(_ expandableTableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
return true
}
}
The data is loaded correctly and the tableview appears as expected. But the problem is when you tap on a collapsed cell. It acts...weird.
Notice how some cells don't appear at all (the second group should show 2 yellow cells). And some cells appear in other groups that they don't belong in. It looks like a cell reuse issue.
I tried overriding the prepareForReuse method and reset the controls manually as well but that didn't work either.
override func prepareForReuse() {
super.prepareForReuse()
textLabel?.text = nil
backgroundColor = nil
}
I saw some similar issues in the library's Github repo but there aren't any answers or fixes.
If anyone has used this library before, any idea what might be causing this issue and how to fix it?
Demo project
Looking at your Demo Project...
In expandedCellsForRowAt in ViewController, you are creating one cell object, then assigning it different text values and appending it to an array.
func expandableTableView(_ expandableTableView: ExpandableTableView, expandedCellsForRowAt indexPath: IndexPath) -> [UITableViewCell]? {
// here, you create a cell object
let cell = tableView.dequeueReusableCell(withIdentifier: TripCell.reuseIdentifier, for: indexPath) as! TripCell
let passenger = passengers[indexPath.row]
if let trips = passenger.trips {
var cells = [TripCell]()
for trip in trips {
// here, you repeatedly set the text of the SAME cell object
cell.textLabel?.text = trip.route
cells.append(cell)
}
return cells
} else {
return nil
}
}
Use this instead:
func expandableTableView(_ expandableTableView: ExpandableTableView, expandedCellsForRowAt indexPath: IndexPath) -> [UITableViewCell]? {
// Don't create the cell here
//let cell = tableView.dequeueReusableCell(withIdentifier: TripCell.reuseIdentifier, for: indexPath) as! TripCell
let passenger = passengers[indexPath.row]
if let trips = passenger.trips {
var cells = [TripCell]()
for trip in trips {
// create a NEW cell for each trip (don't use indexPath)
let cell = tableView.dequeueReusableCell(withIdentifier: TripCell.reuseIdentifier) as! TripCell
cell.textLabel?.text = trip.route
cells.append(cell)
}
return cells
} else {
return nil
}
}
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)
}
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]
}
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)
}