UISearchBar in UITableView by Rxswift - swift

I use RxSwift to show list of Persons in my tableview, and my tableview has two sections, the first one is old searches and the second one is all Persons. now I don't know how should I filter Persons when users type a name on UISearchBar's textfield.
This is my Person model:
struct PersonModel {
let name: String
let family:String
let isHistory:Bool
}
This is my ContactsViewModel
struct SectionOfPersons {
var header: String
var items: [Item]
}
extension SectionOfPersons: SectionModelType {
typealias Item = PersonModel
init(original: SectionOfPersons, items: [SectionOfPersons.Item]) {
self = original
self.items = items
}
}
class ContactsViewModel {
let items = PublishSubject<[SectionOfPersons]>()
func fetchData(){
var subItems : [SectionOfPersons] = []
subItems.append( SectionOfPersons(header: "History", items: [
SectionOfPersons.Item(name:"Michelle", family:"Obama", isHistory:true ),
SectionOfPersons.Item(name:"Joanna", family:"Gaines", isHistory:true )
]))
subItems.append( SectionOfPersons(header: "All", items: [
SectionOfPersons.Item(name:"Michelle", family:"Obama", isHistory:false ),
SectionOfPersons.Item(name:"James", family:"Patterson", isHistory:false ),
SectionOfPersons.Item(name:"Stephen", family:"King", isHistory:false ),
SectionOfPersons.Item(name:"Joanna", family:"Gaines", isHistory:false )
]))
self.items.onNext( subItems )
}
}
and this is my ContactsViewController:
class ContactsViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var searchBar: UISearchBar!
private lazy var dataSource = RxTableViewSectionedReloadDataSource<SectionOfPersons>(configureCell: configureCell, titleForHeaderInSection: titleForHeaderInSection)
private lazy var configureCell: RxTableViewSectionedReloadDataSource<SectionOfPersons>.ConfigureCell = { [weak self] (dataSource, tableView, indexPath, contact) in
guard let cell = tableView.dequeueReusableCell(withIdentifier: "ContactTableViewCell", for: indexPath) as? ContactTableViewCell else { return UITableViewCell() }
cell.contact = contact
return cell
}
private lazy var titleForHeaderInSection: RxTableViewSectionedReloadDataSource<SectionOfPersons>.TitleForHeaderInSection = { [weak self] (dataSource, indexPath) in
return dataSource.sectionModels[indexPath].header
}
private let viewModel = ContactsViewModel()
private let disposeBag = DisposeBag()
var showContacts = PublishSubject<[SectionOfPersons]>()
var allContacts = PublishSubject<[SectionOfPersons]>()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
viewModel.fetchData()
}
func bindViewModel(){
tableView.backgroundColor = .clear
tableView.register(UINib(nibName: "ContactTableViewCell", bundle: nil), forCellReuseIdentifier: "ContactTableViewCell")
tableView.rx.setDelegate(self).disposed(by: disposeBag)
viewModel.items.bind(to: allContacts).disposed(by: disposeBag)
viewModel.items.bind(to: showContacts).disposed(by: disposeBag)
showContacts.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)
searchBar
.rx.text
.orEmpty
.debounce(0.5, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.filter { !$0.isEmpty }
.subscribe(onNext: { [unowned self] query in
////// if my datasource was simple string I cand do this
self.showContacts = self.allContacts.filter { $0.first?.hasPrefix(query) } // if datasource was simple array string, but what about complex custome object?!
})
.addDisposableTo(disposeBag)
}
}
Thanks for your response.

