Swift DiffableDataSource Snapshot Not Updating - ios

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.

Related

iOS tableViewCell is not updating after reloading the tableview even after setting the properties properly

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.

How to save the text in textfield in swift?

I have a tableview consisting of 2 cells and each cell has a textfield. I'd like to hold the text in textfield in an another variable and append it into an array but it just save the second cell's textfield's text into the variable and doesn't append the array.
Here is cellForRowAt code :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableView == queryTableView {
let cell = tableView.dequeueReusableCell(withIdentifier: queryCellReuseIdentifier,
for: indexPath) as! queryCell
cell.selectionStyle = UITableViewCell.SelectionStyle.gray
queryTextField = UITextField(frame: CGRect(x: 20, y: 0, width: 300, height: 20))
queryTextField.delegate = self
queryTextField.placeholder = "Soruyu buraya giriniz"
queryTextField.font = UIFont.systemFont(ofSize: 15)
cell.addSubview(queryTextField)
return cell
}
Here is my related function :
func textFieldDidEndEditing(_ textField: UITextField) {
var temp = queryTextField.text!
queryArray.append(temp)
print(temp)
}
if your project is non-storyboard approach, then I would say, it would be more easier and flexible to take control of each type and data and action since sometimes things not quite flexible with storyboard only.. . as you create, even a single UIElement, UI conforms to those properties, methods and actions which you define, means fully customized code - custom defined code base..
now to your question, according to what you did, probably, your textfield text is overwritten to blank, losing the previously entered value every time the cell is dequeued, cell is generated fresh again for use every time " dequeue cell " method is executed, if you want to persist your previous value, I would say, " remind the dequeued cell that it had some value before being re- dequeued again " means:
declare your global array or dict.
dequeue your cell and setup your cell properties
3 after cell is dequeued, do something like this, and your cells previous value is saved even after the cell is dequeued.
example, common approach:
struct DefaultInfo {
var string: String?
var type: SomeData? //optional
}
let array: [DefaultInfo] = []
var array = self.array[indexPath.row]
// after cell is dequeued:
switch array.type {
case .someCase:
let string = cell.textField.text
array.string = string
cell.textField.text = array.string
//print(array.string!)
// now you should be able to see your input after cell is
//renewed.. . try it.. .
default: break
}
It seems like you have an independant variable queryTextField which is overriden when you create the second cell.
Also, in textFieldDidEndEditing try accessing textField instead of queryTextField, like this:
func textFieldDidEndEditing(_ textField: UITextField) {
var temp = textField.text!
queryArray.append(temp)
print(temp)
}
Responding to this:
I need every cell has its textfield and I need to be able to save
their texts into an array. After that I am going to send them to web
service. To sum up, they will be my parameters.
You don't need to have global queryTextField just for that. You can remove this variable, if that's its only goal.
Do you need to send web request on some trigger? Like button tap. I assume, yes.
Since theoretically not all your cells are visible at the same time (e.g. when thay do not fit the screen), it's bad idea to try and track texts in text fields. Instead, you need some kind of model to store all texts (paired with indeces, maybe). The simplest would be dictionary where key is cell identifier (e.g. indexPath) and value is the text.
In textFieldDidEndEditing you can report the changes to the view controller. For this you need to assign the cell as the delegate for its text field. And view controller - as the delegate for the cell. In textFieldDidEndEditing cell would be calling view controller delegate method to report the text change, passing (for example) self and text as parameters. View controller would then be able to find its index path and store the text in the model (dictionary).
On trigger (button click?) view controller will be able to build parameters from the model (dictionary).
Consider this pseudocode as a direction:
cellForRow {
...
cell = ...
let textField = ...
textField.delegate = cell
cell.addSubview(textField)
cell.delegate = self
}
In cell:
textFieldDidEndEditing(textField: UITextField) {
delegate?.report(text: textField.text ?? "", in: self)
}
In view controller:
report(text: String, in cell: UITableViewCell) {
let indexPath = tableView.indexPath(of: cell)
model[indexPath] = text
}

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 :)

Data From Label in CollectionViewCell Sometimes Refreshes on Reload other times it Doesn't

