When a user swipe a cell, it becomes possible to delete it. However, the deletion occurs without animation.
Part of code in my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
guard let foodCategoryDetailViewModel = foodCategoryDetailViewModel else { return }
tableView.delegate = nil
tableView.dataSource = nil
foodCategoryDetailViewModel.foodsInSelectedCategory
.bind(to: tableView.rx.items(cellIdentifier: FoodCategoryDetailTableViewCell.cellIdentifier, cellType: FoodCategoryDetailTableViewCell.self))
{ row, food, cell in
cell.foodCategoryDetailCellViewModel = foodCategoryDetailViewModel.cellViewModel(forRow: row)
}.disposed(by: disposeBag)
tableView.rx.itemDeleted.subscribe(onNext: { indexPath in
foodCategoryDetailViewModel.removeFoodFromApplication(atRow: indexPath.row)
}).disposed(by: disposeBag)
}
Part of code in my ViewModel:
class FoodCategoryDetailTableViewViewModel: FoodCategoryDetailTableViewViewModelType {
var foodsInSelectedCategory: BehaviorRelay<[Food]>
func removeFoodFromApplication(atRow row: Int) {
if let food = getFood(atRow: row) {
foodsInSelectedCategory.remove(at: row)
//remove from core data
CoreDataHelper.sharedInstance.removeFoodFromApplication(foodName: food.name!)
}
}
How to animating deleting process from tableView?
In order to animate the deleting process, you need a datasource that is designed to do that. The default datasource in RxSwift is not.
The most popular library for such a thing is RxDataSources which brings in a full blown multi-section system for animating table and collection views, but if you don't want something that elaborate, you can easily write your own.
Here is an example of a simple animatable datasource for RxSwift that uses DifferenceKit to calculate which cells must be animated on/off: (https://github.com/danielt1263/RxMultiCounter/blob/master/RxMultiCounter/RxExtensions/RxSimpleAnimatableDataSource.swift)
Related
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
I do have following structure:
- TableView
-- Custom Table View Cell
--- CollectionView
---- Custom CollectionView Cell
I want to understand that how can I pass the data from / using view model in this structure with RxSwift - MVVM Structure.
Whenever I do get response from API it should update the data in table view rows and associated collection view cell respectively.
The simplest solution is to use an array of arrays.
For example. Let's assume your API returns:
struct Group: Decodable {
let items: [String]
}
Then your view model would be as simple as this:
func tableViewItems(source: Observable<[Group]>) -> Observable<[[String]]> {
return source
.map { $0.map { $0.items } }
}
When creating your cell, you can wrap the inner array into an observable with Observable.just() like this:
// in your view controller's viewDidLoad for example.
tableViewItems(source: apiResponse)
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: CollectionTableViewCell.self)) { _, element, cell in
Observable.just(element)
.bind(to: cell.collectionView.rx.items(cellIdentifier: "Cell", cellType: UICollectionViewCell.self)) { _, element, cell in
let label = (cell.viewWithTag(6969) as? UILabel) ?? UILabel()
label.tag = 6969
label.text = element
label.sizeToFit()
cell.addSubview(label)
}
.disposed(by: cell.disposeBag)
}
.disposed(by: dispsoeBag)
Here is an example I wrote just now to demonstrate how you can use RxSwift to do what you want. Important Note: This is a rough example, not optimally written and not tested! I just wrote it using a text editor hope it helps you out, if not I will try to polish it when I have some more time.
class MyViewModel {
// Lets say TableData is your model for the tableView data and CollectionData for your collectionView
public let tableData : PublishSubject<[TableData]> = PublishSubject()
public let collectionData : PublishSubject<[CollectionData]> = PublishSubject()
private let disposeBag = DisposeBag()
func fetchData() {
// Whenever you get an update from your API or whatever source you call .onNext
// Lets assume you received an update and stored them on a variable called newShopsUpdate
self.tableData.onNext(newTableDataUpdate)
self.collectionData.onNext(newCollectionDataDataUpdate)
}
}
class MyViewController: UIViewController {
var tableData: BehaviorRelay<[TableData]> = BehaviorRelay(value: [])
var collectionData: BehaviorRelay<[CollectionData]> = BehaviorRelay(value: [])
let viewModel = MyViewModel()
override func viewDidLoad() {
super.viewDidLoad()
// Setup Rx Bindings
viewModel
.tableData
.observeOn(MainScheduler.instance)
.bind(to: self.tableData)
.disposed(by: DisposeBag())
viewModel
.collectionData
.observeOn(MainScheduler.instance)
.bind(to: self.collectionData)
.disposed(by: DisposeBag())
// Register yours Cells first as usual
// ...
// Setting the datasource using RxSwift
tableData.bind(to: tableView.rx.items(cellIdentifier: "yourCellIdentifier", cellType: costumeTableViewCell.self)) { row, tableData, cell in
// Set all the cell properties here
// Lets also assume you have you collectionView inside one of the cells
cell.tableData = tableData
collectionData.bind(to: cell.collectionView.rx.items(cellIdentifier: "yourCellIdentifier", cellType: costumeCollectionViewCell.self)) { row, collectionData, cell in
// Set all the cell properties here
cell.collectionData = collectionData
}.disposeBag(by: DisposeBag())
}.disposed(by: DisposeBag())
}
}
In my mobile application I would like to update the tableView datasource by a pull to refresh request, but I don't know how to insert the new items on top of the tableview datasource.
I see that there is a a method of insertRows such as : self.tableView?.insertRows(at: [indexPath], with: .top) but how do I add the newItems here according to my methods I have?
I have a function called initializedTableView() that initializes the tableView with PublishSubject observable items.
func initializeTableView() {
viewModel
.items
.subscribe(onNext: { items in
self.tableView?.delegate = nil
self.tableView?.dataSource = nil
Observable.just(items)
.bind(to:(self.tableView?.rx.items(cellIdentifier:
itemCell.Identifier, cellType: itemCell.self))!) {
(index, element, cell) in
cell.itemModel = element
}.disposed(by: self.disposeBag)
})
.disposed(by: disposeBag)
}
This function is called once a pull to refresh is requested by user:
func refreshTableView() {
// get new items
viewModel
.newItems
.subscribe(onNext: { newItems in
//new
let new = newItems.filter({ item in
// items.new == true
})
//old
var old = newItems.filter({ item -> Bool in
// items.new == false
})
new.forEach({item in
// how to update tableView.rx.datasource here???
})
}).disposed(by: disposeBag)
}
struct ViewModel {
let items: BehaviorRelay<[Item]>
init() {
self.items = BehaviorRelay(value: [])
}
func fetchNewItems() {
// This assumes you are properly distinguishing which items are new
// and `newItems` does not contain existing items
let newItems: [Item] = /* However you get new items */
// Get a copy of the current items
var updatedItems = self.items.value
// Insert new items at the beginning of currentItems
updatedItems.insert(contentsOf: newItems, at: 0)
// For simplicity this answer assumes you are using a single cell and are okay with a reload
// rather than the insert animations.
// This will reload your tableView since 'items' is bound to the tableView items
//
// Alternatively, you could use RxDataSources and use the `RxTableViewSectionedAnimatedDataSource`
// This will require a section model that conforms to `AnimatableSectionModelType` and some
// overall reworking of this example
items.accept(updatedItems)
}
}
final class CustomViewController: UIViewController {
deinit {
disposeBag = DisposeBag()
}
#IBOutlet weak var tableView: UITableView!
private var disposeBag = DisposeBag()
private let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CustomTableCell.self, forCellReuseIdentifier: "ReuseID")
tableView.refreshControl = UIRefreshControl()
viewModel.items
.bind(to: tableView.rx.items(cellIdentifier: "ReuseID", cellType: CustomTableCell.self)) { row, item, cell in
// Configure cell with item
cell.configure(with: item)
}
.disposed(by: disposeBag)
tableView.refreshControl?.rx.controlEvent(.valueChanged)
.subscribe(onNext: { [weak self] in
self?.viewModel.fetchNewItems()
})
.disposed(by: disposeBag)
}
}
Alternative answer using BehaviorRelay and bindings. This way, you are only updating the items relay and it will automatically update the tableView. It also provides a more "Rx" way of handling pull to refresh.
As mentioned in the code comments, this assumes you are determining which items are new and that newItems does not contain any existing items. Either way this should provide a starting point.
struct ViewModel {
let items: Observable<[Item]>
init(trigger: Observable<Void>, newItems: #escaping () -> Observable<[Item]>) {
items = trigger
.flatMapLatest(newItems)
.scan([], accumulator: { $1 + $0 })
}
}
The above doesn't handle errors, nor does it handle resets, but the scan will put the new items at the top of the list.
The situation doesn't feel right though. Normally, the API call returns all the items, how can it possibly know which items are "new"?
I did something similar with my app since I had issues with tableView.insertRows.
Here is the code:
func loadMoreComments() {
// call to backend to get more comments
getMoreComments { (newComments) in
// combine the new data and your existing data source array
self.comments = newComments + self.comments
self.tableView.reloadData()
self.tableView.layoutIfNeeded()
// calculate the total height of the newly added cells
var addedHeight: CGFloat = 0
for i in 0...result.count {
let indexRow = i
let tempIndexPath = IndexPath(row: Int(indexRow), section: 0)
addedHeight = addedHeight + self.tableView.rectForRow(at: tempIndexPath).height
}
// adjust the content offset by how much height was added to the start so that it looks the same to the user
self.tableView.contentOffset.y = self.tableView.contentOffset.y + addedHeight
}
}
So, by calculating the heights of the new cells being added to the start and then adding this calculated height to the tableView.contentOffset.y, I was able to add cells to the top of the tableView seamlessly without reworking my tableView. This may look like a jerky workaround, but the shift in tableView.contentOffset isn't noticeable if you calculate the height properly.
I found the pattern used by the Google places IOS SDK to be clean and well designed. Basically they follow what is presented on the following apple presentation: Advanced User interface with Collection view (It start slide 46).
This is what is implemented in their GMSAutocompleteTableDataSource.
It us up to the datasource to define the state of the tableview.
We link the tableview.
var googlePlacesDataSource = GMSAutocompleteTableDataSource()
tableView.dataSource = googlePlacesDataSource
tableView.delegate = googlePlacesDataSource
Then every time something change the event is binded to the datasource:
googlePlacesDataSource.sourceTextHasChanged("Newsearch")
The data source perform the query set the table view as loading and then display the result.
I would like to achieve this from my custom Source:
class JourneyTableViewDataSource:NSObject, UITableViewDataSource, UITableViewDelegate{
private var journeys:[JourneyHead]?{
didSet{
-> I want to trigger tableView.reloadData() when this list is populated...
-> How do I do that? I do not have a reference to tableView?
}
}
override init(){
super.init()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
...
}
Any idea?
to update dynamic tableview, you have to add reloadData, reloadData updates datasource.
self.tableview.reloadData()
Your data source shouldn't know anything about your table view. This is the basic of this patter and it gives you a lot of power in terms of reusability.
For your purpose the best solution is to use the property closure:
var onJourneysUpdate: (() -> Void)?
private var journeys: [JourneyHead]? {
didSet {
onJourneysUpdate?()
}
}
And in the point where you set your data source you can set whatever action you would like to execute after updating the datasource's array:
var googlePlacesDataSource = JourneyTableViewDataSource()
tableView.dataSource = googlePlacesDataSource
tableView.delegate = googlePlacesDataSource
googlePlacesDataSource.onJourneysUpdate = { [unowned self] in
self.tableView.reloadData()
}
I'm using the latest version of Moya with RxSwift and I've encountered a logical issue for which I can't find a solution, at the moment.
Let's say that I have a UITableViewController with a ViewModel that implements the following interface:
protocol ListViewModelType {
var items: Observable<[Item]> { get }
}
The items property is implemented as (using EVReflection):
var items: Observable<[Therapy]> {
get {
let provider = RxMoyaProvider<ItemService>()
return provider
.request(.all)
.map(toArray: Item.self)
}
}
In the viewDidLoad method of the UITableViewController, I have set up a binding between the items property and the tableView via the following code:
self.viewModel.items
.bind(to: tableView.rx.items(cellIdentifier: cellIdentifier, cellType: cellType)) { row, element, cell in
// cell configuration code
}
.disposed(by: self.disposeBag)
Now, I would like to refresh the content of the UITableView to reflect changes that the user has done through other parts of the app. Considering that the RxMoyaProvider returns an Observable, this should be easily done through another value emitted by the Observable, but I don't know how to communicate to the provider that it should refresh the content from the server and put it into the same Observable.
Am I missing something here? Is there a more recommended way to bind a UITableView to a list of objects coming from a RxMoyaProvider?
You have to reload our Moya request. this is a bit hacky but i guess you get the hint.
let didAppear = Variable(true)
override func viewDidAppear(_ animated: Bool) {
didAppear.value = true
}
override func viewDidLoad(){
self.didAppear.flatMap{ _ in self.viewModel.items}
.bind(to: tableView.rx.items(cellIdentifier: cellIdentifier, cellType: cellType)) { row, element, cell in
// cell configuration code
}
.disposed(by: self.disposeBag)
}
alternatively you could redesign our architecture with a offline-first principle