You don't need the two PublishSubjects in your ContactsViewController. You can bind the Observables you obtain from the UISearchBar and your viewModel directly to your UITableView. To filter the contacts with your query you have to filter each section separately. I used a little helper function for that.
So here is what I did
Get rid of the showContacts and allContacts properties
Create an query Observable that emits the text that the user entered into the search bar (don't filter out the empty text, we need that to bring back all contacts when the user deletes the text in the search bar)
Combine the query Observable and the viewModel.items Observable into one Observable
Use this observable to filter all contacts with the query.
Bind that Observable directly to the table view rx.items
I used combineLatest so the table view gets updated whenever the query or viewModel.items changes (I don't know if that list of all contacts is static or if you add / remove contacts).
So now your bindViewModel() code looks like this (I moved the tableView.register(...) to viewDidLoad):
func bindViewModel(){
let query = searchBar.rx.text
.orEmpty
.distinctUntilChanged()
Observable.combineLatest(viewModel.items, query) { [unowned self] (allContacts, query) -> [SectionOfPersons] in
return self.filteredContacts(with: allContacts, query: query)
}
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
Here is the function that filters all contacts using the query:
func filteredContacts(with allContacts: [SectionOfPersons], query: String) -> [SectionOfPersons] {
guard !query.isEmpty else { return allContacts }
var filteredContacts: [SectionOfPersons] = []
for section in allContacts {
let filteredItems = section.items.filter { $0.name.hasPrefix(query) || $0.family.hasPrefix(query) }
if !filteredItems.isEmpty {
filteredContacts.append(SectionOfPersons(header: section.header, items: filteredItems))
}
}
return filteredContacts
}
I assumed that you wanted to check the Persons' name and family against the query.
One more thing: I removed the debounce because you filter a list that is already in memory, which is really fast. You would typically use debounce when typing into the search bar triggers a network request.

Related

How can i use Combine with #resultbuilder to build a dynamic collectionview list?

I want to use #resultbuilder and Combine to create my own reactive and declarative UICollectionView List in UIKit, similiar to what we get with List {} in SwiftUI.
For that, i am using a resultbuilder to create a Snapshot like this:
#resultBuilder
struct SnapshotBuilder {
static func buildBlock(_ components: ListItemGroup...) -> [ListItem] {
return components.flatMap { $0.items }
}
// Support `for-in` loop
static func buildArray(_ components: [ListItemGroup]) -> [ListItem] {
return components.flatMap { $0.items }
}
static func buildFinalResult(_ component: [ListItem]) -> NSDiffableDataSourceSectionSnapshot<ListItem> {
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<ListItem>()
sectionSnapshot.append(component)
return sectionSnapshot
}
}
I also need to use the following extensions to pass ListItemGroup to SnapshotBuilder and get [ListItem]
struct ListItem: Hashable {
let title: String
let image: UIImage?
var children: [ListItem]
init(_ title: String, children: [ListItem] = []) {
self.title = title
self.image = UIImage(systemName: title)
self.children = children
}
}
protocol ListItemGroup {
var items: [ListItem] { get }
}
extension Array: ListItemGroup where Element == ListItem {
var items: [ListItem] { self }
}
extension ListItem: ListItemGroup {
var items: [ListItem] { [self] }
}
My List Class looks like this:
final class List: UICollectionView {
enum Section {
case main
}
var data: UICollectionViewDiffableDataSource<Section, ListItem>!
private var cancellables = Set<AnyCancellable>()
init(_ items: Published<[String]>.Publisher, style: UICollectionLayoutListConfiguration.Appearance = .insetGrouped, #SnapshotBuilder snapshot: #escaping () -> NSDiffableDataSourceSectionSnapshot<ListItem>) {
super.init(frame: .zero, collectionViewLayout: List.createLayout(style))
configureDataSource()
data.apply(snapshot(), to: .main)
items
.sink { newValue in
let newSnapshot = snapshot()
self.data.apply(newSnapshot, to: .main, animatingDifferences: true)
}
.store(in: &cancellables)
}
required init(coder: NSCoder) {
super.init(coder: coder)!
}
private static func createLayout(_ appearance: UICollectionLayoutListConfiguration.Appearance) -> UICollectionViewLayout {
let layoutConfig = UICollectionLayoutListConfiguration(appearance: appearance)
return UICollectionViewCompositionalLayout.list(using: layoutConfig)
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, ListItem> {
(cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.image = item.image
content.text = item.title
cell.contentConfiguration = content
}
data = UICollectionViewDiffableDataSource<Section, ListItem>(collectionView: self) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: ListItem) -> UICollectionViewCell? in
let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
cell.accessories = [.disclosureIndicator()]
return cell
}
}
}
And i am using it in my ViewControllers like this:
class DeclarativeViewController: UIViewController {
#Published var testItems: [String] = []
var collectionView: List!
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.sizeToFit()
title = "Settings"
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "plus"), style: .plain, target: self, action: #selector(addItem))
view.backgroundColor = .systemBackground
collectionView = List($testItems) {
for item in self.testItems {
ListItem(item)
}
}
collectionView.frame = view.bounds
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
}
#objc func addItem() {
testItems.append("Item \(testItems.count)")
}
}
As you can see, i initialize my List with the #Published var testItems variable. In my init() func, i setup a subscriber and store them in cancellables, so i can react on changes.
If i add an item to testItems array, the sink callback is exectued to create a new snapshot and apply them to data. It works, but i need to tap the navigation button twice, to see an item on the list. Two questions:
Why this is happen and how can i solve this? (so i only need to tap the button once to see changes in my list)
and how can i improve my code? (currently I always create a new snapshot instead of extending the already created one)
Let me answer both questions by answering your second one.
How can i improve my code? (currently I always create a new snapshot
instead of extending the already created one)
I'm a bit confused about your use of #resultBuilder. Typically one would use a result builder to create a Domain Specific Language (DSL). In this case you could create a DSL for constructing ListItems, but that would imply that you mean to populate a list at compile time, most of your code here seems to focus on updating the list, dynamically, a runtime. So using result builder seems overly complex.
In this case, you're also using a Publisher where you could probably get by using a simple didSet on your controller's property. However, a Publisher would be a really good idea as part of a more complex Model that the Controller was trying to coordinate with its views. I had a version of your code where I replaced the Publisher with didSet but on second glance - imaging the more complex model case, I put the publisher back in.
You've got your publisher's pipeline all tangled up in your result builder - which is odd because, again, publishers are about reacting dynamically to changes at runtime whereas result builders are about making nice DSLs for the syntax sugaring of compile time code.
So I pulled out the DSL, and set up a rich pipeline that makes good use of having a publisher.
Also, when using Combine publishers, it's common to use type erasure to make the actual nature of the publisher more anonymous. So in my rework, I use eraseToAnyPublisher so that List could take it's values from anyone, not just an #Published list of strings.
So List becomes:
final class List: UICollectionView {
enum Section {
case main
}
private var subscriptions = Set<AnyCancellable>()
private var data: UICollectionViewDiffableDataSource<Section, ListItem>!
init(itemPublisher: AnyPublisher<[String], Never>,
style: UICollectionLayoutListConfiguration.Appearance = .insetGrouped) {
super.init(frame: .zero, collectionViewLayout: List.createLayout(style))
configureDataSource()
itemPublisher
.map{ items in items.map { ListItem($0) }}
.map{ listItems in
var newSnapshot = NSDiffableDataSourceSectionSnapshot<ListItem>()
newSnapshot.append(listItems)
return newSnapshot
}
.sink {
newSnapshot in
self.data?.apply(newSnapshot, to: .main, animatingDifferences: true)
}
.store(in: &subscriptions)
}
required init?(coder : NSCoder) {
super.init(coder: coder)
}
private static func createLayout(_ appearance: UICollectionLayoutListConfiguration.Appearance) -> UICollectionViewLayout {
let layoutConfig = UICollectionLayoutListConfiguration(appearance: appearance)
return UICollectionViewCompositionalLayout.list(using: layoutConfig)
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, ListItem> {
(cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.image = item.image
content.text = item.title
cell.contentConfiguration = content
}
data = UICollectionViewDiffableDataSource<Section, ListItem>(collectionView: self) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: ListItem) -> UICollectionViewCell? in
let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
cell.accessories = [.disclosureIndicator()]
return cell
}
}
}
Note the rich processing pipeline that is set up for itemPublisher and that it comes into the class as AnyPublisher<[String], Never>.
Then your DeclarativeViewController becomes:
class DeclarativeViewController: UIViewController {
#Published var testItems: [String] = []
var collectionView: List!
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.sizeToFit()
title = "Settings"
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "plus"), style: .plain, target: self, action: #selector(addItem))
view.backgroundColor = .systemBackground
collectionView = List(itemPublisher: $testItems.eraseToAnyPublisher())
collectionView.frame = view.bounds
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
}
#objc func addItem() {
testItems.append("Item \(testItems.count)")
}
}
where the testItems model's publisher get erased away to an any publisher.
In my code ListItem stays the same, but all the stuff related to the #resultBuiler is gone. Maybe you could use it if you wanted to create a funciton to build a set of ListItems for the initial set of items in a table (or for a table that has static content) But it didn't seem necessary here.

