How to use RxDataSource with SearchBar? - swift

I master RxSwift and when using RxDataSource, the SearchBar delegates do not work for me and he,
I can’t see the error. Without RxDataSource everything works, on other screens I have no problems.
Tell me, with a fresh look, what is the mistake? why doesn't the filter happen?
private var defaultCategories: [Groups]!
var groupsCoreData = BehaviorRelay<[Groups]>(value: [])
override func viewDidLoad() {
super.viewDidLoad()
searchBarRx()
tableViewRx()
}
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Groups>>(
configureCell: { (_, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "addNewWordsToGroup")!
cell.textLabel?.text = element.title
return cell
},
titleForHeaderInSection: { dataSource, sectionIndex in
return dataSource[sectionIndex].model
}
)
private func tableViewRx() {
let dataSource = self.dataSource
let items = [
SectionModel(model: "Пример", items: self.defaultCategories
.filter { $0.titleCategories == "Тест1"}),
SectionModel(model: "Пример2", items: self.defaultCategories
.filter { $0.titleCategories == "Тест2" })
]
Observable.just(items)
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
tableView
.rx
.modelSelected(Groups.self)
.subscribe(onNext: { [weak self] data in
}
.disposed(by: disposeBag)
}
private func searchBarRx() {
searchBar
.rx
.text
.orEmpty
.debounce(.microseconds(200), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.subscribe { [unowned self] query in
self.searchBar.showsCancelButton = query.element!.isEmpty
self.defaultCategories = query.element!.isEmpty ?
self.defaultCategories :
self.defaultCategories
.filter({ $0.title?.range(of: query.element!, options: .anchored) != nil
})
}
.disposed(by: disposeBag)
}
query - displays the input characters, but no result.
P.S. the arrays are not empty

The key is that you don't replace the datasource. Rx is a functional paradigm so no replacement is required. Instead you have to outline your invariants before hand. Like so:
final class ViewController: UIViewController {
var tableView: UITableView!
var searchBar: UISearchBar!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let initialItems = [
SectionModel(model: "Пример", items: [Groups(title: "Group1", titleCategories: "Тест1")]),
SectionModel(model: "Пример2", items: [Groups(title: "Group2", titleCategories: "Тест2")])
]
let searchTerm = searchBar.rx.text.orEmpty
.debounce(.microseconds(200), scheduler: MainScheduler.instance)
.distinctUntilChanged()
Observable.combineLatest(Observable.just(initialItems), searchTerm)
.map { filteredSectionModels(sectionModels: $0.0, filter: $0.1) }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
}
func filteredSectionModels(sectionModels: [SectionModel<String, Groups>], filter: String) -> [SectionModel<String, Groups>] {
guard !filter.isEmpty else { return sectionModels }
return sectionModels.map {
SectionModel(model: $0.model, items: $0.items.filter { $0.title?.range(of: filter, options: .anchored) != nil
})
}
}
private let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Groups>>(
configureCell: { (_, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "addNewWordsToGroup")!
cell.textLabel?.text = element.title
return cell
},
titleForHeaderInSection: { dataSource, sectionIndex in
return dataSource[sectionIndex].model
}
)
Pay special attention to how I combined the Observable that contains all the items with the Observable that tracks the current search filter. Then I only send the items to the table view that are actually supposed to be displayed.

Related

How do I bind a ViewModel to a Collationview?

I'm trying to bind a view model to a collection view. But I don't know how to do it. I'm using MVVM pattern and RxSwift, and I've only tried table view binding before. Here's my view model and the view controller code I've done so far.
class SearchViewModel: ViewModelType {
private let disposeBag = DisposeBag()
struct input {
let loadData: Signal<Void>
}
struct output {
let result: Signal<String>
let loadApplyList: PublishRelay<friends>
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let result = PublishSubject<String>()
let loadApplyList = PublishRelay<friends>()
input.loadData.asObservable().subscribe(onNext: { [weak self] in
guard let self = self else { return }
api.getFriend().subscribe(onNext: { (response, statuscode) in
switch statuscode {
case .ok:
if let response = response {
loadApplyList.accept(response)
}
default:
print("default")
}
}).disposed(by: self.disposeBag)
}).disposed(by: disposeBag)
return output(result: result.asSignal(onErrorJustReturn: ""), loadApplyList: loadApplyList)
}
}
This is my ViewModel code
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
}
And this is my ViewController code.
How should the collection view bind?
Here is what your view model should look like:
class SearchViewModel {
// no need for a disposedBag. If you are putting a disposeBag in your view model, you are likely doing something wrong.
struct input {
let loadData: Signal<Void>
}
struct output {
let loadApplyList: Driver<[User]> // you should be passing an array here, not an object.
}
func transform(_ input: input) -> output {
let api = SearchAPI()
let friendResult = input.loadData
.flatMapLatest {
api.getFriend()
.compactMap { $0.0.map(Result<friends, Error>.success) }
.asDriver(onErrorRecover: { Driver.just(Result<friends, Error>.failure($0)) })
}
let loadApplyList = friendResult
.compactMap { (result) -> [User]? in
guard case let .success(list) = result else { return nil }
return list.friends
}
return output(loadApplyList: loadApplyList)
}
}
Now in your view controller, you can bind it like this:
func bindViewModel() {
let input = SearchViewModel.input(loadData: loadData.asSignal(onErrorJustReturn: ()))
let output = viewModel.transform(input)
output.loadApplyList
.drive(collectionView.rx.items(cellIdentifier: "Cell", cellType: MyCellType.self)) { index, item, cell in
// configure cell with item here
}
.disposed(by: disposeBag)
}

RxSwift - Filter data from tableview from UIPickerView

i try to filter data from my table View. When i select one value from picker View, The data suppose to filter based on the category and the table view should reload with filtered data.
here i attach the code
struct RecipeList {
let id: Int
let catid: String
let name: String
let description: String
let ingredient: String
let step: String
let image: String
let img: NSData
//static var dataSource = BehaviorRelay(value: [RecipeList]())
static var items = [RecipeList]()
}
here how i fetch data to table View
func setupCellConfiguration() {
let recipeList = Observable.just(RecipeList.items)
recipeList
.bind(to: tableView
.rx
.items(cellIdentifier: RecipeListCell.Identifier,
cellType: RecipeListCell.self)) { row, recipe, cell in
cell.configureWithRecipe(recipe: recipe)
}
.disposed(by: disposeBag)
}
Here how pick event on picker view
func setupRecipeTypePickerView(textField: UITextField) {
let pickerView = UIPickerView()
pickerView.selectRow(Int(self.selectedRecipeType) ?? 0, inComponent: 0, animated: false)
textField.inputView = pickerView
let recipeType = Observable.of(RecipeType.items)
recipeType.bind(to: pickerView.rx.itemTitles) { (row, element) in
return "\(element.name)"
}
.disposed(by: disposeBag)
pickerView.rx.itemSelected
.subscribe { (event) in
switch event {
case .next(let selected):
selected.row == 0 ? (self.isFilter = false) : (self.isFilter = true)
let recipeType = RecipeType.items[selected.row]
textField.text = recipeType.name
self.selectedRecipeType = recipeType.id
RecipeList.items.append(contentsOf: RecipeList.items.filter({$0.catid == recipeType.id}))
default:
break
}
}
.disposed(by: disposeBag)
}
Here how append data from Core data to Model
func getRecipeList() {
RecipeList.items.removeAll()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: Common.ENTITYNAME)
do {
let result = try managedContext.fetch(fetchRequest)
for data in result as! [NSManagedObject] {
let id = data.value(forKey: "id") as! Int
let catid = data.value(forKey: "catid") as? String ?? "1"
let name = data.value(forKey: "name") as! String
let description = data.value(forKey: "rdescription") as! String
let ingredient = data.value(forKey: "ingredient") as! String
let step = data.value(forKey: "step") as! String
let image = data.value(forKey: "image") as! String
let img = data.value(forKey: "img") as! NSData
let recipe = RecipeList(id: id, catid: catid, name: name, description: description, ingredient: ingredient, step: step, image: image, img: img)
RecipeList.items.append(recipe)
}
// RecipeList.dataSource.accept(RecipeList.items)
} catch {
print("Failed")
}
}
I try alot solution from multiple sources, seems does not work. This is my first day learning RXSwift. Usually i didnt use RXSwift, i just simply, create variable for filtered data, then rereload the tableView
The missing bit in your code is that you are just emitting the RecipeList.items and then completing. You have to emit a new value from recipeList every time you want the table view to reload.
Try something like this instead:
class Example: UIViewController {
var tableView: UITableView!
var textField: UITextField!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let pickerView = createPickerView()
textField.inputView = pickerView
configureTextField(pickerView: pickerView)
configureTableView(pickerView: pickerView)
}
func createPickerView() -> UIPickerView {
let pickerView = UIPickerView()
Observable.just(RecipeType.items)
.bind(to: pickerView.rx.itemTitles) { row, element in
return element.name
}
.disposed(by: disposeBag)
return pickerView
}
func configureTextField(pickerView: UIPickerView) {
pickerView.rx.itemSelected
.map { RecipeType.items[$0.row].name }
.bind(to: textField.rx.text)
.disposed(by: disposeBag)
}
func configureTableView(pickerView: UIPickerView) {
let recipeList = Observable.just(RecipeList.items)
Observable.combineLatest(recipeList, pickerView.rx.itemSelected.map { $0.row })
.map { (list, selected) -> [RecipeList] in
guard selected != 0 else { return list }
let recipeType = RecipeType.items[selected]
return list.filter { $0.catid == recipeType.id }
}
.bind(to: tableView.rx.items(cellIdentifier: RecipeListCell.Identifier,
cellType: RecipeListCell.self)) { row, recipe, cell in
cell.configureWithRecipe(recipe: recipe)
}
.disposed(by: disposeBag)
}
}

