UITableView deleterows scroll to top - ios

I made a query to delete specified rows in UITableView but the problem when the deleteRows is called the UITableView scroll to top so how to prevent it scrolling to top? and why it scroll to top!
I try called a method to force it scroll to down but the tableview scroll to top first after return
the code of remove
FIRDatabase.database().reference().child("messages").child(self.roomId).observe(.childRemoved, with: {(snap) in
print("child Removed")
for msg in self.messageArray {
let message: NSDictionary = msg as! NSDictionary
let messageContent: [String: Any] = message as! [String: Any]
if messageContent["messageID"] as! String == snap.key {
let indexRemoved = self.messageArray.index(of: msg)
self.messageArray.remove(msg)
self.userArray.removeObject(at: indexRemoved)
let indexPath = IndexPath(row: indexRemoved, section: 0)
print(indexPath)
self.conversationTableView.deleteRows(at: [indexPath], with: UITableViewRowAnimation.none)
break;
}
}
})

Please be sure you are not using table view method like 'estimatedHeightFor...' to calculate hight to scroll table view after row delete. One more point to be sure is, reloadData method should not use when deleting row from table view if you don't want to table scrolling.
If all are good then on deleting a particular row, table view will move all rows up to fill deleted row empty space.

There are a few issues with the code that can be easily corrected.
First before doing anything, grab the current scroll position of the first visible item (given the cells are the same size)
let savedIndex = myTableView.indexPathsForVisibleRows?.first
then once the row is deleted from the array and you reload the tableview, you can return to where it was with
myTableView.scrollToRowAtIndexPath(savedIndex, atScrollPosition: .Top, animated: false)
If you need another option (variable heights), you can use
tableView.contentOffset.y;
Also, there's no need to iterate over the entire array to find the row to delete from it. Assuming the Firebase nodes(s) data are stored in the array as a class, in the array with the Firebase key as a property of each class, you can simply get the index of that element in the array and remove it. Here's an option
let key = snapshot.key
if let index = self.myArray.indexOf({$0.firebaseKey == key}) {
self.myArray.removeAtIndex(index)
self.myTableView.reloadData()
}
This object would look like:
class MyClass {
var firebaseKey: String?
var some_var: String?
var another_var: String?
}

Related

Reconfiguring UICollectionView cell data within a UICollectionViewDiffableDataSource

Say I have a collectionView, using a UICollectionViewDiffableDataSource<Section, Row> where Section is
enum Section {
case info
}
and Row is
enum Row {
case name(name: String)
case description(description: String)
}
I then define a configuration for these cells
let textRegistration = UICollectionView.CellRegistration<TextInputCollectionViewCell, Row> { (cell, indexPath, item) in
cell.item = item
}
I have a delegate method setup to be notified when the content of these cells change from user input,
func nameDidChange(to newName: String?) {
self.navigationItem.title = newTitle
//Update my model
}
func descriptionDidChange(to newDescription: String?) {
//Update my model
}
Now here is my issue, if I initially add this data to my dataSource like this:
var infoSnapshot = self.dataSource.snapshot()
infoSnapshot.appendSections([.info])
infoSnapshot.appendItems([
.name(name: self.myModel.name),
.description(description: self.myModel.description)
], toSection: .info)
self.dataSource.apply(infoSnapshot, animatingDifferences: false)
As soon as this cell is offscreen and reset, once it comes back it uses the old data initially set in the method above.
How do I reset the data, if doing so would change the hash, messing with my diffable datasource? Should I set the data elsewhere, not tying it to the dataSource?
Before Editing
During Editing
After Editing

Swift DiffableDataSource Snapshot Not Updating

