How to get an error response with driver so I can show it in alert. When I see the trait driver is can't error out, so should I use subject or behaviourRelay to get error response when I subscribe. Actually I like how to use driver but I don't know how to passed error response using driver.
this is my network service
func getMovies(page: Int) -> Observable<[MovieItem]> {
return Observable.create { observer -> Disposable in
self.service.request(endpoint: .discover(page: page)) { data, response, error in
if let _ = error {
observer.onError(MDBError.unableToComplete)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
observer.onError(MDBError.invalidResponse)
return
}
guard let data = data else {
observer.onError(MDBError.invalidData)
return
}
if let decode = self.decode(jsonData: MovieResults.self, from: data) {
observer.onNext(decode.results)
}
observer.onCompleted()
}
return Disposables.create()
}
}
This is my viewModel
protocol ViewModelType {
associatedtype Input
associatedtype Output
func transform(input: Input) -> Output
}
class PopularViewModel: ViewModelType {
struct Input {
let viewDidLoad: Driver<Void>
}
struct Output {
let loading: Driver<Bool>
let movies: Driver<[MovieItem]>
}
private let service: NetworkDataFetcher
init(service: NetworkDataFetcher = NetworkDataFetcher(service: NetworkService())) {
self.service = service
}
func transform(input: Input) -> Output {
let loading = ActivityIndicator()
let movies = input.viewDidLoad
.flatMap { _ in
self.service.getMovies(page: 1)
.trackActivity(loading)
.asDriver(onErrorJustReturn: [])
}
let errorResponse = movies
return Output(loading: loading.asDriver(),movies: movies)
}
}
this is how I bind the viewModel in viewController
let input = PopularViewModel.Input(viewDidLoad: rx.viewDidLoad.asDriver())
let output = viewModel.transform(input: input)
output.movies.drive { [weak self] movies in
guard let self = self else { return }
self.populars = movies
self.updateData(on: movies)
}.disposed(by: disposeBag)
output.loading
.drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible)
.disposed(by: disposeBag)
You do this the same way you handled the ActivityIndicator...
The ErrorRouter type below can be found here.
This is such a common pattern that I have created an API class that takes care of this automatically.
class PopularViewModel: ViewModelType {
struct Input {
let viewDidLoad: Driver<Void>
}
struct Output {
let loading: Driver<Bool>
let movies: Driver<[MovieItem]>
let displayAlertMessage: Driver<String>
}
private let service: NetworkDataFetcher
init(service: NetworkDataFetcher = NetworkDataFetcher(service: NetworkService())) {
self.service = service
}
func transform(input: Input) -> Output {
let loading = ActivityIndicator()
let errorRouter = ErrorRouter()
let movies = input.viewDidLoad
.flatMap { [service] in
service.getMovies(page: 1)
.trackActivity(loading)
.rerouteError(errorRouter)
.asDriver(onErrorRecover: { _ in fatalError() })
}
let displayAlertMessage = errorRouter.error
.map { $0.localizedDescription }
.asDriver(onErrorRecover: { _ in fatalError() })
return Output(
loading: loading.isActive.asDriver(onErrorRecover: { _ in fatalError() }),
movies: movies,
displayAlertMessage: displayAlertMessage
)
}
}
How to get the result from Result<Bool, Error> as a Boolean answer only in Swift?
I do the purchase from the AppStore from the ViewController:
Purchases.default.purchaseProduct(productId: "com.justdoit.buy_1week") { [weak self] res in
self?.hideSpinner()
if res == .success {
// Handle result
// IF OKAY - WE DO THE REQUEST AND CHANGE EVERYTHING FOR PREMIUM USER
} else {
self?.alert(alertMessage: "Error during the purchase") // my popUp view
}
}
Purchases.swift:
import StoreKit
typealias RequestProductsResult = Result<[SKProduct], Error>
typealias PurchaseProductResult = Result<Bool, Error>
typealias RequestProductsCompletion = (RequestProductsResult) -> Void
typealias PurchaseProductCompletion = (PurchaseProductResult) -> Void
// class
class Purchases: NSObject {
static let `default` = Purchases()
fileprivate var productPurchaseCallback: ((PurchaseProductResult) -> Void)?
private let productIdentifiers = Set<String>(
arrayLiteral:
"com.justdoit.buy_1week",
"com.justdoit.buy_1month",
"com.justdoit.buy_3months",
"com.justdoit.buy_1year"
)
private var products: [String: SKProduct]?
private var productRequest: SKProductsRequest?
func initialize(completion: #escaping RequestProductsCompletion) {
requestProducts(completion: completion)
}
private var productsRequestCallbacks = [RequestProductsCompletion]()
private func requestProducts(completion: #escaping RequestProductsCompletion) {
guard productsRequestCallbacks.isEmpty else {
productsRequestCallbacks.append(completion)
return
}
productsRequestCallbacks.append(completion)
let productRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productRequest.delegate = self
productRequest.start()
self.productRequest = productRequest
}
func purchaseProduct(productId: String, completion: #escaping (PurchaseProductResult) -> Void) {
guard productPurchaseCallback == nil else {
completion(.failure(PurchasesError.purchaseInProgress))
return
}
guard let product = products?[productId] else {
completion(.failure(PurchasesError.productNotFound))
return
}
productPurchaseCallback = completion
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
public func restorePurchases(completion: #escaping (PurchaseProductResult) -> Void) {
guard productPurchaseCallback == nil else {
completion(.failure(PurchasesError.purchaseInProgress))
return
}
productPurchaseCallback = completion
// 4:
SKPaymentQueue.default().restoreCompletedTransactions()
}
}
// responses
extension Purchases: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
guard !response.products.isEmpty else {
print("Found 0 products")
productsRequestCallbacks.forEach { $0(.success(response.products)) }
productsRequestCallbacks.removeAll()
return
}
var products = [String: SKProduct]()
for skProduct in response.products {
print("Found product: \(skProduct.productIdentifier)")
products[skProduct.productIdentifier] = skProduct
}
self.products = products
productsRequestCallbacks.forEach { $0(.success(response.products)) }
productsRequestCallbacks.removeAll()
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Failed to load products with error:\n \(error)")
productsRequestCallbacks.forEach { $0(.failure(error)) }
productsRequestCallbacks.removeAll()
}
}
extension Purchases: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
// 1:
for transaction in transactions {
switch transaction.transactionState {
// 2:
case .purchased, .restored:
if finishTransaction(transaction) {
SKPaymentQueue.default().finishTransaction(transaction)
productPurchaseCallback?(.success(true))
} else {
productPurchaseCallback?(.failure(PurchasesError.unknown))
}
// 3:
case .failed:
productPurchaseCallback?(.failure(transaction.error ?? PurchasesError.unknown))
SKPaymentQueue.default().finishTransaction(transaction)
default:
break
}
}
productPurchaseCallback = nil
}
}
extension Purchases {
// 4:
func finishTransaction(_ transaction: SKPaymentTransaction) -> Bool {
let productId = transaction.payment.productIdentifier
print("Product \(productId) successfully purchased")
return true
}
}
enum PurchasesError: Error {
case purchaseInProgress
case productNotFound
case unknown
}
The first time I've been working with payments in Swift, I have no idea how to get the .success(true) result for successful payments only and use it later...
Is there any advice?
Use switch case. Here enum have Associated Values.
Purchases.default.purchaseProduct(productId: "com.justdoit.buy_1week") { [weak self] res in
switch res {
case .success(let value):
break
case .failure(let error):
print(error.lo)
}
}
You can also use if
if case Result.success(let value) = res {
print(value)
} else if case Result.failure(let error) = res {
print(error.localizedDescription)
}
This is what I am doing:
-> Login/Signup to Firebase using FirebaseAuthentification
-> Listining to AuthStateDidChangeListenerHandle
-> I store extra user information in Firestore, therefore I check if the user exists in Firestore
-> If the user does not exist I create an empty user
-> If everything was successful I return a Future Publisher via callback (I want to change that as well)
This is the checkLoginState function:
func checkLoginState(completion: #escaping (AnyPublisher<AccountDetails,Error>) -> Void) {
self.handler = Auth.auth().addStateDidChangeListener { [weak self] auth, user in
guard let safeSelf = self else { return }
completion(Future<AccountDetails,Error> { promise in
if let user = user {
print(user)
print(auth)
safeSelf.checkIfUserIsInDatabase(user: user.uid) { result in
switch result {
case .success(let isAvailable):
if isAvailable {
promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
} else {
safeSelf.createEmptyUser(user: user.uid,email: user.email) { result in
switch result {
case .success(_):
promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
case .failure(let error):
print(error)
}
}
}
case .failure(let error):
print(error)
}
}
} else {
promise(.success(AccountDetails(userUID: nil, loggedIn: false, premiumUser: false)))
}
}.eraseToAnyPublisher()
)
}
}
These are my current functions:
private func checkIfUserIsInDatabase(user id: String, completion: #escaping (Result<Bool,Error>) -> Void)
private func createEmptyUser(user id: String, email:String?, completion: #escaping (Result<Bool,Error>) -> Void)
Thats what I want to use:
private func checkIfUserIsInDatabase(user id: String) -> AnyPublisher<Bool,Error>
private func createEmptyUser(user id: String) -> AnyPublisher<Bool,Error>
func checkLoginState() -> AnyPublisher<AccountDetails,Error>
I had something like that, but it does not work, also looks confusing:
func checkLoginState(completion: #escaping (AnyPublisher<AccountDetails,Error>) -> Void) {
self.handler = Auth.auth().addStateDidChangeListener { [weak self] auth, user in
guard let safeSelf = self else { return }
completion(Future<AccountDetails,Error> { promise in
if let user = user {
print(user)
print(auth)
safeSelf.checkIfUserIsInDatabase(user: user.uid)
.sinkToResult { value in
switch value {
case .success(let isUserInDatabase):
if isUserInDatabase {
promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
} else {
safeSelf.createEmptyUser(user: user.uid)
.sinkToResult { value in
switch value {
case .success( _):
promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
case .failure(let error):
print(error)
}
}
}
case .failure(let error):
print(error)
}
}
} else {
promise(.success(AccountDetails(userUID: nil, loggedIn: false, premiumUser: false)))
}
}.eraseToAnyPublisher()
)
}
}
So you have some AccountDetails type:
import Combine
import FirebaseAuth
struct AccountDetails {
var userId: String
var name: String?
var isLoggedIn: Bool
var isPremiumUser: Bool
}
Let's extend it with an init that takes a User, because it will simplify things later:
extension AccountDetails {
init(user: User) {
self.userId = user.uid
self.name = user.displayName
self.isLoggedIn = true
self.isPremiumUser = false
}
}
I think your end goal is a Publisher that emits AccountDetails. But since there isn't always a logged-in user, it should really emit Optional<AccountDetails>, so that it can emit nil when the user logs out.
Let's start by wrapping the addStateDidChangeListener API in a Publisher. We can't use a Future for this, because a Future emits at most one output, but addStateDidChangeListener can emit multiple events. So we'll use a CurrentValueSubject instead. That means we need a place to store the subject and the AuthStateDidChangeListenerHandle. You could store them as globals, or in your AppDelegate, or wherever you feel is appropriate. For this answer, let's create a Demo class to hold them:
class Demo {
static let shared = Demo()
let userPublisher: AnyPublisher<User?, Error>
private let userSubject = CurrentValueSubject<User?, Error>(nil)
private var tickets: [AnyCancellable] = []
private init() {
userPublisher = userSubject.eraseToAnyPublisher()
let handle = Auth.auth().addStateDidChangeListener { [userSubject] (_, user) in
userSubject.send(user)
}
AnyCancellable { Auth.auth().removeStateDidChangeListener(handle) }
.store(in: &tickets)
}
}
So now you can get a Publisher of the logged-in user (or nil if no user is logged in) like this:
let loggedInUserPublisher: AnyPublisher<User?, Error> = Demo.shared.userPublisher
But you really want an AccountDetails? publisher, not a User? publisher, like this:
let accountDetailsPublisher: AnyPublisher<AccountDetails?, Error> = Demo.shared
.accountDetailsPublisher()
So we need to write an accountDetailsPublisher method that maps the User? to an AccountDetails?.
If the User? is nil, we just want to emit nil. But if the User? is .some(user), we need to do more asynchronous actions: we need to check whether the user is in the database, and add the user if not. The flatMap operator lets you chain asynchronous actions, but there's some complexity because we need to take different actions depending on the output of the upstream publisher.
We'd really like to hide the complexity away and just write this:
extension Demo {
func loggedInAccountDetailsPublisher() -> AnyPublisher<AccountDetails?, Error> {
return userPublisher
.flatMap(
ifSome: { $0.accountDetailsPublisher().map { Optional.some($0) } },
ifNone: { Just(nil).setFailureType(to: Error.self) })
.eraseToAnyPublisher()
}
}
But then we need to write flatMap(ifSome:ifNone:). Here it is:
extension Publisher {
func flatMap<Wrapped, Some: Publisher, None: Publisher>(
ifSome: #escaping (Wrapped) -> Some,
ifNone: #escaping () -> None
) -> AnyPublisher<Some.Output, Failure>
where Output == Optional<Wrapped>, Some.Output == None.Output, Some.Failure == Failure, None.Failure == Failure
{
return self
.flatMap { $0.map { ifSome($0).eraseToAnyPublisher() } ?? ifNone().eraseToAnyPublisher() }
.eraseToAnyPublisher()
}
}
Now we need to implement accountDetailsPublisher in a User extension. What does this method need to do? It needs to check whether the User is in the database (an asynchronous action) and, if not, add the User (another asynchronous action). Since we need to chain asynchronous actions, we again need flatMap. But we'd really like to just write this:
extension User {
func accountDetailsPublisher() -> AnyPublisher<AccountDetails, Error> {
return isInDatabasePublisher()
.flatMap(
ifTrue: { Just(AccountDetails(user: self)).setFailureType(to: Error.self) },
ifFalse: { self.addToDatabase().map { AccountDetails(user: self) } })
}
}
Here is flatMap(ifTrue:ifFalse:):
extension Publisher where Output == Bool {
func flatMap<True: Publisher, False: Publisher>(
ifTrue: #escaping () -> True,
ifFalse: #escaping () -> False
) -> AnyPublisher<True.Output, Failure>
where True.Output == False.Output, True.Failure == Failure, False.Failure == Failure
{
return self
.flatMap { return $0 ? ifTrue().eraseToAnyPublisher() : ifFalse().eraseToAnyPublisher() }
.eraseToAnyPublisher()
}
}
Now we need to write isInDatabasePublisher and addToDatabase methods on User. I don't have the source code to your checkIfUserIsInDatabase and createEmptyUser functions, so I can't convert them to publishers directly. But we can wrap them using Future:
extension User {
func isInDatabasePublisher() -> AnyPublisher<Bool, Error> {
return Future { promise in
checkIfUserIsInDatabase(user: self.uid, completion: promise)
}.eraseToAnyPublisher()
}
func addToDatabase() -> AnyPublisher<Void, Error> {
return Future { promise in
createEmptyUser(user: self.uid, email: self.email, completion: promise)
} //
.map { _ in } // convert Bool to Void
.eraseToAnyPublisher()
}
}
Note that, since your example code ignores the Bool output of createEmptyUser, I wrote addToDatabase to output Void instead.
Thats what I came up with:
Reference by matt:
http://www.apeth.com/UnderstandingCombine/operators/operatorsflatmap.html#SECSerializingAsynchronicity
https://stackoverflow.com/a/60418000/341994
var handler: AuthStateDidChangeListenerHandle?
var storage = Set<AnyCancellable>()
func checkLoginState(completion: #escaping (AnyPublisher<AccountDetails,Error>) -> Void) {
self.handler = Auth.auth().addStateDidChangeListener { [weak self] auth, user in
guard let safeSelf = self else { return }
completion(Future<AccountDetails,Error> { promise in
if let user = user {
safeSelf.handleUserInDatabase(user: user.uid)
.sink(receiveCompletion: { completion in
if let error = completion.error {
print(error.localizedDescription)
promise(.failure(error))
}
}, receiveValue: { result in
if result {
promise(.success(AccountDetails(userUID: user.uid,name: user.displayName, loggedIn: true, premiumUser: false)))
}
}).store(in: &safeSelf.storage)
} else {
promise(.success(AccountDetails(userUID: nil, loggedIn: false, premiumUser: false)))
}
}.eraseToAnyPublisher())
}
}
/// Checks if User exists in Firestore, if not creates an empty User and returns true
private func handleUserInDatabase(user: String) -> AnyPublisher<Bool,Error> {
return Future<Bool,Error>( { [weak self] promise in
guard let safeSelf = self else { return }
safeSelf.checkIfUserIsInDatabase(user: user)
.flatMap { result -> AnyPublisher<Bool,Error> in
if result == false {
return safeSelf.createEmptyUser(user: user).eraseToAnyPublisher()
} else {
promise(.success(true))
return Empty<Bool,Error>(completeImmediately: true).eraseToAnyPublisher()
}}
.sink(receiveCompletion: { completion in
if let error = completion.error {
promise(.failure(error))
}}, receiveValue: {promise(.success($0))})
.store(in:&safeSelf.storage)
}
).eraseToAnyPublisher()
}
I am a beginner and I need to test my FirestoreService class below. I tried to make a mock but I do not go in the snapshot. I just need to test the case where it fails.
class FirebaseService {
let decoder = JSONDecoder()
var movies = [Movies]()
enum Result {
case success([Movies])
case failure(Error)
}
func getMovies(completion: #escaping (Result) -> Void) {
let movieReference = Firestore.firestore().collection("movies").order(by: "id")
movieReference.addSnapshotListener { (snapshot, _) in
guard let snapshot = snapshot else {return}
do {
self.movies = try snapshot.decoded()
completion(.success(self.movies))
} catch {
completion(.failure(error))
}
}
}
This is my FirebaseServiceTests class
XCTestclass FirebaseServiceTests: XCTestCase {
func testGetMovies() {
let firebaseSercice = FirebaseService()
let expectation = XCTestExpectation(description: "Wait for queue change ")
firebaseSercice.getMovies { result in
XCTAssertNotNil(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 0.5)
}
func testGetMoviesWithMock() {
let firebaseSercice = MockDatabaseReference()
let expectation = XCTestExpectation(description: "Wait for queue change ")
firebaseSercice.getMovies { result in
XCTAssertNil(result)
expectation.fulfill()
}
wait(for: [expectation], timeout: 0.5)
}
}
private class MockDatabaseReference: FirebaseService {
override func getMovies(completion: #escaping (Result) -> Void) {
let movieTestReference = Firestore.firestore().collection("mov").order(by: "id")
movieTestReference.addSnapshotListener { (snapshot, _) in
guard let snapshot = snapshot else {return}
do {
self.movies = try snapshot.decoded()
completion(.success(self.movies))
} catch {
completion(.failure(error))
}
}
}
}
Could you explain me how to do ? Thank you.
I can't manage to get this solution to work:
https://github.com/liuznsn/RxMoyaPaginationNetworking
Maybe someone can tell me where is the mistake. The loading variable never goes to false. I guess the issue is in the request observable, but I can't find out why.
class PaginationNetworkModel<T1: Mappable>: NSObject {
let refreshTrigger = PublishSubject<Void>()
let loadNextPageTrigger = PublishSubject<Void>()
let loading = Variable<Bool>(false)
let elements = Variable<[T1]>([])
var offset:Int = 0
let error = PublishSubject<Swift.Error>()
private let disposeBag = DisposeBag()
override init() {
super.init()
let refreshRequest = loading.asObservable()
.sample(refreshTrigger)
.flatMap { [unowned self] loading -> Observable<[T1]> in
if loading {
return Observable.empty()
} else {
return self.loadData(offset: self.offset)
}
}
let nextPageRequest = loading.asObservable()
.sample(loadNextPageTrigger)
.flatMap { [unowned self] loading -> Observable<[T1]> in
if loading {
return Observable.empty()
} else {
self.offset += 1
return self.loadData(offset: self.offset)
}
}
let request = Observable
.of(refreshRequest, nextPageRequest)
.merge()
.shareReplay(1)
let response = request.flatMap { events -> Observable<[T1]> in
request
.do(onError: { error in
self.error.onNext(error)
}).catchError({ error -> Observable<[T1]> in
Observable.empty()
})
}.shareReplay(1)
Observable
.combineLatest(request, response, elements.asObservable()) { [unowned self] request, response, elements in
return self.offset == 0 ? response : elements + response
}
.sample(response)
.bind(to: elements)
.addDisposableTo(rx_disposeBag)
Observable
.of(request.map { _ in true },
response.map { $0.count == 0 },
error.map { _ in false }
)
.merge()
.bind(to: loading)
.addDisposableTo(rx_disposeBag)
}
func loadData(offset: Int) -> Observable<[T1]> {
return Observable.empty()
}
Thank you Daniel for help, here completed solution
Call example:
goodsModel = GoodsNetworkModel()
goodsModel.elements.asObservable().bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: StoreCell.self)) { (ip, item: Goods, cell: StoreCell) in
cell.configure(goods: item)
}.addDisposableTo(rx_disposeBag)
tableView.rx.itemSelected.bind() { [unowned self] ip in
self.didSelectRow(ip: ip.row)
}.addDisposableTo(rx_disposeBag)
rx.sentMessage(#selector(UIViewController.viewDidAppear(_:)))
.map { _ in () }
.bind(to: goodsModel.refreshTrigger)
.addDisposableTo(rx_disposeBag)
tableView.rx_reachedBottom
.map{ _ in ()}
.bind(to: goodsModel.loadNextPageTrigger)
.addDisposableTo(rx_disposeBag)
Model code:
class PaginationNetworkModel<T1: Mappable>: NSObject {
let refreshTrigger = PublishSubject<Void>()
let loadNextPageTrigger = PublishSubject<Void>()
let loading = Variable<Bool>(false)
let elements = Variable<[T1]>([])
var offset:Int = 0
let error = PublishSubject<Swift.Error>()
private let disposeBag = DisposeBag()
override init() {
super.init()
let refreshRequest = loading.asObservable()
.sample(refreshTrigger)
.flatMap { loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { observer in
observer.onNext(0)
observer.onCompleted()
return Disposables.create()
}
}
}
let nextPageRequest = loading.asObservable()
.sample(loadNextPageTrigger)
.flatMap { [unowned self] loading -> Observable<Int> in
if loading {
return Observable.empty()
} else {
return Observable<Int>.create { [unowned self] observer in
self.offset += 1
observer.onNext(self.offset)
observer.onCompleted()
return Disposables.create()
}
}
}
let request = Observable
.of(refreshRequest, nextPageRequest)
.merge()
.shareReplay(1)
let response = request.flatMap { offset -> Observable<[T1]> in
self.loadData(offset: offset)
.do(onError: { [weak self] error in
self?.error.onNext(error)
}).catchError({ error -> Observable<[T1]> in
Observable.empty()
})
}.shareReplay(1)
Observable
.combineLatest(request, response, elements.asObservable()) { [unowned self] request, response, elements in
return self.offset == 0 ? response : elements + response
}
.sample(response)
.bind(to: elements)
.addDisposableTo(rx_disposeBag)
Observable
.of(request.map{_ in true},
response.map { $0.count == 0 },
error.map { _ in false })
.merge()
.bind(to: loading)
.addDisposableTo(rx_disposeBag)
}
func loadData(offset: Int) -> Observable<[T1]> {
return Observable.empty()
}
The problem is here:
let refreshRequest: Observable<[T1]> = loading.asObservable()
.sample(refreshTrigger)
.flatMap { [unowned self] loading -> Observable<[T1]> in
if loading {
return Observable.empty()
} else {
return self.loadData(offset: self.offset)
}
}
refreshRequest doesn't emit a value until after loadData returns. The way your code is structured, emitting a signal on the refreshTrigger will start the network request, and then set loading to true after the network request completes.
It would be better to have refreshRequest and nextPageRequest return an Observable of what page to load and then merge them and call flatMap with the network call on the merged result.