RxDatasource in RxSwift reload animation don't update data source

RxDatasource in RxSwift [RxTableViewSectionedAnimatedDataSource] Reload Animation don't update data source. What mistake I am doing? I am even unable to bind my action with button properly.
TableDataSource and Table editing commands
struct TableDataSource {
var header: String
var items: [Item]
var SectionViewModel: SectionViewModel
}
extension TableDataSource: AnimatableSectionModelType {
var identity: String {
return header
}
type alias Item = Data
init(original: TableDataSource, items: [Item]) {
self = original
self.items = items
self.sectionViewModel = original.sectionViewModel
}
}
enum TableViewEditingCommand {
case deleteSingle(IndexPath)
case clearAll(IndexPath)
case readAll(IndexPath)
}
struct SectionedTableViewState {
var sections: [TableDataSource]
init(sections: [TableDataSource]) {
self.sections = sections
}
func execute(command: TableViewEditingCommand) -> SectionedTableViewState {
var sections = self.sections
switch command {
// Delete single item from datasource
case .deleteSingle(let indexPath):
var items = sections[indexPath.section].items
items.remove(at: indexPath.row)
if items.count <= 0 {
sections.remove(at: indexPath.section)
} else {
sections[indexPath.section] = TableDataSource(
original: sections[indexPath.section],
items: items) }
// Clear all item from datasource with isUnread = false
case .clearAll(let indexPath):
sections.remove(at: indexPath.section)
// Mark all item as read in datasource with isUnread = true
case .readAll(let indexPath):
var items = sections[indexPath.section].items
items = items.map { var unread = $0
if $0.isUnRead == true { unreadData.isUnRead = false }
return unreadData
}
sections.remove(at: indexPath.section)
if sections.count > 0 {
let allReadItems = sections[indexPath.section].items + items
sections[indexPath.section] = TableDataSource(
original: sections[indexPath.section],
items: allReadItems)
}
}
return SectionedTableViewState(sections: sections)
}
}
This is my controller and its extensions
class ViewController: UIViewController, Storyboardable {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var closeButton: UIButton!
#IBOutlet weak var titleText: UILabel!
var viewModel: ViewModel!
let disposeBag = DisposeBag()
let sectionHeight: CGFloat = 70
let dataSource = ViewController.dataSource()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
bindInitials()
bindDataSource()
bindDelete()
}
private func bindInitials() {
tableView.delegate = nil
tableView.rx.setDelegate(self)
.disposed(by: disposeBag)
registerNibs()
}
private func registerNibs() {
let headerNib = UINib.init(
nibName: TableViewSection.identifier,
bundle: nil)
tableView.register(
headerNib,
forHeaderFooterViewReuseIdentifier: TableViewSection.identifier)
}
}
extension ViewController: Bindable {
func bindViewModel() {
bindActions()
}
private func bindDataSource() {
bindDelete()
// tableView.dataSource = nil
// Observable.just(sections)
// .bind(to: tableView.rx.items(dataSource: dataSource))
// .disposed(by: disposeBag)
}
private func bindDelete() {
/// TODO: to check and update delete code to work properly to sink with clear all and mark all as read
guard let sections = self.viewModel?.getSections() else {
return
}
let deleteState = SectionedTableViewState(sections: sections)
let deleteCommand = tableView.rx.itemDeleted.asObservable()
.map(TableViewEditingCommand.deleteSingle)
tableView.dataSource = nil
Observable.of(deleteCommand)
.merge()
.scan(deleteState) {
(state: SectionedTableViewState,
command: TableViewEditingCommand) -> SectionedTableViewState in
return state.execute(command: command) }
.startWith(deleteState) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
private func bindActions() {
guard let openDetailsObserver = self.viewModel?.input.openDetailsObserver,
let closeObserver = self.viewModel?.input.closeObserver else {
return
}
viewModel.output.titleTextDriver
.drive(titleText.rx.text)
.disposed(by: disposeBag)
// viewModel.input.dataSourceObserver
// .mapObserver({ (result) -> [Data] in
// return result
// })
// .disposed(by: disposeBag)
/// Close button binding with closeObserver
closeButton.rx.tap
.bind(to: (closeObserver))
.disposed(by: disposeBag)
/// Tableview item selected binding with openDetailsObserver
tableView.rx.itemSelected
.map { indexPath in
return (self.dataSource[indexPath.section].items[indexPath.row])
}.subscribe(openDetailsObserver).disposed(by: disposeBag)
}
}
extension ViewController: UITableViewDelegate {
static func dataSource() -> RxTableViewSectionedAnimatedDataSource<TableDataSource> {
return RxTableViewSectionedAnimatedDataSource(
animationConfiguration: AnimationConfiguration(insertAnimation: .fade,
reloadAnimation: .fade,
deleteAnimation: .fade),
configureCell: { (dataSource, table, idxPath, item) in
var cell = table.dequeueReusableCell(withIdentifier: TableViewCell.identifier) as? TableViewCell
let cellViewModel = TableCellViewModel(withItem: item)
cell?.setViewModel(to: cellViewModel)
return cell ?? UITableViewCell()
}, canEditRowAtIndexPath: { _, _ in return true })
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard var headerView = tableView.dequeueReusableHeaderFooterView(
withIdentifier: TableViewSection.identifier)
as? TableViewSection
else { return UITableViewHeaderFooterView() }
let viewModel = self.dataSource[section].sectionViewModel
headerView.setViewModel(to: viewModel)
headerView.dividerLine.isHidden = section == 0 ? true : false
headerView.section = section
let data = TableViewEditingCommand.clearAll(IndexPath(row: 0, section: section ?? 0))
// /// Section button binding with closeObserver
// headerView.sectionButton.rx.tap
// .map(verseNum -> TableViewEditingCommand in (TableViewEditingCommand.deleteSingle))
// .disposed(by: disposeBag)
headerView.sectionButtonTappedClosure = { [weak self] (section, buttonType) in
if buttonType == ButtonType.clearAll {
self?.showClearAllConfirmationAlert(section: section, buttonType: buttonType)
} else {
self?.editAction(section: section, buttonType: buttonType)
}
}
return headerView
}
func editAction(section: Int, buttonType: ButtonType) {
var sections = self.dataSource.sectionModels
let updateSection = (sections.count == 1 ? 0 : section)
switch buttonType {
/// Clear all
case .clearAll:
sections.remove(at: updateSection)
let data = SectionedTableViewState(sections: sections)
self.tableView.dataSource = nil
Observable.of(data)
.startWith(data) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
/// Mark all as read
case .markAllAsRead:
if updateSection == 0 { sections = self.viewModel.getSections() }
var items = sections[updateSection].items
items = items.map { var unread = $0
if $0.isUnRead == true { unread.isUnRead = false }
return unread
}
sections.remove(at: updateSection)
let allReadItems = sections[updateSection].items + items
sections[updateSection] = TableDataSource(
original: sections[updateSection],
items: allReadItems)
let data = SectionedTableViewState(sections: sections)
self.tableView.dataSource = nil
Observable.of(data)
.startWith(data) .map { $0.sections }
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
}
func showClearAllConfirmationAlert(section: Int, buttonType: ButtonType) {
let alert = UIAlertController(title: "Clear All",
message: "Are you sure, you want to clear all Items?",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { action in
switch action.style{
case .default:
self.editAction(section: section, buttonType: buttonType)
case .cancel: break
case .destructive: break
default:break
}}))
let cancel = UIAlertAction(title: "Cancel", style: .default, handler: { action in
})
alert.addAction(cancel)
self.present(alert, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return sectionHeight
}
}
View model for controller
class ViewModel {
private enum Constants {
static let titleText = "Test".localized
static let testHistoryHeaderText = "test".localized
static let unreadHeaderText = "Unread".localized
}
struct Input {
let dataSourceObserver: AnyObserver<[Data]>
let openDetailsObserver: AnyObserver<Data>
let closeObserver: AnyObserver<Void>
let sectionButtonTappedObserver: AnyObserver<IndexPath>
}
struct Output {
let titleTextDriver: Driver<String>
let dataSourceDriver: Driver<[Data]>
let viewComplete: Observable<DataCoordinator.Event>
}
let input: Input
let output: Output
private let dataSourceSubject = PublishSubject<[Data]>()
private let closeSubject = PublishSubject<Void>()
private let openDetailsSubject = BehaviorSubject<Data>(value:Data())
private let sectionButtonTappedSubject = PublishSubject<IndexPath>()
private let disposeBag = DisposeBag()
init(withRepository repository: Repository) {
input = Input(
dataSourceObserver: dataSourceSubject.asObserver(),
openDetailsObserver: openDetailsSubject.asObserver(),
closeObserver: closeSubject.asObserver(), sectionButtonTappedObserver: sectionButtonTappedSubject.asObserver()
)
let closeEventObservable = closeSubject.asObservable().map { _ in
return Coordinator.Event.goBack
}
let openDetailsEventObservable = openDetailsSubject.asObservable().map { _ in
return Coordinator.Event.goToDetails
}
let viewCompleteObservable = Observable.merge(closeEventObservable, openDetailsEventObservable)
let list = ViewModel.getData(repository: repository)
output = Output(
titleTextDriver: Observable.just(Constants.titleText).asDriver(onErrorJustReturn: Constants.titleText),
dataSourceDriver: Observable.just(list).asDriver(onErrorJustReturn: list),
viewComplete: viewCompleteObservable)
}
///TODO: To be updated as per response after API integration
static func getData(repository: Repository) -> [Data] {
return repository.getData()
}
func getSections() -> [TableDataSource] {
let List = ViewModel.getData(repository: Repository())
let unread = list.filter { $0.isUnRead == true }
let read = list.filter { $0.isUnRead == false }
let headerText = String(format:Constants.unreadHeaderText, unread.count)
let sections = [TableDataSource(
header: headerText,
items: unread,
sectionViewModel: SectionViewModel(
withHeader: headerText,
buttonType: ButtonType.markAllAsRead.rawValue,
items: unread)),
TableDataSource(
header: Constants.historyHeaderText,
items: read,
SectionViewModel: SectionViewModel(
withHeader: Constants.historyHeaderText,
buttonType: ButtonType.clearAll.rawValue,
items: read))]
return sections
}
}
i think your problem is with :
var identity: String {
return header
}
make a uuid and pass it to identity:
let id = UUID().uuidString
var identity: String {
return id
}

RxSwift and MVVM: observable not executing without binding

I'm new to RxSwift and trying implement app that using MVVM architecture. I have view model:
class CategoriesViewModel {
fileprivate let api: APIService
fileprivate let database: DatabaseService
let categories: Results<Category>
// Input
let actionRequest = PublishSubject<Void>()
// Output
let changeset: Observable<(AnyRealmCollection<Category>, RealmChangeset?)>
let apiSuccess: Observable<Void>
let apiFailure: Observable<Error>
init(api: APIService, database: DatabaseService) {
self.api = api
self.database = database
categories = database.realm.objects(Category.self).sorted(byKeyPath: Category.KeyPath.name)
changeset = Observable.changeset(from: categories)
let requestResult = actionRequest
.flatMapLatest { [weak api] _ -> Observable<Event<[Category]>> in
guard let strongAPI = api else {
return Observable.empty()
}
let request = APIService.MappableRequest(Category.self, resource: .categories)
return strongAPI.mappedArrayObservable(from: request).materialize()
}
.shareReplayLatestWhileConnected()
apiSuccess = requestResult
.map { $0.element }
.filterNil()
.flatMapLatest { [weak database] newObjects -> Observable<Void> in
guard let strongDatabase = database else {
return Observable.empty()
}
return strongDatabase.updateObservable(with: newObjects)
}
apiFailure = requestResult
.map { $0.error }
.filterNil()
}
}
and I have following binginds in view controller:
viewModel.apiSuccess
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.apiFailure
.map { _ in false }
.bind(to: refreshControl.rx.isRefreshing)
.disposed(by: disposeBag)
But if I comment bindings, part with database updating stops executing. I need to make it execute anyway, without using dispose bag in the view model. Is it possible?
And little additional question: should I use weak-strong dance with api/database and return Observable.empty() like in my view model code or can I just use unowned api/unowned database safely?
Thanks.
UPD:
Function for return observable in APIService:
func mappedArrayObservable<T>(from request: MappableRequest<T>) -> Observable<[T]> {
let jsonArray = SessionManager.jsonArrayObservable(with: request.urlRequest, isSecured: request.isSecured)
return jsonArray.mapResponse(on: mappingSheduler, { Mapper<T>().mapArray(JSONArray: $0) })
}
Work doesn't get done unless there is a subscriber prepared to receive the results.
Your DatabaseService needs to have a dispose bag in it and subscribe to the Observable<[Category]>. Something like:
class ProductionDatabase: DatabaseService {
var categoriesUpdated: Observable<Void> { return _categories }
func updateObservable(with categories: Observable<[Category]>) {
categories
.subscribe(onNext: { [weak self] categories in
// store categories an then
self?._categories.onNext()
})
.disposed(by: bag)
}
private let _categories = PublishSubject<Void>()
private let bag = DisposeBag()
}
Then apiSuccess = database.categoriesUpdated and database.updateObservable(with: requestResult.map { $0.element }.filterNil())

Correct usage of RxSwift with TableView

I don't know how transfer the data between ModelView and ViewController. In
SelectModelViewController
class SelectModelViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var errorLabel: UILabel!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
var markViewModel : MarkViewModel?
let markService = MarkService()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
markViewModel = MarkViewModel(markService: markService)
markViewModel?.data.asObservable()
.bindTo(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (_, element, cell) in
cell.textLabel?.text = element
}
.addDisposableTo(disposeBag)
}
}
MarkViewModel has an error. I'am doing something wrong
struct MarkViewModel {
let markService: MarkService
var data: Driver<[Mark]>
init(markService: MarkService) {
self.markService = markService
data = markService.get()
.map { result in
switch result {
case.success(let marks):
return marks.map { mark in
return mark
}
case .error(let error):
print(error)
}
}.asDriver(onErrorJustReturn: .error(.other))
}}
MarkService
struct MarkService {
func get() -> Observable<Result<[Mark]>> {
return URLSession.shared.rx.json(url: URL(string: API.BaseURL)!)
.retry(3)
.map {
var marks = [Mark]()
guard let json = $0 as? [String: Any],
let items = json["RBMarks"] as? [[String : Any]] else {
return .error(.badJSON)
}
for item in items {
if let mark = Mark(json: item) {
marks.append(mark)
} else {
return .error(.badJSON)
}
}
return .success(marks)
}
.catchErrorJustReturn(.error(.noInternet))
}}
First, we can return Observable<Result<[Mark]>> from MarkService instead of Any. This will be useful later on to display them.
struct MarkService {
func get() -> Observable<Result<[Mark]>> {
return URLSession.shared.rx.json(url: URL(string: API.BaseURL)!)
.retry(3)
.map {
var marks = [Mark]()
guard let json = $0 as? [String: Any],
let items = json["RBMarks"] as? [[String : Any]] else {
return .error(.badJSON)
}
for item in items {
if let mark = Mark(json: item) {
marks.append(mark)
} else {
return .error(.badJSON)
}
}
return .success(marks)
}
.catchErrorJustReturn(.error(.noInternet))
}
}
Then, let's remove the subscription to data in MarkViewModel. We'll also transform the marks to something that is more suited for presentation.
struct MarkViewModel {
let markService: MarkService
var data: Driver<[String]>
var marks: Variable<[Mark]>
let disposeBag = DisposeBag()
init(markService: MarkService) {
self.markService = markService
data = markService.get()
.map { result in
guard case .success(let marks) = result else {
return ["Error while getting marks"]
}
return marks.map { mark in
"For assignment \(mark.assignmentName), you were marked with \(mark.grade)/10"
}
}
.asDriver(onErrorJustReturn: .error(.other))
}
}
Now, in view controller, we can use RxSwift's table view bindings to display those data
let disposeBag = DisposeBag()
func viewDidLoad() {
viewModel.data
.bindTo(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (_, element, cell) in
cell.textLabel?.text = element
}
.addDisposableTo(disposeBag)
}
This is obviously only an example of how you could do it and code will change depending on the requirements for specific views.