RxSwift - subscribe to a method - ios

Is there a way with RxSwift to subscribe to a method which returns a completion block?
Example, let's have this object:
struct Service {
private var otherService = ...
private var initSucceeded = PublishSubject<Bool>()
var initSucceededObservale: Observable<Bool> {
return initSucceeded.asObservable()
}
func init() {
otherService.init {(success) in
self.initSucceeded.onNext( success)
}
}
}
And in a different place have a way to be notified when the service has been initialised:
service.initSucceededObservable.subscribe(onNext: {
[unowned self] (value) in
...
}).addDisposableTo(disposeBag)
service.init()
Would be there a simpler solution?

I like to use Variables for this sort of thing. Also, I'd recommend using class here because you're tracking unique states and not just concerning yourself with values.
class Service {
private let bag = DisposeBag()
public var otherService: Service?
private var isInitializedVariable = Variable<Bool>(false)
public var isInitialized: Observable<Bool> {
return isInitializedVariable.asObservable()
}
public init(andRelyOn service: Service? = nil) {
otherService = service
otherService?.isInitialized
.subscribe(onNext: { [unowned self] value in
self.isInitializedVariable.value = value
})
.addDisposableTo(bag)
}
public func initialize() {
isInitializedVariable.value = true
}
}
var otherService = Service()
var dependentService = Service(andRelyOn: otherService)
dependentService.isInitialized
.debug()
.subscribe({
print($0)
})
otherService.initialize() // "Initializes" the first service, causing the second to set it's variable to true.

You could use a lazy property:
lazy let initSucceededObservale: Observable<Bool> = {
return Observable.create { observer in
self.otherService.init {(success) in
observer.on(.next(success))
observer.on(.completed)
}
return Disposables.create()
}
}()
and then you can use:
service.init()
service.initSucceededObservable.subscribe(onNext: {
[unowned self] (value) in
...
}).addDisposableTo(disposeBag)
Let me know in the comments if you have problems before downvoting, thanks.

Related

RxSwift - Determining whether an Observable has been disposed

I'm trying to get Publisher which vends Observables to its clients Consumer, to determine when one of its consumers has disposed of its Observable.
Annoyingly. my code was working fine, until I removed an RxSwift .debug from within the Consumer code.
Is there some alternative way I might get this working?
private class Subscriber {
var ids: [Int]
// This property exists so I can watch whether the observable has
// gone nil (which I though would happen when its been disposed, but it
// seems to happen immediately)
weak var observable: Observable<[Updates]>?
}
class Publisher {
private let relay: BehaviorRelay<[Int: Updates]>
private var subscribers: [Subscriber] = []
func updatesStream(for ids: [Int]) -> Observable<[Updates]> {
let observable = relay
.map { map in
return map
.filter { ids.contains($0.key) }
.map { $0.value }
}
.filter { !$0.isEmpty }
.asObservable()
let subscriber = Subscriber(ids: ids, observable: observable)
subscribers.append(subscriber)
return observable
}
private func repeatTimer() {
let updates: [Updates] = ....
// I need to be able to determine at this point whether the subscriber's
// observable has been disposed of, so I can remove it from the list of
// subscribers. However `subscriber.observable` is always nil here.
// PS: I am happy for this to happen before the `repeatTimer` func fires
subscribers.remove(where: { subscriber in
return subscriber.observable == nil
})
relay.accept(updates)
}
}
class Client {
private var disposeBag: DisposeBag?
private let publisher = Publisher()
func startWatching() {
let disposeBag = DisposeBag()
self.disposeBag = disposeBag
publisher
// with the `.debug` below things work OK, without it the
///`Publisher.Subscriber.observable` immediately becomes nil
//.debug("Consumer")
.updatesStream(for: [1, 2, 3])
.subscribe(onNext: { values in
print(values)
})
.disposed(by: disposeBag)
}
func stopWatching() {
disposeBag = nil
}
}
I think this is a very bad idea, but it solves the requested problem... If I had to put this code in one of my projects, I would be very worried about race conditions...
struct Subscriber {
let ids: [Int]
var subscribeCount: Int = 0
let lock = NSRecursiveLock()
}
class Publisher {
private let relay = BehaviorRelay<[Int: Updates]>(value: [:])
private var subscribers: [Subscriber] = []
func updatesStream(for ids: [Int]) -> Observable<[Updates]> {
var subscriber = Subscriber(ids: ids)
let observable = relay
.map { map in
return map
.filter { ids.contains($0.key) }
.map { $0.value }
}
.filter { !$0.isEmpty }
.do(
onSubscribe: {
subscriber.lock.lock()
subscriber.subscribeCount += 1
subscriber.lock.unlock()
},
onDispose: {
subscriber.lock.lock()
subscriber.subscribeCount -= 1
subscriber.lock.unlock()
})
.asObservable()
subscribers.append(subscriber)
return observable
}
private func repeatTimer() {
subscribers.removeAll(where: { subscriber in
subscriber.subscribeCount == 0
})
}
}

