I'm currently working on an app that has a table-view-like collection view and some other view controllers. Basically, my question is how can I update the indexPath of each cell when one of the collection view cells is deleted.
I attached my view controller file below, but here is what's going on on the app.
When a user opens the table-view-like collection view (in EventCollectionVC), it reloads the data from a database and presents them on the collection view. I also added the code to the navigation bar button item that the user can change the collection view to the edit mode. While in the edit mode, a small ellipsis.circle (SF symbols) is displayed on the collection view cell. When a user taps the ellipsis.circle icon, it displays a new view controller (ModalVC) and lets the user select either delete or edit the cell. When the user selects delete, it shows an alert to delete the cell and delete the cell information with modal dismiss (which means the ModalVC is closed and the MyCollectionVC is displayed now).
Since I have to make the two view controllers (like getting cell information from EventCollectionVC and present in ModalVC) talk to each other, I need to use the indexPath.row to get the information of the cell. Before deleting the cells, the numbers of indexPath.row in the collection view is like
[0,1,2,3,4,5]
But, for example, after I delete the second (indexPath.row = 1) cell and when I try to delete another item, the indexPath becomes
[0,2,3,4,5]
and I can see the collection view's index is not refreshed.
So my question is how can I update/refresh the cell's indexPath.row value after I delete a cell from the collection view?
This is the code with some explanations.
import UIKit
class EvnetCollectionViewController: UIViewController {
var EventDataSource: EventDataSource! // <- this is a class for the Model, and it stores array or Events
let ListView = ListView() // view file
var collectionViewDataSource: UICollectionViewDiffableDataSource<Section, Event>?
var targetEventIndex = Int() // variable to store the index of the event when event cell is tapped
override func loadView() {
view = ListView
}
override func viewDidLoad() {
super.viewDidLoad()
configureNavItem()
setupCollectionView()
displayEvents()
}
func configureNavItem() {
navigationItem.rightBarButtonItem = editButtonItem
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
if (editing){
ListView.collectionView.isEditing = true
} else {
ListView.collectionView.isEditing = false
}
}
func setupCollectionView() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Event> { cell, indexPath, Event in
var content = UIListContentConfiguration.cell()
content.text = Event.title
cell.contentConfiguration = content
let moreAction = UIAction(image: UIImage(systemName: "ellipsis.circle"),
handler: { _ in
let vc = EventActionModalViewController(); // when the user select the cell in edit mode, it displays action modal VC and then come back to this VC with dismiss later
vc.modalPresentationStyle = .overCurrentContext
self.targetEventIndex = indexPath.row // I need targetEvemtIndex when user selects delete event in EventActionModalVC, so just sotre value in here
})
let moreActionButton = UIButton(primaryAction: moreAction)
let moreActionAccessory = UICellAccessory.CustomViewConfiguration(
customView: moreActionButton,
placement: .trailing(displayed: .whenEditing, at: { _ in return 0 })
)
cell.accessories = [
.disclosureIndicator(displayed: .whenNotEditing),
.customView(configuration: moreActionAccessory)
]
}
collectionViewDataSource = UICollectionViewDiffableDataSource<Section, Event>(collectionView: ListView.collectionView) {
collectionView, indexPath, Event in
collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: Event)
}
}
func displayEvents() {
EventDataSource = EventDataSource()
EventDataSource.loadData() // get Events in db and sore in an array Events
populate(with: EventDataSource.Events)
}
func populate(with Events: [Event]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Event>()
snapshot.appendSections([.List])
snapshot.appendItems(Events)
collectionViewDataSource?.apply(snapshot)
}
func showDeleteAlert() {
let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { _ in
self.EventDataSource.delete(at: targetEventIndex)
self.refreshList()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
self.showAlert(title: "Delete", message: nil, actions: [deleteAction, cancelAction], style: .actionSheet, completion: nil)
}
func refreshList() {
EventDataSource.loadData()
setupCollectionView() // since I write this code, it updates all of the indexPath in the collection view, but after deleting one item, the whole collection view is deleted and new collection view is reappeared.
populate(with: EventDataSource.Events)
}
}
I kinda know why this is happening. I only configure cell (in let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Event>...) once, so it won't update the cell information as well as its index path until I configure it again. But if I call setupCollectionView every after deleting one item, the whole collection view disappears and shows up again. Is it possible to reload the collection view list and updates its information without reloading the entire collection view?
Without writing setupCollectionView() in refreshList, the cell's indexPath is not refreshed and I get an error after I delete one cell and try to delete another one. So, I was wondering if there is a way to avoid recreating the whole collection view but update cells' indexPath when the user delete one of the cell in collection view.
I fixed the code in the refresh list function.
func refreshList() {
self.EventDataSource.loadData()
self.populate(with: self.EventDataSource.Events)
ListView.collectionView.reloadData()
}
I just needed to call reloadData after I populate all the data...
Related
I have a tableview which shows a custom cell.
Inside the cell is a button.
Once the button is clicked, a network call is made and the tableview should reload.
I tried this, but I get Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value at vc.reloadData().
#IBAction func acceptRequestPressed(_ sender: UIButton) {
DatabaseManager.shared.AnswerFriendRequest(emailFriend: friendNameLabel.text!, answer: true) { success in
if success {
let vc = FriendRequestViewController()
vc.reloadData()
}else {
print ("error at answer")
}
}
}
The problem is this line:
let vc = FriendRequestViewController()
After that, vc is the wrong view controller — it is just some view controller living off emptily in thoughtspace, whereas the view controller you want is the already existing view controller that's already in the view controller hierarchy (the "interface").
Few pointers:
You can have a closure upon the data call completion in your class that has the table view.
Eg:
// Custom Table Cell Class
class CustomCellClass: UITableViewCell {
let button = UIButton()
// All other UI set up
// Create a closure that your tableView class can use to set the logic for reloading the tableView
public let buttonCompletion: () -> ()
#IBAction func acceptRequestPressed(_ sender: UIButton) {
DatabaseManager.shared.AnswerFriendRequest(emailFriend: friendNameLabel.text!, answer: true) { [weak self] success in
if success {
// Whatever logic is provided in cellForRow will be executed here. That is, tableView.reloadData()
self?.buttonCompletion()
}else {
print ("error at answer")
}
}
}
}
// Class that has tableView:
class SomeViewController: UIViewController {
private let tableView = UITableView()
.
.
// All other set up, delegate, data source
func cellForRow(..) {
let cell = tableView.dequeueTableViewCell(for: "Cell") as! CustomCellClass
cell.buttonCompletion = {[weak self] in
tableView.reloadData()
}
}
I've been trying to figure out a problem in my code but no luck finding a solution.
I have a dropdown library which I'm using for dropdown the library is ios Dropdown
1: https://github.com/AssistoLab/DropDown here in this I've customised the the tableViewCell and I've added a radio button. But when I'm trying to reload it its not updating its state it still remains the same even after setting the state.
func updateCellBy(text: String, index: Int, isSelected: Bool) {
self.index = index
radioButton.isSelected = isSelected
titleLabel.text = text
checkboxButtonWidthConstraint.constant = 20
radioButtonLeadingConstraint.constant = 15
}
But when I scroll this dropdown/tableview dropdown buttons state gets updated. I'm still wodering whats wrong with this. Tested if the data coming in the above method is correct or not but that is correct.DroppDown image
Note this dropdown is inside a collectionViewCell which is indeed inside a view.
dropDown.cellNib = UINib(nibName: "TableViewCell", bundle: nil)
dropDown.customCellConfiguration = { [self] (index: Index, item: String, cell: DropDownCell) -> Void in
guard let cell = cell as? TableViewCell else { return }
cell.updateCellBy(text: DropdownArrays.shared.getData()[index], index: index, isSelected: self.cellIndexes.contains(index))
cell.delegate = self
}
dropDown.dataSource = DropdownArrays.shared.getData()
above is how I'm configuring that drop down. Its just a tableview with cells.
for reloadind the drop down I'm calling the below method
dropDown.reloadAllComponents()
which is indeed reloading the cell inside the DropDown library
Reloads all the cells.
It should not be necessary in most cases because each change to
`dataSource`, `textColor`, `textFont`, `selectionBackgroundColor`
and `cellConfiguration` implicitly calls `reloadAllComponents()`.
*/
public func reloadAllComponents() {
DispatchQueue.executeOnMainThread {
self.tableView.reloadData()
self.setNeedsUpdateConstraints()
}
}
but the cell data is not updating.
For my CollectionView I have this animation inside willDisplay :
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// Add animations here
let animation = AnimationFactory.makeMoveUpWithFade(rowHeight: cell.frame.height, duration: 0.5, delayFactor: 0.1)
let animator = Animator(animation: animation)
animator.animate(cell: cell, at: indexPath, in: collectionView)
}
This is how the animation works (I implemented it for CollectionView) if you need it for more info.
Probelm:
Inside my project the user can create and delete an item.
Right now the collectionView is not animating after deleting even though I am calling reloadData:
extension MainViewController: DismissWishlistDelegate {
func dismissWishlistVC(dataArray: [Wishlist], dropDownArray: [DropDownOption]) {
self.dataSourceArray = dataArray
self.dropOptions = dropDownArray
self.makeWishView.dropDownButton.dropView.tableView.reloadData()
// reload the collection view
theCollectionView.reloadData()
theCollectionView.performBatchUpdates(nil, completion: nil)
}
}
This is where I call the delegate inside my other ViewController:
func deleteTapped(){
let alertcontroller = UIAlertController(title: "Wishlist löschen", message: "Sicher, dass du diese Wishlist löschen möchtest?", preferredStyle: .alert)
let deleteAction = UIAlertAction(title: "Löschen", style: .default) { (alert) in
DataHandler.deleteWishlist(self.wishList.index)
self.dataSourceArray.remove(at: self.currentWishListIDX)
self.dropOptions.remove(at: self.currentWishListIDX)
// change heroID so wishlist image doesnt animate
self.wishlistImage.heroID = "delete"
self.dismiss(animated: true, completion: nil)
// update datasource array in MainVC
self.dismissWishlistDelegate?.dismissWishlistVC(dataArray: self.dataSourceArray, dropDownArray: self.dropOptions)
}
let cancelAction = UIAlertAction(title: "Abbrechen", style: .default) { (alert) in
print("abbrechen")
}
alertcontroller.addAction(cancelAction)
alertcontroller.addAction(deleteAction)
self.present(alertcontroller, animated: true)
}
When creating the animation works just fine. This is how my createDelegateFunction looks like this:
func createListTappedDelegate(listImage: UIImage, listImageIndex: Int, listName: String) {
// append created list to data source array
var textColor = UIColor.white
if Constants.Wishlist.darkTextColorIndexes.contains(listImageIndex) {
textColor = UIColor.darkGray
}
let newIndex = self.dataSourceArray.last!.index + 1
self.dataSourceArray.append(Wishlist(name: listName, image: listImage, wishData: [Wish](), color: Constants.Wishlist.customColors[listImageIndex], textColor: textColor, index: newIndex))
// append created list to drop down options
self.dropOptions.append(DropDownOption(name: listName, image: listImage))
// reload the collection view
theCollectionView.reloadData()
theCollectionView.performBatchUpdates(nil, completion: {
(result) in
// scroll to make newly added row visible (if needed)
let i = self.theCollectionView.numberOfItems(inSection: 0) - 1
let idx = IndexPath(item: i, section: 0)
self.theCollectionView.scrollToItem(at: idx, at: .bottom, animated: true)
})
}
Animation of IUCollectionView elements insertion and deletion is correctly done using finalLayoutAttributesForDisappearingItem(at:) and initialLayoutAttributesForAppearingItem(at:)
Little excerpt from Apple's documentation on finalLayoutAttributesForDisappearingItem(at:)
This method is called after the prepare(forCollectionViewUpdates:) method and before the finalizeCollectionViewUpdates() method for any items that are about to be deleted. Your implementation should return the layout information that describes the final position and state of the item. The collection view uses this information as the end point for any animations. (The starting point of the animation is the item’s current location.) If you return nil, the layout object uses the same attributes for both the start and end points of the animation.
I solved the problem by removing performBatchUpdates ... I have no idea why it works now and what exactly performBatchUpdates does but it works so if anyone wants to explain it to me, feel free :D
The final function looks like this:
func dismissWishlistVC(dataArray: [Wishlist], dropDownArray: [DropDownOption], shouldDeleteWithAnimation: Bool, indexToDelete: Int) {
if shouldDeleteWithAnimation {
self.shouldAnimateCells = true
self.dataSourceArray.remove(at: self.currentWishListIDX)
self.dropOptions.remove(at: self.currentWishListIDX)
// reload the collection view
theCollectionView.reloadData()
} else {
self.shouldAnimateCells = false
self.dataSourceArray = dataArray
self.dropOptions = dropDownArray
// reload the collection view
theCollectionView.reloadData()
theCollectionView.performBatchUpdates(nil, completion: nil)
}
self.makeWishView.dropDownButton.dropView.tableView.reloadData()
}
I have simple chat created using UITableView. I want to add the ability to highlight a message after long press. In fact, I want to create the same feature like iMessage:
After a long press, we unhighlight background (more darker), highlight message, scroll to this message and show the actionSheet
For now I managed to add only longPress and actionSheet
Long press recongizer on viewDidLoad:
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(onCellLongPressed(gesture:)))
messagesTableView.addGestureRecognizer(longPressRecognizer)
onCellLongPressed function:
#objc func onCellLongPressed(gesture: UILongPressGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.began {
let touchPoint = gesture.location(in: self.messagesTableView)
if let indexPath = messagesTableView.indexPathForRow(at: touchPoint) {
self.messagesTableView.selectRow(at: indexPath, animated: true, scrollPosition: UITableViewScrollPosition.none)
shareWithFriend()
}
}
}
#objc func shareWithFriend() {
alert(style: .actionSheet, actions: [
UIAlertAction(title: "Share with friend", style: .default, handler: { [weak self] (_) in
print("SHARE HERE")
}),
UIAlertAction(title: "Cancel", style: .destructive),
])
}
func alert(_ title: String? = nil, message: String? = nil, style: UIAlertController.Style, actions: [UIAlertAction]) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: style)
actions.forEach(alertController.addAction)
present(alertController, animated: true)
}
As you can see, the background color is above the navigation bar so I guess there is a secondary view controller astutely presented above the collection view when the user selects a cell.
I think this is two different view hierarchies which look like one:
One view controller contains the balloon list
One view controller contains a copy of the selected balloon and the few actions associated to it
Here is the road map :
Detect a long press in the collection view cells
Copy the selected balloon and present it inside a secondary view controller (ActionVC)
Adjust the position of the selected balloon inside the ActionVC. If the selected balloon is under the future action button, it should be moved. If the selected balloon does not bother anyone, it should be presented without any change.
Adjust the content offset of the collection view to reflect 3. The modification should be done alongside 3 to look like if the cell was actually moved.
Detect any touch on the ActionVC
Dismiss the ActionVC
Here is an example project.
To copy the balloon, I actually create a view of the same class as the one used in the collection view cell. But you could use a snapshot.
To adjust the selected balloon position inside the ActionVC, I used constraints with priorities. One claims "don't be under the action button", another one claims "be exactly where the cell was". I used a simple coordinates conversion to compute the expected selected balloon position inside the ActionVC.
To perform 3 alongside 4, I used the transitionCoordinator of the ActionVC, but you could use a simple animation block and present the ActionVC without animation.
I'm sorry if this answer will not fulfill your request completely, but hope it will help you.
Your initial code was right, but you have not set Scrolling type. So I suggest you use this method selectRow(at:animated:scrollPosition:) Just set scroll position:
self.messagesTableView.selectRow(at: indexPath, animated: true, scrollPosition: UITableViewScrollPosition.top)
This also will select this row and hence call the following method tableView(_:didSelectRowAt:). So you will need to implement highlighting logic here:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// your logic of changing color
}
Then you should implement similar action, but to deselect the row using deselectRow(at:animated:)I believe this should be done when user finished his action. Also you need to implement similar function tableView(_:didDeselectRowAt:) to return color back:
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
// change color back
}
Update:
You may also use the setHighlighted(_:animated:) method to highlight a cell. In this way you may avoid using selectRowAt/didSelectRowAt/didDeselectRowAt and scroll tableView using
tableView?.scrollToRow(at: indexPath, at: UITableViewScrollPosition.top, animated: true).
I am showing visited person list in Collectionview is working fine. I can reschedule visitor date and time I added custom button(reschedule)once click reschedule button after I'm showing viewcontroller(custompopupview) with date picker when user rescheduled after click ok. I'm navigated to Collectionview, but tableview data is not updated. I tried many way I did not find solution. After confirm the reschedule Collectionview update the new data....
This is Collectionview of visitors:
Reschedule customviewcontoller(popupviewcontroller):
Collectionview code:
class MyvistorListViewController:NANavigationViewController
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NAString().cellID(), for: indexPath) as! MyvistorListListCollectionViewCell
cell.lbl_MyVisitorTime.text = timeString
cell.lbl_MyVisitorDate.text = dateString
cell.actionRescheduling = {
let dv = NAViewPresenter().rescheduleMyVisitorVC()
dv.getTime = cell.lbl_MyVisitorTime.text!
dv.getDate = cell.lbl_MyVisitorDate.text!
}
return cell
}
This is the code of reschedule:
class RescheduleMyGuestListViewController: NANavigationViewController {
#IBAction func btnReschedule(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
reschedulingVisitorTimeInFirebase()
let lv = NAViewPresenter().myGuestListVC()
self.navigationController?.pushViewController(lv, animated: true)
}
}
Data is not update in collectionView after reschedule after.collectionview is also not calling after push view controller.