I am working with DiffableDataSource inside of a collection view for the first time, but I am running into an error while trying to update a view.
So I am working with 2 custom cells. CustomCell1 is a cell that has a Label on the left and a UITextField on the right. CustomCell2 is a cell that has 2 column UIPickerView inside of it. CustomCell2 has a delegate that will let my main view controller know when it has been updated. This is all working fine.
So the problem is coming when I am trying to update the textfield in CustomCell1 with the value selected in the UIPickerView from CustomCell2. As a note, the textfield is being used for text entry in rows of the page. And the picker view is shown when a cell is clicked, and hidden when the cell is clicked again.
So in my delegate to update the cell, I have this code:
func updateSelectedCharacter(withCharacter character: String, indexPath: IndexPath) {
print(character)
DispatchQueue.main.async {
guard let charItem = self.dataSource.itemIdentifier(for: IndexPath(row: indexPath.row - 1, section: indexPath.section)), let parentId = charItem.parentId else { return }
self.updatePlayerData(forPlayerIndex: parentId, character: character)
var snap = self.dataSource.snapshot()
snap.reloadItems([charItem])
self.dataSource.apply(snap)
}
}
And in my cellForRowAt, I have the following code to update the text field in the character cell (CustomCell1). The gameInfo object is a struct that has information, including the character name I am trying to display, that I will eventually store off into CoreData from the entries in the cells.
guard let parentId = info.parentId else { return UICollectionViewListCell() }
let playerData = self.gameInfo.playerData[parentId]
var obj = TextEntryCellInformation(title: rowTitle, rowType: info.rowType)
obj.value = obj.value = playerData?.character
obj.isSelectable = false
return collectionView.dequeueConfiguredReusableCell(using: textEntryCell, for: indexPath, item: obj)
And here is the cell registration for that cell, where TextEntryCellInformation holds the row type, title for the label, and an optional value that can set textfield's text:
private func createTextEntryListCellRegistration() -> UICollectionView.CellRegistration<TextEntryCollectionViewCell, TextEntryCellInformation> {
return UICollectionView.CellRegistration<TextEntryCollectionViewCell, TextEntryCellInformation> { cell, indexPath, data in
var config = TextEntryContentConfiguration()
config.title = data.title
config.tag = data.rowType.rawValue
config.textChangedDelegate = self
config.isSelectable = data.isSelectable
config.customPlaceholder = "required"
if let val = data.value {
config.textValue = val
}
cell.contentConfiguration = config
}
}
So now, I think I have explained most of the code. Whenever I select the value in the picker view, the first time it will show the value correctly in the character cell. But if I change the value, the text seems to disappear from the character cell. However, I am getting the correct value from the picker view, verified with the print call at the start of the delegate function. I am also verifying that after I apply in the update delegate function, I am running through the cell registration and the correct value is being assigned to the config.textValue. I am not sure why then, if the config is being data is being set as expected with the correct character name, why the UI is not updating to show that information.
I included what I think is the relevant code and information. However, if more is needed, I will definitely update.
Thanks in advance for any help given!
Turns out I found the answer. When setting the configuration on the content view of the textfield custom cell (CustomCell1), I was doing check to conform to equatable:
static func == (lhs: TextEntryContentConfiguration, rhs: TextEntryContentConfiguration) -> Bool {
return lhs.title == rhs.title
}
I needed to be doing:
static func == (lhs: TextEntryContentConfiguration, rhs: TextEntryContentConfiguration) -> Bool {
return lhs.textValue == rhs.textValue
}
This checks the difference to know if I should be updating the config values or not.

UITableView Scrolls automatically while textfield begins editing

I am developing an iOS app which has different forms which is populated into a UITableview based on users selection. Each form has different fields like Textfield, DatePicker, PickerView. So I used a single TableViewCell (nib) to hold all these and show or hide the items based on question.
There is save function defined which will save values when user enters to an array.
My issue is, at times my tableview scrolls as if the index goes out of control. like when I select any textfield, Tableview scrolls to top. I have removed all keyboard observer methods and checked, still it is happening.
Below is my save function code:
func saveFormValue(mystr: String) {
//selectedIndex is a global variable (NSIndexPath)
let dict = sections[selectedIndex!.section].questions![selectedIndex!.row]
dict.answer = mystr
sections[selectedIndex!.section].questions![selectedIndex!.row] = dict
let answer = updated_answer.init(ID: ID, form_id: selected_form_id, form_name: formName, section_id: dict.section_id ?? "",question_id: dict.question_id ?? "", question_name: dict.question_name!,question_type:dict.question_type!)
updatedArray.append(answer)
self.tableView.reloadData()
}
This is the code in textfieldDidBeginEditing function (how selectedIndexPath is initialized):
guard let index = tableView.indexPath(for: cell) else {
return
}
selectedIndex = index as NSIndexPath
I have added delegate for cell, and one thing I noticed is, this issue is happening whenever I press pickerview or datepicker once. I couldn't see this issue If I only touch textField cells only.
Please let me know for any further details.
Try this code hope this helps to you.
if let thisIndexPath = tableView.indexPath(for: cell) {
tableView.scrollToRow(at: thisIndexPath, at: .top, animated: false)
}
On textfield delegate method textFieldDidBeginEditing use the following code:
func textFieldDidBeginEditing(_ textField: UITextField) {
let indexParh = NSIndexPath(row: textField.tag, section: 0)
self.constTBL_Bottom.constant = 260
self.tblViewObj.scrollToRow(at: indexParh as IndexPath, at: .middle, animated: false)
}
Also you need to manage the table bottom constant. When you resigning your keyboard set table view constant to 0
Hope this will work :)