How to pass chain view controller presenter with observable

I'm new in the RxSwift development and I've an issue while presentation a view controller.
My MainViewController is just a table view and I would like to present detail when I tap on a item of the list.
My DetailViewController is modally presented and needs a ViewModel as input parameter.
I would like to avoid to dismiss the DetailViewController, I think that the responsability of dismiss belongs to the one who presented the view controller, i.e the dismiss should happen in the MainViewController.
Here is my current code
DetailsViewController
class DetailsViewController: UIViewController {
#IBOutlet weak private var doneButton: Button!
#IBOutlet weak private var label: Label!
let viewModel: DetailsViewModel
private let bag = DisposeBag()
var onComplete: Driver<Void> {
doneButton.rx.tap.take(1).asDriver(onErrorJustReturn: ())
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
bind()
}
private func bind() {
let ouput = viewModel.bind()
ouput.id.drive(idLabel.rx.text)
.disposed(by: bag)
}
}
DetailsViewModel
class DetailsViewModel {
struct Output {
let id: Driver<String>
}
let item: Observable<Item>
init(with vehicle: Observable<Item>) {
self.item = item
}
func bind() -> Output {
let id = item
.map { $0.id }
.asDriver(onErrorJustReturn: "Unknown")
return Output(id: id)
}
}
MainViewController
class MainViewController: UIViewController {
#IBOutlet weak private var tableView: TableView!
private var bag = DisposeBag()
private let viewModel: MainViewModel
private var detailsViewController: DetailsViewController?
override func viewDidLoad(_ animated: Bool) {
super.viewDidLoad(animated)
bind()
}
private func bind() {
let input = MainViewModel.Input(
selectedItem: tableView.rx.modelSelected(Item.self).asObservable()
)
let output = viewModel.bind(input: input)
showItem(output.selectedItem)
}
private func showItem(_ item: Observable<Item>) {
let viewModel = DetailsViewModel(with: vehicle)
detailsViewController = DetailsController(with: viewModel)
item.flatMapFirst { [weak self] item -> Observable<Void> in
guard let self = self,
let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
self.present(detailsViewController, animated: true)
return detailsViewController.onComplete.asObservable()
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController? = nil
})
.disposed(by: bag)
}
}
MainViewModel
class MainViewModel {
struct Input {
let selectedItem: Observable<Item>
}
struct Output {
let selectedItem: Observable<Item>
}
func bind(input: Input) -> Output {
let selectedItem = input.selectedItem
.throttle(.milliseconds(500),
latest: false,
scheduler: MainScheduler.instance)
.asObservable()
return Output(selectedItem: selectedItem)
}
}
My issue is on showItem of MainViewController.
I still to think that having the DetailsViewController input as an Observable isn't working but from what I understand from Rx, we should use Observable as much as possible.
Having Item instead of Observable<Item> as input could let me use this kind of code:
item.flatMapFirst { item -> Observable<Void> in
guard let self = self else {
return Observable<Void>.never()
}
let viewModel = DetailsViewModel(with: item)
self.detailsViewController = DetailsViewController(with: viewModel)
guard let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
present(detailsViewController, animated: true)
return detailsViewController
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController = nil
})
.disposed(by: bag)
What is the right way to do this?
Thanks
You should not "use Observable as much as possible." If an object is only going to ever have to deal with a single item, then just pass the item. For example if a label is only ever going to display "Hello World" then just assign the string to the label's text property. Don't bother wrapping it in a just and binding it to the label's rx.text.
Your second option is much closer to what you should have. It's a fine idea.
You might find my CLE library interesting. It takes care of the issue you are trying to handle here.

