Search code examples
iosswiftuirefreshcontrolrx-swift

Binding to a UIRefreshControl after refresh using RxSwift


I have a TableView of notification. I want to refresh by pull to refresh using UIRefreshControl. How to do that with rx-swift? This is my code. Why tableView not refreshed after I set value to variable data

var refreshControl = UIRefreshControl()
var disposeBag = DisposeBag()

let loadingData = ActivityIndicator()

var data: Observable<[Notification]>!

override func viewDidLoad() {
    super.viewDidLoad()

    self.view = v

    v.tableView.registerClass(NotificationsViewCell.self, forCellReuseIdentifier: "Cell")
    v.tableView.addSubview(refreshControl)
    data = getNotifications()

    configureTableDataSource()
    configureActivityIndicatorsShow()

    refreshControl.rx_controlEvent(.ValueChanged)
        .flatMapLatest{ [unowned self] _ in
            return self.getNotifications()
            .trackActivity(self.loadingData)
        }.subscribe(
            onNext: {notification in
                print("success")
                self.data = Observable.just(notification) // NOT REFRESH TABLEVIEW
            },
            onError: { error in
                print("Error \(error)")
            },
            onCompleted: {() in
                print("complete")
            },
            onDisposed: {() in
                print("disposed")
            })
        .addDisposableTo(disposeBag)
}


func configureTableDataSource(){
    data
        .retry(3)
        .doOnError{ [weak self] error in
            self?.v.emptyLabel.hidden = false
            self?.v.retryButton.hidden = false
        }
        .doOnNext{ [weak self] result in
            if result.count == 0 {
                self?.v.emptyLabel.hidden = false
                self?.v.emptyLabel.text = "Tidak ada bisnis favorit"
            } else {
                self?.v.emptyLabel.hidden = true
                self?.v.retryButton.hidden = true
            }
        }
        .trackActivity(loadingData)
        .retryWhen{ _ in
            self.v.retryButton.rx_tap
        }
        .asDriver(onErrorJustReturn: [])
        .map{ results in
            results.map(NotificationsViewModel.init)
        }
        .drive(v.tableView.rx_itemsWithCellIdentifier("Cell", cellType: NotificationsViewCell.self)) { (index, viewModel, cell) in
            cell.viewModel = viewModel

            let tap = UITapGestureRecognizer(target: self, action: #selector(self.goToProfile(_:)))
            tap.numberOfTapsRequired = 1
            cell.photo.tag = index
            cell.photo.addGestureRecognizer(tap)
        }
        .addDisposableTo(disposeBag)
}

func configureActivityIndicatorsShow(){
    loadingData
        .driveNext{ isLoading in
            if !isLoading {
                self.v.indicatorView.stopAnimating()
            } else {
                self.v.indicatorView.startAnimating()
                self.v.retryButton.hidden = true
                self.v.emptyLabel.hidden = true
            }
        }
        .addDisposableTo(disposeBag)

    loadingData.asObservable()
        .bindTo(refreshControl.rx_refreshing)
        .addDisposableTo(disposeBag)
}

func getNotifications() -> Observable<[Notification]> {
    let parameters = [
        "token": NSUserDefaults.standardUserDefaults().objectForKey("token")! as! String
    ]
    return string(.POST, NOTIFICATION_LIST, parameters: parameters)
        .map { json in
            return Notification.parseJSON(JSON.parse(json)["notifications"])
        }
        .observeOn(MainScheduler.instance)
}

EDIT::

var data = Variable<[Notification]>([])

override func viewDidLoad() {
    getNotifications()
        .retry(3)
        .doOnError{ [weak self] error in
            self?.v.emptyLabel.hidden = false
            self?.v.retryButton.hidden = false
        }
        .doOnNext{ [weak self] result in
            if result.count == 0 {
                self?.v.emptyLabel.hidden = false
                self?.v.emptyLabel.text = "Tidak ada notifikasi"
            } else {
                self?.v.emptyLabel.hidden = true
                self?.v.retryButton.hidden = true
            }
        }
        .trackActivity(loadingData)
        .retryWhen{ _ in
            self.v.retryButton.rx_tap
        }
        .bindTo(data)
        .addDisposableTo(disposeBag)


    refreshControl.rx_controlEvent(.ValueChanged)
        .flatMapLatest{ [unowned self] _ in
            return self.getNotifications()
                .doOnError{ [weak self] error in 
                    // This not call after the second pull to refresh if No network connection, so refresh control still appear
                    self?.refreshControl.endRefreshing()  
                }
                .doOnCompleted{ [weak self] result in
                    self?.refreshControl.endRefreshing()
                }
        }.bindTo(data)
        .addDisposableTo(disposeBag)
}

func configureTableDataSource(){
    datas.asObservable()
        .asDriver(onErrorJustReturn: [])
        .map{ results in
            results.map(NotificationsViewModel.init)
        }
        .drive(v.tableView.rx_itemsWithCellIdentifier("Cell", cellType: NotificationsViewCell.self)) { (index, viewModel, cell) in
            cell.viewModel = viewModel
        }
        .addDisposableTo(disposeBag)
}


func configureActivityIndicatorsShow(){
    loadingData
        .driveNext{ isLoading in
            if !isLoading {
                self.v.indicatorView.stopAnimating()
            } else {
                self.v.indicatorView.startAnimating()
                self.v.retryButton.hidden = true
                self.v.emptyLabel.hidden = true
            }
        }
        .addDisposableTo(disposeBag)
}

Solution

  • self.data = Observable.just(notification) is creating a new Observable and sending the new [Notification] element on that Observable, which no one is subscribed to.

    You should be using a Subject such as Variable.

    // instead of `var data: Observable<[Notification]>!`
    let data = Variable<[Notification]>([])
    
    // and then later, when you want to send out a new element:
    self.data.value = notification
    

    EDIT: To show you how to use this in conjunction with what you already have.

    // this will update `data` upon `refreshControl` value change
    refreshControl.rx_controlEvent(.ValueChanged)
        .flatMapLatest{ [unowned self] _ in
            return self.getNotifications()
        }
        .bindTo(data)
        .addDisposableTo(disposeBag)
    
    // this will update `loadingData` when `data` gets a new element
    data.asDriver().trackActivity(self.loadingData)
    
    // bind to your table view
    data.asDriver().drive(//.....
    

    Also, consider moving the retry and retryWhen to happen sooner, instead of happening downstream where you currently have it (in the table view binding). Instead, I think it should belong in getNotifications.