RxSwift - auto reloading UITableView initialized with Observable - ios

I have main UITableView with some Todos that are populated used an array of Observables:
// on viewDidLoad
self.todosViewModel.todos.asObservable()
.bind(to: tableView.rx.items(cellIdentifier: "TodoViewCell", cellType: TodoTableViewCell.self)) { (row, todo, cell) in
cell.status.image = todo.getStatusImage()
cell.title.text = todo.title.value
cell.todoDescription.text = todo.description.value
cell.dueDate.text = String(describing: Utilities.dateFormatter.string(from: todo.dueDate.value))
}.disposed(by: disposeBag)
On another screen I can add/edit the data and then, click "save" button to append a new todo or modify the one being edited. This works great, except that the tableView don't reload automatically, forcing me to call tableView.reloadData() on viewDidAppear, which is triggered when the other screen is dismissed.
So,
Is there a native way for me to reload a table automatically when then todosViewModel variable is updated?
EDIT:
If on the screen of edition of the todo I reassociate the todosViewModel's value property with the same value, it also works:
self.todosViewModel.todos.value = self.todosViewModel.todos.value
That's pretty ugly, but I only know how to reload a tableView using one of those ways.

When I append a new todo or reassociate a new value to the base todos it works. When I edit, no.
That's the whole point. For the UITableView to update, your todos have to emit when an item inside has been edited or you have to bind something from your todo within the cell (this way you cannot change hight of the cell but you can push some dynamic information onto the cell).
Another option would be to map some observable from todo to the index of the cell where it's presented and call UITableView.reloadRows to update your edited cell.
I would recommend you to take a look at RxDataSources examples.

Related

What is "stable identifier" for SwiftUI Cell?

While listening Use SwiftUI with UIKit (16:54), I heard, that the reporter said:
"When defining swipe actions, make sure your buttons perform their actions using a stable identifier for the item represented.
Do not use the index path, as it may change while the cell is visible, causing the swipe actions to act on the wrong item."
- what??? All these years I was fighting with prepareForReuse() and indexPath in cells trying to somehow fix bugs related to cell reusing.
What is this "stable identifier"?
Why does no one talk about it?
On stackoverflow you can find answers only related to prepareForReuse() function. No "stable identifier".
Is it reuseIdentifier?
If so, how I suppose to use it?
Creating for each cell its own reuseIdentifier, like this:
for index in 0..<dataSourceArray.count {
tableView.register(MyTableViewCell.self, forCellReuseIdentifier: "ReuseIdForMyCell" + "\(index)")
}
She made a mistake, if you download the sample associated with the video you'll see the deleteHandler captures the item from outside already, it doesn't look it up again when the handler is invoked. She was trying to say that if you look up an item in the handler then there is a chance if other rows have been added or removed then its index will have changed so if you use the rows old index to look up the item then you would delete the wrong one. But since no lookup is required that will never happen, so she shouldn't have even mentioned it. Here is the code in question:
// Configures a list cell to display a medical condition.
private func configureMedicalConditionCell(_ cell: UICollectionViewListCell, for item: MedicalCondition) {
cell.accessories = [.delete()]
// Create a handler to execute when the cell's delete swipe action button is triggered.
let deleteHandler: () -> Void = { [weak self] in
// Make sure to use the item itself (or its stable identifier) in any escaping closures, such as
// this delete handler. Do not capture the index path of the cell, as a cell's index path will
// change when other items before it are inserted or deleted, leaving the closure with a stale
// index path that will cause the wrong item to be deleted!
self?.dataStore.delete(item)
}
// Configure the cell with a UIHostingConfiguration inside the cell's configurationUpdateHandler so
// that the SwiftUI content in the cell can change based on whether the cell is editing. This handler
// is executed before the cell first becomes visible, and anytime the cell's state changes.
cell.configurationUpdateHandler = { cell, state in
cell.contentConfiguration = UIHostingConfiguration {
MedicalConditionView(condition: item, isEditable: state.isEditing)
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: deleteHandler) {
Label("Delete", systemImage: "trash")
}
}
}
}
}

Adding a cell to another TableView. (selecting an item from a tableview, showing it in another tableview)

I'm building an app which which has built in with 2 different tabs. First tab is is "Home" which basically has a tableview with cells that configured from an api.(The api gets me country names for now)
Those cells also have a "Star" button which prints the data of the specific cell for now.
Second tab is "Saved" tab(SavedViewController), where I want to show the "starred" countries, using a tableview.
You can see the image below in order to get an idea for the app.
App simulation Image
The star button has a function in my CountriesTableViewCell. I'm using a saveButtonDelegate in order to let the SavedViewController know about an item is going to be saved. The code in CountriesTableViewCell for star button is as below.
#objc func buttonTapped() {
//If Button is selected fill the image. Else unfill it.
if !isSaveButtonSelected {
saveButton.setImage(UIImage(systemName: "star.fill"), for: .normal)
isSaveButtonSelected = true
saveButtonDelegate?.saveButtonClicked(with: countryData) //Checking if save button is clicked
}
}
countryData is the data that I get from the api, and this is the data I want to pass to SavedViewController.
struct CountryData: Codable {
let name : String
}
So on the SavedViewController, I'm handling the data using the SaveButtonProtocol conformance as below:
extension SavedViewController: SaveButtonProtocol {
func saveButtonClicked(with data: CountryData) {
countryDataArray.append(data)
print("saveButtonClicked")
print("countryData in savevc is \(countryDataArray)")
DispatchQueue.main.async {
self.countriesTableView.reloadData()
}
}
}
Whenever I click the star button on the first tab, this function is getting called on SavedViewController. So whenever I click to button, those print statements above work fine.
The problem is, whenever the star button is clicked, it should append the data of the current clicked cell to countryDataArray in SavedViewController. But the array is not populating as it should.
Let's say I pressed the first cell's star button, my print("countryData in savevc is (countryDataArray)") statement prints : ["Vatican City"], then I press the second cell's star button it only prints ["Ethiopia"] while it should print ["Vatican City", "Ethiopia"]
Why this issue is happening? My best guess is I'm delegating SavedViewController from the cell class so it behaves differently for every other cell. If that is the problem what should I do to solve it?
Many Thanks.
You should store your data in a shared (static array) object so you only have one source and add the saved indicator in your country struct so you do not rely on what is displayed in one view controller.