Viewing initial data source and filtering with RxSwift using MVVM

I'm starting out with RxSwift and trying to get a simple example of filtering a data source with a UISearchController working.
I have the basic setup of a UISearchController wired into a UITableViewController. Using MVVM I also have a basic view model setup that will drive the table.
self.viewModel.searchText.accept(searchController.searchBar.text ?? "")
viewModel.listItems.bind(to: tableView.rx.items(cellIdentifier: "ItemCell")) { row, item, cell in
cell.textLabel!.text = item.name
}
.disposed(by: disposeBag)
View Model
class ListViewModel {
private let sourceItems: BehaviorRelay<[ListItem]> = BehaviorRelay(value: [
ListItem(name: "abc"),
ListItem(name: "def"),
ListItem(name: "ghi"),
ListItem(name: "jkl"),
ListItem(name: "mno")
])
let searchText = BehaviorRelay<String>(value: "")
var listItems: Observable<[ListItem]> = Observable.just([])
init() {
listItems = sourceItems.asObservable()
}
}
I can add in the search filtering and this works such that only the values matching the filter string will show
let searchObservable = searchText
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.filter { query in
return query.count > 2
}
.share(replay: 1)
listItems = Observable.combineLatest(sourceItems.asObservable(), searchObservable) { items, query in
return items.filter({ item in
item.name.lowercased().contains(query.lowercased())
})
}
However, this will not show any values until the filter is matched. What I am trying to do is initially show all the values and then only show the filtered values. I'm not quite sure how to populate the listItems when the searchText changes but is empty or events are filtered out.
You forgot to subscribe for changes, instead of
listItems = Observable.combineLatest(sourceItems.asObservable(), searchObservable) { items, query in
return items.filter({ item in
item.name.lowercased().contains(query.lowercased())
})
}
should be
Observable.combineLatest(sourceItems.asObservable(), searchObservable) { items, query in
return items.filter({ item in
item.name.lowercased().contains(query.lowercased())
})
}.subscribe(onNext: { resultArray in
print(resultArray) // here you can change your listItems
})
.disposed(by: disposeBag)
this is how to change searchText searchText.accept("123")
UPDATED:
to handle any searchBar updates you should implement serachBar.rx
Here is some example how to
import UIKit
import RxSwift
import RxCocoa
class ListItem: NSObject {
var name: String = ""
public init(name str: String) {
super.init()
name = str
}
}
class ViewController: UIViewController, UISearchBarDelegate {
#IBOutlet weak var searchBar: UISearchBar!
private let sourceItems: BehaviorRelay<[ListItem]> = BehaviorRelay(value: [
ListItem(name: "abc"),
ListItem(name: "def"),
ListItem(name: "ghi"),
ListItem(name: "jkl"),
ListItem(name: "mno")
])
let searchText = BehaviorRelay<String>(value: "")
var listItems: Observable<[ListItem]> = Observable.just([])
var disposeBag: DisposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
addSearchBarObserver()
listItems = sourceItems.asObservable()
Observable.combineLatest(sourceItems.asObservable(), searchText) { items, query in
return items.filter({ item in
item.name.lowercased().contains(query.lowercased())
})
}.subscribe(onNext: { resultArray in
print(resultArray)
})
.disposed(by: disposeBag)
}
private func addSearchBarObserver() {
searchBar
.rx
.text
.orEmpty
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.subscribe { [weak self] query in
guard
let query = query.element else { return }
self?.searchText.accept(query)
}
.disposed(by: disposeBag)
}
}
my approach will be the following
create such observable class as below
import Foundation
class Observable<Generic> {
var value: Generic {
didSet {
DispatchQueue.main.async {
self.valueChanged?(self.value)
}
}
}
private var valueChanged: ((Generic) -> Void)?
init(_ value: Generic) {
self.value = value
}
/// Add closure as an observer and trigger the closure imeediately if fireNow = true
func addObserver(fireNow: Bool = true, _ onChange: ((Generic) -> Void)?) {
valueChanged = onChange
if fireNow {
onChange?(value)
}
}
func removeObserver() {
valueChanged = nil
}
}
have in your VM the original list
create different filtered list as below
var cellViewModels: Observable<[/* your model */]?>
have the search bar delegate method in the View as below - remember it will vary depending on your implementation -
class TradeDealsViewController: UIViewController, UISearchBarDelegate {
// Mark :- IB Outlets
#IBOutlet weak var collectionView: UICollectionView!
private var viewModel: ViewModel /* your VM */?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
viewModel = TradeDealsViewModel()
bindViewLiveData()
}
private func bindViewLiveData(){
viewModel?.cellViewModels.addObserver({ [weak self] (responsee) in
self?.collectionView.reloadData()
})
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
/* do your filtration logic here */
}
}
note that this solution does not use RxSwift it uses only foundation
With thanks to #AlexandrKolesnik I managed to tweak his answer and get it working:
class ListViewModel {
private let sourceItems: BehaviorRelay<[ListItem]> = BehaviorRelay(value: [
ListItem(name: "abc"),
ListItem(name: "def"),
ListItem(name: "ghi"),
ListItem(name: "jkl"),
ListItem(name: "mno"),
ListItem(name: "abcdef")
])
private var filteredItems: BehaviorRelay<[ListItem]> = BehaviorRelay(value: [])
let searchText = BehaviorRelay<String>(value: "")
var listItems: Observable<[ListItem]> {
return filteredItems.asObservable()
}
private let disposeBag = DisposeBag()
init() {
searchText
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.subscribe({ [unowned self] event in
guard let query = event.element, !query.isEmpty else {
self.filteredItems.accept(self.sourceItems.value)
return
}
self.filteredItems.accept(
self.sourceItems.value.filter {
$0.name.lowercased().contains(query.lowercased())
}
)
})
.disposed(by: disposeBag)
}
}