Combine - subscribe to a publisher of PassthroughSubject twice

I'm trying to subscribe to a Publisher created from a PassthroughSubject twice and only one of them is executed when PassthroughSubject fires a value.
Here is what I tried:
class Worker {
let stringGeneratorResultSubject: PassthroughSubject<String, Error>
init(stringGeneratorResultSubject: PassthroughSubject<String, Error>) {
self.stringGeneratorResultSubject = stringGeneratorResultSubject
}
func generateString() {
stringGeneratorResultSubject.send("someValue")
}
}
class A {
let workerObj: Worker
let workerObjPublisher: AnyPublisher<String, Swift.Error>
init(workerObj: Worker,
workerObjPublisher: AnyPublisher<String, Swift.Error>) {
self.workerObj = workerObj
self.workerObjPublisher = workerObjPublisher
super.init()
getString()
}
func getString() {
workerObjPublisher.sink { result in
// do something with result for
}.store(in: &cancellable)
workerObj.generateString()
}
}
class B {
let workerObjPublisher: AnyPublisher<String, Swift.Error>
init(workerObjPublisher: AnyPublisher<String, Swift.Error>) {
self.workerObjPublisher = workerObjPublisher
super.init()
loadString()
}
func loadString() {
workerObjPublisher.sink { result in
// do something with result
}.store(in: &cancellable)
}
}
class Parent {
lazy var stringGeneratorResultSubject: PassthroughSubject<String, Swift.Error> = .init()
lazy var workerObj: Worker = .init(stringGeneratorResultSubject: stringGeneratorResultSubject)
lazy var aObj: A = .init(workerObj: workerObj,
workerObjPublisher: stringGeneratorResultSubject.eraseToAnyPublisher())
lazy var bObj: B = .init(workerObjPublisher: stringGeneratorResultSubject.eraseToAnyPublisher())
_ = bObj
aObj.getString()
}
Only class A's subscription block in getString() is called. class B's subscription block inside loadString() is not executed. Am I missing something?

Mocking and Validating Results in RxSwift Unit Testing