Update array data used to create UITableView when IBAction is fired

I have created a UITableView that includes a UIImage, and two UILabels. This data is passed to a details view controller when the user selects the row. I have also successfully created a UIButton in the prototype cell that updates the text of one of the UILabels. I am unsuccessfully attempting to update this label within the array so this change is reflected on the details view controller. This is what I tried with no success:
#IBAction func updateTableData(_ sender: Any) {
grinds.removeAll()
grinds = createArray()
}
Here is a link to the repo: https://github.com/andrewTuzson/tableViewPractice
In the screenshot below, the left image displays the tableview after the first cell has been toggled to "Needs Work". The right image displays the details screen that is loaded after selecting the row.
How should I approach solving this?
If you want to update a tableViewCell upon the button click then, you should make the model array global to access it in click method and update what you want then reload the tableView either with delegate or NSNotificationCenter
--
in the custom class of the cell declare an integer property and in cellForRow set it like this
cell.index = indexpath.row

Simultaneously change display parameters on all table view cells

I am trying to implement a table view design where a user can click a button outside of a table view cell and the display mode of all the buttons should change. However this is not the 'selected' mode for a given cell (that will be yet a third state that becomes accessible via switching to this second state). What's the proper way to accomplish this?
I am using dequeueReusableCellWith so I don't want to simply cycle through every cell because some that are out of sight probably shouldn't be modified. I simply want any cell that is visible, or becomes visible, while the table view cell is in this second display mode to follow a second design rather than the first design.
The second design, for now, is being modified via a method I added to a subclass of UITableViewCell like so:
- (void) p_refreshDisplay {
if (self.editing) {
self.buttonToClearWidth.constant = 20;
self.buttonToClearLeadingWidth.constant = 20;
} else {
self.buttonToClearWidth.constant = 0;
self.buttonToClearLeadingWidth.constant = 0;
}
}
However, I'm not sure how to trigger this p_refreshDisplay for every visible (and to become visible) cell. It seems unwise to call this many times and refresh the table view. What would be the proper way to accomplish what I want to do?
You do what should be done for any table view change:
Update your data model or some flag as needed.
Either call reloadData on the table view or call reloadRowsAtIndexPaths:withRowAnimation: passing in indexPathsForVisibleRows as the list of rows to reload.
Implement cellForRowAtIndexPath to provide appropriate cells for the given data/flags.
It sounds like you should have a custom cell class that has one or more properties that can be set on the cell in cellForRowAtIndexPath so the cell can render itself properly based on the specified state.
You can achieve this by doing three things:
Establish some property that indicates the "mode" of the table, either a boolean or perhaps an enum if there are more than three states
Ensure that cellForRowAtIndexPath configures the cell appropriately based on the value of this property. This will ensure that newly displayed cells are configured correctly.
When the "mode" changes you can use the tableview's visibleCells property to update any currently visible cells:
for cell in tableview.visibleCells {
if let myCell = cell as? MyCustomCellClass {
myCell.setButtonStyle()
}
}

Delete dynamic cell in GetCell method

I am trying trying to remove a cell from the UITableView if the image for the cell is a bad image. Basically my code does a threadPool call for each cell's image to make the flow of binding the data as a user scroll smooth as so inside the GetCell method:
public override UITableViewCell GetCell (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
{
//other stuff
ThreadPool.QueueUserWorkItem(new WaitCallback(RetrieveImage),(object)cell);
}
within that asynchronous method I call the cell.imageUrl property and bind it to an NSData object as so:
NSData data = NSData.FromUrl(nsUrl); //nsUrl is cell.imageUrl
From there I know that if data==null then there was a problem retrieving the image so I want to remove it. What I currently do is set the hidden property to true, but this leaves a blank space. I want to remove the cell entirety so the user won't even know it existed.
I can't set the cell height to 0 because I don't know if the imageUrl is bad until that indexrowpath initiates getcell call. I don't want to just check all images from the data thats binded to the UITableView because that would be a big performance hit since it can be easily a hundred items. I am currently grabbing the items from a webservice that gives me the first 200 items.
How can I remove the cell altogther if the data==null example
NSUrl nsUrl = new NSUrl(cell.imageUrl);
NSData data = NSData.FromUrl(nsUrl);
if (data != null) {
InvokeOnMainThread (() => {
cell.imgImage = new UIImage (data);
//cell.imgImage.SizeToFit();
});
} else {
//I tried the below line but it does't work, I have a cell.index property that I set in the getCell method
_data.RemoveAt(cell.Index);//_data is the list of items that are binded to the uitableview
InvokeOnMainThread (() => {
//cell.Hidden = true;
});
}
You remove cells by removing them from your source and then you let UITableView know that the source has changed by calling ReloadData(). This will trigger the refresh process. UITableView will ask your source how many rows there are and because you removed one row from your data, the cell representing that row will be gone.
However ReloadData() will reload your entire table. There is a methods which allows you to remove cells specifically, named DeleteRows(). You can find an example here at Xamarin.
When deleting rows it is important that you update your model first, then update the UI.
Alternatively, I recommend you to check out MonoTouch.Dialog which allows interaction with UITableView in a more direct way. It also includes support for lazy loading images.

Resources