UICollectionView custom layout cell-type-dependent attribute

My collection view has a dynamic layout and multiple cell/header types, and I would like to apply background color to only certain types of headers. The way I tried, unsuccessfully, to determine the IndexPath of those headers during run time is:
class MyCustomLayout: UICollectionViewFlowLayout {
private var backgroundAttrs = [UICollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
guard let numberOfSections = collectionView?.numberOfSections else { return }
backgroundAttrs.removeAll()
for section in 0 ..< numberOfSections {
if let header = collectionView?
.supplementaryView(forElementKind: UICollectionElementKindSectionHeader,
at: IndexPath(item: 0, section: section)) as? HeaderThatNeedsBackground,
let attr = getBackgroundAttribute(forSection: section) {
backgroundAttrs.append(attr)
}
}
}
...
}
supplementaryView(forElementKind:, at:) keeps giving me nil, so the loop is never executed. My guess is that the cells are not yet instantiated at this point? Any advice on how to selectively apply layout attribute based on cell/header type would be appreciated
First of all, you shouldn't call collection view methods that retrieve cell or supplementary views, since the collection view uses the layout (the one you are implementing) to retrieve information for those elements. You can read how to properly subclass a UICollectionViewLayout in this link.
In your particular case you need to override the function:
layoutAttributesForSupplementaryView(ofKind:at:)

Is it unsafe to call reloadData() after getting an indexPath but before removing a cell at that indexPath?

I'm trying to track down a difficult crash in an app.
I have some code which effectively does this:
if let indexPath = self.tableView.indexPath(for: myTableViewCell) {
// .. update some state to show a different view in the cell ..
self.tableView.reloadData()
// show nice fade out of the cell
self.friendRequests.remove(at: indexPath.row)
self.tableView.deleteRows(at: [indexPath], with: .fade)
}
The concern is that calling reloadData() somehow makes the indexPath I just retrieved invalid so the app crashes when it tries to delete the cell at that indexPath. Is that possible?
Edit:
The user interaction is this:
User taps a button [Add Friend] inside of table view cell <-- indexPath retrieved here
Change the button to [Added] to show the tap was received. <-- reloadData called here
Fade the cell out after a short delay (0.5s). <-- delete called here with indexPath from #1
I can change my code to not call reloadData and instead just update the view of the cell. Is that advisable? What could happen if I don't do that?
Personally, I'd just reload the button in question with reloadRows(at:with:), rather than the whole table view. Not only is this more efficient, but it will avoid jarring scrolling of the list if you're not already scrolled to the top of the list.
I'd then defer the deleteRows(at:with:) animation by some small fraction of time. I personally think 0.5 seconds is too long because a user may proceed to tap on another row and they can easily get the a row other than what they intended if they're unlucky enough to tap during the wrong time during the animation. You want the delay just long enough so they get positive confirmation on what they tapped on, but not long enough to yield a confusing UX.
Anyway, you end up with something like:
func didTapAddButton(in cell: FriendCell) {
guard let indexPath = tableView.indexPath(for: cell), friendsToAdd[indexPath.row].state == .notAdded else {
return
}
// change the button
friendsToAdd[indexPath.row].state = .added
tableView.reloadRows(at: [indexPath], with: .none)
// save reference to friend that you added
let addedFriend = friendsToAdd[indexPath.row]
// later, animate the removal of that row
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
if let row = self.friendsToAdd.index(where: { $0 === addedFriend }) {
self.friendsToAdd.remove(at: row)
self.tableView.deleteRows(at: [IndexPath(row: row, section: 0)], with: .fade)
}
}
}
(Note, I used === because I was using a reference type. I'd use == with a value type that conforms to Equatable if dealing with value types. But those are implementation details not relevant to your larger question.)
That yields:
Yes, probably what's happening is the table view is invalidating stored index path.
To test whether or not it is the issue try to change data that is represented in the table right before reloadData() is called.
If it is a problem, then you'll need to use an identifier of an object represented by the table cell instead of index path. Modified code will look like this:
func objectIdentifer(at: IndexPath) -> Identifier? {
...
}
func indexPathForObject(_ identifier: Identifier) -> IndexPath? {
...
}
if
let path = self.tableView.indexPath(for: myTableViewCell)
let identifier = objectIdentifier(at: path) {
...
self.tableView.reloadData()
...
if let newPath = indexPathForObject(identifier) {
self.friendRequests.removeObject(identifier)
self.tableView.deleteRows(at: [newPath], with: .fade)
}
}

Resources