I have just started learning RxSwift and trying to build a sample application to practice these concepts.
I have written a QuestionViewModel that loads list of questions from QuestionOps class. QuestionOps has a function getQuestions that returns Single<[Question]>.
Problem that I am facing is, how to mock the behavior of QuestionOps class while testing QuestionViewModel.
public class QuestionsListViewModel {
public var questionOps: QuestionOps!
private let disposeBag = DisposeBag()
private let items = BehaviorRelay<[QuestionItemViewModel]>(value: [])
public let loadNextPage = PublishSubject<Void>()
public var listItems: Driver<[QuestionItemViewModel]>
public init() {
listItems = items.asDriver(onErrorJustReturn: [])
loadNextPage
.flatMapFirst { self.questionOps.getQuestions() }
.map { $0.map { QuestionItemViewModel($0) } }
.bind(to: items)
.disposed(by: disposeBag)
}
}
public class QuestionOps {
public func getQuestions() -> Single<[Question]> {
return Single.create { event -> Disposable in
event(.success([]))
return Disposables.create()
}
}
}
I have created this MockQuestionOps for test purpose:
public class MockQuestionOps : QuestionOps {
//MARK: -
//MARK: Responses
public var getQuestionsResponse: Single<[Question]>?
public func getQuestions() -> Single<[Question]> {
self.getQuestionsResponse = Single.create { event -> Disposable in
return Disposables.create()
}
return self.getQuestionsResponse!
}
}
In my test case I am doing the following:
/// My idea here is to test in following maner:
/// - at some point user initates loading
/// - after some time got network response with status true
func testLoadedDataIsDisplayedCorrectly() {
scheduler = TestScheduler(initialClock: 0)
let questionsLoadedObserver = scheduler.createObserver([QuestionItemViewModel].self)
let qOps = MockQuestionOps()
vm = QuestionsListViewModel()
vm.questionOps = qOps
vm.listItems
.drive(questionsLoadedObserver)
.disposed(by: disposebag)
// User initiates load questions
scheduler.createColdObservable([.next(2, ())])
.bind(to: vm.loadNextPage)
.disposed(by: disposebag)
// Simulating question ops behaviour of responding
// to get question request
/// HERE: -----------
/// This is where I am stuck
/// How should I tell qOps to send particular response with delay
scheduler.start()
/// HERE: -----------
/// How can I test list is initialy empty
/// and after loading, data is correctly loaded
}
Here is a complete, compilable answer (not including imports.)
You tell qOps to emit by giving it a cold test observable that will emit the correct values.
You test the output by comparing the events gathered by the test observer with the expected results.
There is no notion of "the list is initially empty". The list is always empty. It emits values over time and what you are testing is whether it emitted the correct values.
class rx_sandboxTests: XCTestCase {
func testLoadedDataIsDisplayedCorrectly() {
let scheduler = TestScheduler(initialClock: 0)
let disposebag = DisposeBag()
let questionsLoadedObserver = scheduler.createObserver([QuestionItemViewModel].self)
let qOps = MockQuestionOps(scheduler: scheduler)
let vm = QuestionsListViewModel(questionOps: qOps)
vm.listItems
.drive(questionsLoadedObserver)
.disposed(by: disposebag)
scheduler.createColdObservable([.next(2, ())])
.bind(to: vm.loadNextPage)
.disposed(by: disposebag)
scheduler.start()
XCTAssertEqual(questionsLoadedObserver.events, [.next(12, [QuestionItemViewModel(), QuestionItemViewModel()])])
}
}
protocol QuestionOpsType {
func getQuestions() -> Single<[Question]>
}
struct MockQuestionOps: QuestionOpsType {
func getQuestions() -> Single<[Question]> {
return scheduler.createColdObservable([.next(10, [Question(), Question()]), .completed(10)]).asSingle()
}
let scheduler: TestScheduler
}
class QuestionsListViewModel {
let listItems: Driver<[QuestionItemViewModel]>
private let _loadNextPage = PublishSubject<Void>()
var loadNextPage: AnyObserver<Void> {
return _loadNextPage.asObserver()
}
init(questionOps: QuestionOpsType) {
listItems = _loadNextPage
.flatMapFirst { [questionOps] in
questionOps.getQuestions().asObservable()
}
.map { $0.map { QuestionItemViewModel($0) } }
.asDriver(onErrorJustReturn: [])
}
}
struct Question { }
struct QuestionItemViewModel: Equatable {
init() { }
init(_ question: Question) { }
}

MVVM with RxSwift

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.

RxSwift+Moya+Moya_ObjectMapper+MJRefresh, refresh failed?

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

Resources