When run in Debug scheme, there is a fatal error in line 30, if you code are something like this.
https://github.com/ReactiveX/RxSwift/blob/master/RxSwift/Observables/Implementations/Sink.swift
rxFatalError("Warning: Recursive call or synchronization error!")
If I choose run scheme from Debug to Release. The fatal error won't show. But I wonder if I could do something to suppress it.
class ViewController4: UIViewController {
var v = Variable(0)
var disposeBag = DisposeBag()
var notiBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
v.asObservable()
.subscribe(onNext: { _ in
let noti = Notification(name: MyNotificationName)
NotificationCenter.default.post(noti)
})
.disposed(by: disposeBag)
NotificationCenter.default.rx.notification(MyNotificationName)
.subscribe(onNext: { [unowned self] _ in
if self.v.value == 10 { self.notiBag = DisposeBag() }
else { self.v.value += 1 } // this line cause the issue
print(self.v.value)
self.counterTextView.text! += "\(self.v.value)\n"
})
.disposed(by: notiBag)
v.value = 0
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBOutlet weak var counterTextView: UITextView!
}
let MyNotificationName = Notification.Name.init(rawValue: "My Notification Name")
This fatal error appears only in debug mode because it calls rxFataError only when you compile for debug. Its defined like this:
#if DEBUG
if AtomicIncrement(&_numberOfConcurrentCalls) > 1 {
rxFatalError("Warning: Recursive call or synchronization error!")
}
defer {
_ = AtomicDecrement(&_numberOfConcurrentCalls)
}
#endif
I just got this fatal error after an RxSwift update (3.4.0). My code updated Variable value in concurrent queue. Changed to serial queue fixed this crash.
Thierry
I have mark #thierryb 's answer as the correct one. The known correct answers are as below.
NotificationCenter.default.rx.notification(MyNotificationName)
.observeOn(MainScheduler.asyncInstance)
.subscribe(onNext: { [unowned self] _ in
if self.v.value == 10 { self.notiBag = DisposeBag() }
else { self.v.value += 1 } // this line cause the issue
print(self.v.value)
self.counterTextView.text! += "\(self.v.value)\n"
})
.disposed(by: notiBag)
or
NotificationCenter.default.rx.notification(MyNotificationName)
.subscribe(onNext: { [unowned self] _ in
DispatchQueue.main.async { [unowned self] in
if self.v.value == 10 { self.notiBag = DisposeBag() }
else { self.v.value += 1 } // this line cause the issue
print(self.v.value)
self.counterTextView.text! += "\(self.v.value)\n"
}
})
.disposed(by: notiBag)
You can have the same behavior without using the Variable.
let scheduler = SerialDispatchQueueScheduler(qos: .userInitiated)
NotificationCenter.default.rx.notification(MyNotificationName)
.take(9)
.observeOn(scheduler)
.subscribe(onNext: { _ in
let noti = Notification(name: MyNotificationName)
print("foo")
NotificationCenter.default.post(noti)
}).disposed(by: bag)
let noti = Notification(name: MyNotificationName)
NotificationCenter.default.post(noti)
Related
I found a probable leak in my code because of a unit test which tests whether a dependency is called at deinit:
func testDeinit_ShouldStopMinutesTicker() throws {
let minutesTicker = MockMinutesTicker(elapsedTicksAfterStart: [(), (), ()])
var viewModel: AppointmentViewModel? = createAppointmentViewModel(minutesTicker: minutesTicker)
viewModel = nil
XCTAssertTrue(minutesTicker.isStopCalled)
}
This test is usually green. But when I refactored this:
func selectAppointment(index: Int) {
selectedCellParamRelay.accept(index)
}
private func setupCellParamSelection() {
selectedCellParamRelay
.withLatestFrom(sortedCellParams) { ($0, $1) }
.compactMap { [weak self] index, cellParams in
guard let `self` = self,
let selectedParam = cellParams[safe: index],
self.isUpcomingAppointment(selectedParam) else { return nil }
return selectedParam
}
.bind(to: upcomingCellParamRelay)
.disposed(by: disposeBag)
}
Into this:
func selectAppointment(index: Int) {
selectedCellParamRelay.accept(index)
}
private func setupCellParamSelection() {
selectedCellParamRelay
.withLatestFrom(sortedCellParams) { ($0, $1) }
.compactMap(selectAppointment(index:from:))
.bind(to: upcomingCellParamRelay)
.disposed(by: disposeBag)
}
private func selectAppointment(
index: Int,
from cellParams: [AppointmentCellParam]
) throws -> AppointmentCellParam? {
guard let selectedParam = cellParams[safe: index],
isUpcomingAppointment(selectedParam) else { return nil }
return selectedParam
}
private func isUpcomingAppointment(_ appointment: AppointmentCellParam) -> Bool {
return appointment.status != .missed && appointment.status != .finished
}
It becomes red and the deinit is not called at all.
The setupCellParamSelection func is being called from the init to setup the event handler for selecting any appointment cell by index. The sortedCellParams is a relay that is emitted by a dependency that in turn will get the values from the backend.
Can you help me figure out what went wrong with the second code?
Thanks.
Yes, because .compactMap(selectAppointment(index:from:)) needs to hold self in order to call selectAppointment even though the function doesn't need self.
The solution here is to move isUpcomingAppointment(_:) and selectAppointment(index:from:) outside of the class or as static functions within the class.
I'm implementing the mvvm using RxSwif.
Here is what happens:
Validate Fields (write 7 characters in both textFields).
Tap the login button.
API is called which is fine.
Tap the button again.
API is not called.
I noticed that the "validObservable" changes every time that I write something in textFields which is fine.
ViewController:
var viewModel: LoginViewModelType!
let disposeBag = DisposeBag()
#IBOutlet weak var dniTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
#IBOutlet weak var logInButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
logInButton.rx
.tap
.bind(to: viewModel.inputs.logInButtonDidTap)
.disposed(by: disposeBag)
dniTextField.rx
.text
.bind(to: viewModel.inputs.dniChanged)
.disposed(by: disposeBag)
passwordTextField.rx
.text
.bind(to: viewModel.inputs.passwordChanged)
.disposed(by: disposeBag)
/*viewModel.outputs.isLoginButtonEnabled
.drive(onNext: { [weak self] isEnabled in
guard let `self` = self else { return }
self.logInButton.isEnabled = isEnabled
})
.disposed(by: disposeBag)
*/
viewModel.outputs.logIn
.drive(onNext: { [weak self] user in
guard let `self` = self else { return }
self.performSegue(withIdentifier: "showMainController", sender: user)
})
.disposed(by: disposeBag)
}
override func awakeFromNib() {
super.awakeFromNib()
viewModel = LoginViewModel()
}
ViewModel:
var inputs: LoginViewModelInputs { return self }
var outputs: LoginViewModelOutputs { return self }
// ---------------------
// MARK: - Inputs
// ---------------------
var dniChanged: BehaviorRelay<String?>
var passwordChanged: BehaviorRelay<String?>
var logInButtonDidTap: PublishSubject<Void> = PublishSubject<Void>()
// ---------------------
// MARK: - Outpuds
// ---------------------
var logIn: Driver<User>
var isLoginButtonEnabled: Driver<Bool>
private let disposeBag = DisposeBag()
public init () {
dniChanged = BehaviorRelay<String?>(value: "")
passwordChanged = BehaviorRelay<String?>(value: "")
let dniObservable = dniChanged.asDriver().filterNil().asObservable()
let passwordObservable = passwordChanged.asDriver().filterNil().asObservable()
let dniValidation = dniObservable.map { $0.count > 4 }
let passwordValidation = passwordObservable.map { $0.count > 3 }
let validObservable = Observable.combineLatest(dniValidation, passwordValidation) { return $0 && $1 }.filter { $0 }
isLoginButtonEnabled = validObservable.asDriver(onErrorDriveWith: .empty())
let loginSuccessObservable = Observable.combineLatest(dniObservable, passwordObservable, validObservable) { (dni, password, valid) -> LogInRequest in
return LogInRequest(dni: dni, password: password)
}
logIn = logInButtonDidTap.withLatestFrom(loginSuccessObservable).flatMapLatest({ request -> Observable<User> in
return API.shared.post(endpoint: EndPoints.Authorize, type: User.self, body: request)
}).asDriver(onErrorDriveWith: .empty())
}
I'd say that .empty() is causing the observable to complete, and the subscription to be disposed of as a consequence.
I'd put some .debug() instructions to make sure what gets disposed and when.
I got this problem because the button size is zero
I'm trying to understand mvvm + RxSwift but I got some questions.
I'm currently using this approach which I'm not sure if is the right or can be better. How can I do to like grouping the methods, I mean, maybe something like doFirst(loading = true).doNext(getData).doLast(loading = false).catch(apiError) then subscribe to this event? It's possible?
ViewController:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
viewModel = UsersViewModel(apiService: apiService)
configureBindings()
}
func configureBindings() {
tableView.delegate = nil
tableView.dataSource = nil
viewModel.isLoading.bind(to: loadingView.rx.isAnimating)
.disposed(by: disposeBag)
viewModel.models
.bind(to: tableView.rx.items(cellIdentifier: "userCell", cellType: UserCell.self)) {(_, _, cell) in
print("Binding the cell items")
}.disposed(by: disposeBag)
tableView.rx.modelSelected(User.self).subscribe(onNext: { value in
print(value)
}).disposed(by: disposeBag)
viewModel.error.filterNil().subscribe(onNext: { (err) in
self.tableView.backgroundView = EmptyView(title: "No Users", description: "No users found")
print("Showing empty view...")
print(err)
}).disposed(by: disposeBag)
}
}
Then in my UsersViewModel:
class UsersViewModel {
var models: Observable<[User]> {
return modelsVariable.asObservable()
}
var isLoading: Observable<Bool> {
return isLoadingVariable.asObservable()
}
var error: Observable<ApiError?> {
return errorVariable.asObservable()
}
private var modelsVariable = BehaviorRelay<[User]>(value: [])
private var isLoadingVariable = BehaviorRelay<Bool>(value: false)
private var errorVariable = BehaviorRelay<ApiError?>(value: nil)
// MARK: - Data Manager
var apiService: API
required init(apiService: API) {
self.apiService = apiService
isLoadingVariable.accept(true)
apiService.GET(EndPoints.USER_LIST, type: Several<User>.self)
.subscribe(onNext: { (model) in
self.isLoadingVariable.accept(false)
self.modelsVariable.accept(model.items)
}, onError: { (err) in
self.isLoadingVariable.accept(false)
self.errorVariable.accept(err as? ApiError)
})
}
}
My 'GET' function just returns a Observable<Several<User>>.
Several:
struct Several {
var items: [User]
}
Is there any improvements that I can do?
It's a little hard to understand what you're asking, but if you're concerned about the imperative nature of your init method, and want to wrap your API call into a continuous Observable sequence that can be repeated, you could do something like this:
class UsersViewModel {
//...
var fetchUsersObserver: AnyObserver<Void> {
return fetchUsersSubject.asObserver()
}
//...
private let fetchUsersSubject = PublishSubject<Void>()
private let disposeBag = DisposeBag()
//...
required init(apiService: API) {
self.apiService = apiService
bindFetchUsers()
}
private func bindFetchUsers() {
fetchUsersSubject
.asObservable()
.do(onNext: { [weak self] _ in self?.isLoadingVariable.accept(true) })
.flatMap(self.fetchUsers)
.do(onNext: { [weak self] _ in self?.isLoadingVariable.accept(false) })
.bind(to: modelsVariable)
.disposed(by: disposeBag)
}
private func fetchUsers() -> Observable<[User]> {
return apiService
.GET(EndPoints.USER_LIST, type: Several<User>.self)
.map { $0.items }
.catchError { [weak self] error in
self?.errorVariable.accept(error as? ApiError)
return .just([])
}
}
}
Then, you need only bind a control to this AnyObserver, or send it an event manually:
func configureBindings() {
// from a control, such as UIButton
someButton
.rx
.tap
.bind(to: viewModel.fetchUsersObserver)
.disposed(by: disposeBag)
// manually
viewModel.fetchUsersObserver.onNext(())
}
Footnote 1: I typically like to make my view models structs so that I don't have to worry about all the [weak self] statements.
Footnote 2: Notice how the fetchUsers() function catches any errors thrown and does not let the error propagate to the outer Observable sequence. This is important because if this outer Observable emits an error event, it can never emit another next event.
I'm newer to RxSwift. I want to refresh the tableview to show new data.The first request that I can get the data. but when I pull down the tableview, the request didn't finished. I have no ideas about this? My code is belowing:
1: My viewController's code:
class RecommendViewController: UIViewController {
lazy var tableView = DefaultManager.createTableView(HomeImageCell.self,
HomeImageCell.idenfitier)
let disposeBag = DisposeBag()
lazy var viewModel = HomeViewModel()
lazy var dataSource: [HomeListDetailModel] = []
override func viewDidLoad() {
super.viewDidLoad()
viewModel.fetchRecommendList("answer_feed",0)
setupTableView()
configureRefresh()
bindDataToTableView()
}
func setupTableView() {
view.addSubview(tableView)
tableView.snp.makeConstraints { (make) in
make.edges.equalTo(0)
}
tableView.estimatedHeight(200)
}
func bindDataToTableView() {
viewModel.recommend
.observeOn(MainScheduler.instance)
.do(onNext: { [unowned self] model in
print("endAllRefresh")
self.endAllRefresh()
}, onError: { (error) in
self.endAllRefresh()
print("error = \(error)")
})
.map { [unowned self] model in
return self.handleData(model)
}.bind(to: tableView.rx.items(cellIdentifier: HomeImageCell.idenfitier , cellType: HomeImageCell.self )) { index, model, cell in
cell.updateCell(data: model)
}.disposed(by: disposeBag)
}
func configureRefresh() {
tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: { [unowned self] in
let model = self.dataSource[0]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
tableView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: { [unowned self] in
let model = self.dataSource[self.dataSource.count - 1]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
}
func endAllRefresh() {
self.tableView.mj_header.endRefreshing()
self.tableView.mj_footer.endRefreshing()
}
func handleData(_ model: HomeListModel) -> [HomeListDetailModel] {
guard let data = model.detailData else {
return dataSource
}
self.dataSource = data
return data
}
}
2: My ViewModel
protocol HomeProtocol {
func fetchRecommendList(_ category: String, _ behot_time: Int)
}
class HomeViewModel: HomeProtocol {
lazy var provider = HTTPServiceProvider.shared
var recommend: Observable<HomeListModel>!
init() {}
init(_ provider: RxMoyaProvider<MultiTarget>) {
self.provider = provider
}
func fetchRecommendList(_ category: String, _ behot_time: Int) {
recommend = provider.request(MultiTarget(HomeAPI.homeList(category: category,behot_time: behot_time)))
.debug()
.mapObject(HomeListModel.self)
}
}
When I made a breakpoint at request method, it didn't do a request? Does anyone know it ? Thanks first
SomeOne told me the reason,So I write it here. In my ViewModel recommend should be backed by PublishSubject or BehaviourSubject or ReplaySubject and then I should share this for View as Observable. In fetchRecommentList method I should bind request to created Subject.
Now I have created observable, but request will run after subsribe or bind
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)
}
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.