RxSwift - How to reflect the number of item's count to TableView

I'm new to RxSwift. This is quite tricky.
I'm creating like ToDoList that views which are tableView and add-item view are separated by TabBarController.
I have successfully displayed the list array and added a new item into tableView.
I also wanted to display the number of array's count and favourite count in the view that has tableView so that I have displayed it by throwing a value with .just.
But displaying a value based on the result of the array displayed by SearchBar, the value is not reflected as I expected.
In MainViewModel, I made sure if I could get the number of array's count properly by print, but apparently the value was fine.
It is just not reflected in the View.
// Model
struct Item: Codable {
var name = String()
var detail = String()
var tag = String()
var memo = String()
var fav = Bool()
var cellNo = Int()
init(name: String, detail: String, tag: String, memo: String, fav: Bool, celllNo: Int) {
self.name = name
self.detail = detail
self.tag = tag
self.memo = memo
self.fav = fav
self.cellNo = celllNo
}
init() {
self.init(
name: "Apple",
detail: "ringo",
tag: "noun",
memo: "",
fav: false,
celllNo: 0
)
}
}
struct SectionModel: Codable {
var list: [Item]
}
extension SectionModel: SectionModelType {
var items: [Item] {
return list
}
init(original: SectionModel, items: [Item]) {
self = original
self.list = items
}
}
Singleton share class
final class Sharing {
static let shared = Sharing()
var items: [Item] = [Item()]
var list: [SectionModel] = [SectionModel(list: [Item()])] {
didSet {
UserDefault.shared.saveList(list: list)
}
}
let listItems = BehaviorRelay<[SectionModel]>(value: [])
}
extension Sharing {
func calcFavCount(array: [Item]) -> Int {
var count = 0
if array.count > 0 {
for i in 0...array.count - 1 {
if array[i].fav {
count += 1
}
}
}
return count
}
}
// MainTabViewController
class MainTabViewController: UIViewController {
#IBOutlet weak var listTextField: UITextField!
#IBOutlet weak var tagTextField: UITextField!
#IBOutlet weak var itemCountLabel: UILabel!
#IBOutlet weak var favCountLabel: UILabel!
#IBOutlet weak var favIcon: UIImageView!
#IBOutlet weak var infoButton: UIButton!
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var tableView: UITableView!
private lazy var viewModel = MainTabViewModel(
searchTextObservable: searchTextObservable
)
private let disposeBag = DisposeBag()
private var dataSource: RxTableViewSectionedReloadDataSource<SectionModel>!
override func viewDidLoad() {
super.viewDidLoad()
setupTableViewDataSource()
tableViewSetup()
listDetailSetup()
}
// create Observable searchBar.text to pass to ViewModel
var searchTextObservable: Observable<String> {
let debounceValue = 200
// observable to get the incremental search text
let incrementalTextObservable = rx
.methodInvoked(#selector(UISearchBarDelegate.searchBar(_:shouldChangeTextIn:replacementText:)))
.debounce(.milliseconds(debounceValue), scheduler: MainScheduler.instance)
.flatMap { [unowned self] _ in Observable.just(self.searchBar.text ?? "") }
// observable to get the text when the clear button or enter are tapped
let textObservable = searchBar.rx.text.orEmpty.asObservable()
// merge these two above
let searchTextObservable = Observable.merge(incrementalTextObservable, textObservable)
.skip(1)
.debounce(.milliseconds(debounceValue), scheduler: MainScheduler.instance)
.distinctUntilChanged()
return searchTextObservable
}
func setupTableViewDataSource() {
dataSource = RxTableViewSectionedReloadDataSource<SectionModel>(configureCell: {(_, tableView, indexPath, item) in
let cell = tableView.dequeueReusableCell(withIdentifier: "ListCell") as! ListCell
cell.selectionStyle = .none
cell.backgroundColor = .clear
cell.configure(item: item)
return cell
})
}
func tableViewSetup() {
tableView.rx.itemDeleted
.subscribe {
print("delete")
}
.disposed(by: disposeBag)
viewModel.dispItems.asObservable()
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
func listDetailSetup() {
viewModel.itemCountObservable
.bind(to: itemCountLabel.rx.text)
.disposed(by: disposeBag)
viewModel.favCountObservable
.bind(to: favCountLabel.rx.text)
.disposed(by: disposeBag)
}
}
MainTabViewModel
final class MainTabViewModel {
private let disposeBag = DisposeBag()
private let userDefault: UserDefaultManager
var dispItems = BehaviorRelay<[SectionModel]>(value: [])
private let shared = Sharing.shared
// lazy var itemCount = shared.list[0].list.count
// lazy var favCount = shared.calcFavCount
var itemCountObservable: Observable<String>
var favCountObservable: Observable<String>
init(searchTextObservable: Observable<String>,
userDefault: UserDefaultManager = UserDefault()) {
self.userDefault = userDefault
let initialValue = shared.list
shared.listItems.accept(initialValue)
dispItems = shared.listItems
// this part is to display the initil number -> success
var itemCount = shared.list[0].list.count
itemCountObservable = .just(itemCount.description + " items")
var favCount = shared.calcFavCount(array: shared.list[0].list)
favCountObservable = .just(favCount.description)
// this part is based on the searching result -> failure
searchTextObservable.subscribe(onNext: { text in
if text.isEmpty {
let initialValue = self.shared.list
self.shared.listItems.accept(initialValue)
self.dispItems = self.shared.listItems
}else{
let filteredItems: [Item] = self.shared.list[0].list.filter {
$0.name.contains(text)
}
let filteredList = [SectionModel(list: filteredItems)]
self.shared.listItems.accept(filteredList)
self.dispItems = self.shared.listItems
itemCount = filteredItems.count
self.itemCountObservable = .just(itemCount.description + " items")
favCount = self.shared.calcFavCount(array: filteredItems)
self.favCountObservable = .just(favCount.description)
print("\(itemCount) items") // the ideal number is in but not shown in the view
}
})
.disposed(by: disposeBag)
}
}
I removed unnecessary code but I mostly pasted a whole code for your understanding.
Hope you could help me.
Thank you.
I solved this issue anyway; the value was reflected.
the issue was that itemCountObservable was declared as observable and .just was used.
How .just works is to throw onNext once and it is completed, which means the change I did in searchTextObservable.subscribe(onNext~ is unacceptable.
So I shifted the itemCountObservable: Observable<String> to BehaviorRelay<String>that only onNext is thrown and not completed, then it works.
My understanding of this issue is that itemCountObservable: Observable<String> stopped throwing a value due to .just as I've written above.
Am I correct??
If you're familiar with the difference between Observable and BehaviorRelay, it would be appreciated if you could tell me.
Thanks.

make dynamic UIbutton appear and dissapear based on number of items in table view

I'm currently trying to make a dynamic UIbutton appear and disappear based on number of items loaded into a table view, fetched from a backend url. I want to button to appear if there's 12 or more items loaded into the table view and not appear if there's less than 12. Any ideas on the best way to handle this?
import UIKit
import RxSwift
import RxCocoa
public class AllProvidersPickerViewController: InputableTableViewController, ViewModelHolder {
#IBOutlet private(set) weak var searchBar: UISearchBar!
#IBOutlet weak var dontSeeProviderButton: UIButton!
var viewModel: AllProvidersPickerViewModel! = nil
private let bag = DisposeBag()
override public func viewDidLoad() {
super.viewDidLoad()
setupRx()
}
private func setupRx() {
viewModel.shownProviders
.bind(to: tableView.rx.items(cellIdentifier: "ProviderCell")) { _, mvpd, cell in
cell.textLabel?.text = mvpd.displayName
}
.addDisposableTo(bag)
tableView
.rx
.modelSelected(MVPD.self)
.bind(to: viewModel.selectedProvider)
.addDisposableTo(bag)
searchBar
.rx.text
.orEmpty
.bind(to: viewModel.searchQuery)
.addDisposableTo(bag)
dontSeeProviderButton
.rx.tap
.bind(to: viewModel.tappedDontSeeProvider)
.addDisposableTo(bag)
}
}
private extension MVPD {
var displayName: String {
return self.names.first ?? ""
}
}
XFreire's answers are fine, or you could do:
viewModel.shownProviders
.map { $0.count < 12 }
.bind(to: dontSeeProviderButton.rx.isHidden)
.disposed(by: bag)
Make sure shownProviders can handle being subscribed to without having to re-send any network requests or whatever. You might need shareReplayLatestWhileConnected() for that.
I have been asked to explain this code... I will do so by breaking it down...
let shownProviders = viewModel.shownProviders
At this point, I know that shownProviders is an array. I don't know much about the type that the array contains because that info wasn't in the question, but I don't need to know
let shownProviders = viewModel.shownProviders
let shouldHideButton = shownProviders.map { $0.count < 12 }
In the above line, I know that $0 is an array and I know that the button should hide if there are fewer than 12 items in the array. $0.count < 12 returns a Bool. map will transform the shownProviders Observable into whatever the block returns, so I know that shouldHideButton is an Observable<Bool>.
let shownProviders = viewModel.shownProviders
let shouldHideButton = shownProviders.map { $0.count < 12 }
let disposable = shouldHideButton.bind(to: dontSeeProviderButton.rx.isHidden)
The above line of code binds the result of shouldHideButton to the isHidden property of the button. It returns a disposable.
let shownProviders = viewModel.shownProviders
let shouldHideButton = shownProviders.map { $0.count < 12 }
let disposable = shouldHideButton.bind(to: dontSeeProviderButton.rx.isHidden)
disposable.disposed(by: bag)
This last line ensures that the binding will be broken when the view controller goes out of scope.
Simplest way:
viewModel.shownProviders
.subscribe(onNext: { [weak self] items in
if items.count < 12 {
self?.viewAllProvidersButton.isHidden = true
}
else {
self?.viewAllProvidersButton.isHidden = false
}
})
.addDisposableTo(bag)
Other way could be to create a property buttonVisibilityObserver of type AnyObserver and bind it to viewModel.shownProviders. Something like this (not tested):
var buttonVisibilityObserver: AnyObserver<[ItemsType]> {
return UIBindingObserver(UIElement: viewAllProvidersButton) { button, items in
button.isHidden = items.count < 12 ? true : false
}.asObserver()
}
And then in your setupRx():
viewModel.shownProviders
.bind(to: buttonVisibilityObserver)
.addDisposableTo(bag)