Search code examples
swiftrx-swift

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.


Solution

  • 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.