First let me say this seems to be a common question on SO and I've read through every post I could find from Swift to Obj-C. I tried a bunch of different things over the last 9 hrs but my problem still exists.
I have a vc (vc1) with a collectionView in it. Inside the collectionView I have a custom cell with a label and an imageView inside of it. Inside cellForItem I have a property that is also inside the the custom cell and when the property gets set from datasource[indePath.item] there is a property observer inside the cell that sets data for the label and imageView.
There is a button in vc1 that pushes on vc2, if a user chooses something from vc2 it gets passed back to vc1 via a delegate. vc2 gets popped.
The correct data always gets passed back (I checked multiple times in the debugger).
The problem is if vc1 has an existing cell in it, when the new data is added to the data source, after I reload the collectionView, the label data from that first cell now shows on the label in new cell and the data from the new cell now shows on the label from old cell.
I've tried everything from prepareToReuse to removing the label but for some reason only the cell's label data gets confused. The odd thing is sometimes the label updates correctly and other times it doesn't? The imageView ALWAYS shows the correct image and I never have any problems even when the label data is incorrect. The 2 model objects that are inside the datasource are always in their correct index position with the correct information.
What could be the problem?
vc1: UIViewController, CollectionViewDataSource & Delegate {
var datasource = [MyModel]() // has 1 item in it from viewDidLoad
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell
cell.priceLabel.text = ""
cell.cleanUpElements()
cell.myModel = dataSource[indexPath.item]
return cell
}
// delegate method from vc2
func appendNewDataFromVC2(myModel: MyModel) {
// show spinner
datasource.append(myModel) // now has 2 items in it
// now that new data is added I have to make a dip to fb for some additional information
firebaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
if let dict = snapshot.value as? [String: Any] else { }
for myModel in self.datasource {
myModel.someValue = dict["someValue"] as? String
}
// I added the gcd timer just to give the loop time to finish just to see if it made a difference
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
self.datasource.sort { return $0.postDate > $1.postDate } // Even though this sorts correctly I also tried commenting this out but no difference
self.collectionView.reloadData()
// I also tried to update the layout
self.collectionView.layoutIfNeeded()
// remove spinner
}
})
}
}
CustomCell Below. This is a much more simplified version of what's inside the myModel property observer. The data that shows in the label is dependent on other data and there are a few conditionals that determine it. Adding all of that inside cellForItem would create a bunch of code that's why I didn't update the data it in there (or add it here) and choose to do it inside the cell instead. But as I said earlier, when I check the data it is always 100% correct. The property observer always works correctly.
CustomCell: UICollectionViewCell {
let imageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
let priceLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var someBoolProperty = false
var myModel: MyModel? {
didSet {
someBoolProperty = true
// I read an answer that said try to update the label on the main thread but no difference. I tried with and without the DispatchQueue
DispatchQueue.main.async { [weak self] in
self?.priceLabel.text = myModel.price!
self?.priceLabel.layoutIfNeeded() // tried with and without this
}
let url = URL(string: myModel.urlStr!)
imageView.sd_setImage(with: url!, placeholderImage: UIImage(named: "placeholder"))
// set imageView and priceLabel anchors
addSubview(imageView)
addSubview(priceLabel)
self.layoutIfNeeded() // tried with and without this
}
}
override func prepareForReuse() {
super.prepareForReuse()
// even though Apple recommends not to clean up ui elements in here, I still tried it to no success
priceLabel.text = ""
priceLabel.layoutIfNeeded() // tried with and without this
self.layoutIfNeeded() // tried with and without this
// I also tried removing the label with and without the 3 lines above
for view in self.subviews {
if view.isKind(of: UILabel.self) {
view.removeFromSuperview()
}
}
}
func cleanUpElements() {
priceLabel.text = ""
imageView.image = nil
}
}
I added 1 breakpoint for everywhere I added priceLabel.text = "" (3 total) and once the collectionView reloads the break points always get hit 6 times (3 times for the 2 objects in the datasource).The 1st time in prepareForReuse, the 2nd time in cellForItem, and the 3rd time in cleanUpElements()
Turns out I had to reset a property inside the cell. Even though the cells were being reused and the priceLabel.text was getting cleared, the property was still maintaining it's old bool value. Once I reset it via cellForItem the problem went away.
10 hrs for that, smh
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: customCell, for: indexPath) as! CustomCell
cell.someBoolProperty = false
cell.priceLabel.text = ""
cell.cleanUpElements()
cell.myModel = dataSource[indexPath.item]
return cell
}

how can I refresh a single button in UITableViewCell instead of refreshing whole table or whole cell?

In my swift app I have a UITableView with one static cell and many dynamic cells.
Static cell contains different fields, such as labels, map (taken from MapKit) and a button, that indicates whether user voted up or not.
Now, when user presses the button, I want to change its color, possibly without refreshing anything else.
So far my code looks like this:
var currentUserVote:Int = 0
func tableView(_ tview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath as NSIndexPath).row == 0 {
let cell = tview.dequeueReusableCell(withIdentifier: "cellStatic") as! VideoDetailsCell
fetchScore(cell.score)
let voteUpImage = UIImage(named: "voteUp");
let tintedVoteUpImage = voteUpImage?.withRenderingMode(UIImageRenderingMode.alwaysTemplate)
cell.voteUpButton.setImage(tintedVoteUpImage, for: UIControlState())
checkUsersVote() { responseObject in
if(responseObject == 1) {
cell.voteUpButton.tintColor = orangeColor
} else if (responseObject == -1){
cell.voteUpButton.tintColor = greyColor
} else {
cell.voteUpButton.tintColor = greyColor
}
self.currentUserVote = responseObject
}
//map handling:
let regionRadius: CLLocationDistance = 1000
let initialLocation = CLLocation(latitude: latitude, longitude: longitude)
centerMapOnLocation(initialLocation, map: cell.mapView, radius: regionRadius)
//cell.mapView.isScrollEnabled = false
cell.mapView.delegate = self
.
.
.
return cell
} else {
//handle dynamic cells
}
}
So in the method above I'm checking if user voted already and based on that I'm setting different color on the button. I'm also centering the map on a specific point.
Now, since it's a static cell, I connected IBAction outlet to that button:
#IBAction func voteUpButtonAction(_ sender: AnyObject) {
if(currentUserVote == 1) {
self.vote(0)
}else if (currentUserVote == -1){
self.vote(1)
} else {
self.vote(1)
}
}
and the vote method works as follows:
func vote(_ vote: Int){
let indexPath = IndexPath(row: 0, section: 0)
let cell = tview.dequeueReusableCell(withIdentifier: "cellStatic") as! VideoDetailsCell
switch(vote) {
case 1:
cell.voteUpButton.tintColor = orangeColor
case 0:
cell.voteUpButton.tintColor = greyColor
case -1:
cell.voteUpButton.tintColor = greyColor
default:
cell.voteUpButton.tintColor = greyColor
}
tview.beginUpdates()
tview.reloadRows(at: [indexPath], with: .automatic)
tview.endUpdates()
currentUserVote = vote
//sending vote to my backend
}
My problem is, that when user taps the button, he invokes the method vote, then - based on the vote, the button changes color, but immediately after that method cellForRow is called and it changes the color of the button again. Also, it refreshes the map that's inside of it.
What I want to achieve is that when user taps the button, it should immediately change its color and that's it. Map stays untouched and the button is not changed again from cellForRow.
Is there a way of refreshing only that particular button without calling again cellForRow?
First of all, you confuse static and dynamic cells. You can use static cells only in the UITableViewController and you can't use static and dynamic cell at the same time.
I strongly recommend you not to use cell for storing map and button. All elements from the cell will be released after scrolling it beyond the screen.
I can advise you use TableViewHeaderView for this task. In this case you will be able set button and map view as #IBOutlet.
(See more about adding tableview headerView. You can also set it from interface builder.)
Another way is change tableView.contentInset and set your view with map and button as subview to tableView. This method is used when you need create Stretchy Headers.
It should be quite easy, simply do it in your button handler. The sender argument there is the button object that caused the action. When you were connecting it from IB there was a dropdown to select sender type, you may have missed it and the whole thing would have been obvious with UIButton type there.
Simply change your handler like this :
#IBAction func voteUpButtonAction(_ sender: UIButton) {
if(currentUserVote == 1) {
self.vote(0)
}else if (currentUserVote == -1){
self.vote(1)
} else {
self.vote(1)
}
sender.backgroundColor = yourFavouriteColor
}
Another approach would be to create an IBOutlet for your button, since its from a static cell, and then you would be able to reference it from any place in your view controller.
In this call:
func tableView(_ tview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
I see it calls checkUsersVote() which I'm guessing should get the updated value set in the vote() call. Could the problem be that you aren't doing this
currentUserVote = vote
until after reloadRows() is called?

Resources