RxSwift to juggle local database and remote network? - swift

Currently I have services that manages retrieving data from the local storage, but also checks the remote network for any modified data. It is using a completion handler with Result pattern and protocol type, but would like to convert this to an observable approach.
Here is the current logic:
struct AuthorWorker: AuthorWorkerType, Loggable {
private let store: AuthorStore
private let remote: AuthorRemote
init(store: AuthorStore, remote: AuthorRemote) {
self.store = store
self.remote = remote
}
}
extension AuthorWorker {
func fetch(id: Int, completion: #escaping (Result<AuthorType, DataError>) -> Void) {
store.fetch(id: id) {
// Immediately return local response
completion($0)
guard case .success(let cacheElement) = $0 else { return }
// Sync remote updates to cache if applicable
remote.fetch(id: id) {
// Validate if any updates occurred and return
guard case .success(let element) = $0,
element.modifiedAt > cacheElement.modifiedAt else {
return
}
// Update local storage with updated data
self.store.createOrUpdate(element) {
guard case .success = $0 else { return }
// Callback handler again if updated
completion($0)
}
}
}
}
}
I'm always instantly returning the local data to the UI so the user doesn't wait. In the background, it is checking the remote network for modified data and updates the UI again only if necessary. I use it like this:
authorWorker.fetch(1) { [weak self] in
guard case .success(let value) = $0 else {
// alert error
}
self?.myLabel.text = value.name
}
How can this be converted to RxSwift or an observable concept? This is what I got started, but I don't see the code on the walls like Neo yet when it comes to Rx, so I need help seeing the light.
extension AuthorWorker {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable<AuthorType>.create { observer in
store.fetch(id: id) {
// Immediately return local response
observer.on(.next($0))
guard case .success(let cacheElement) = $0 else {
observer.on(.completed)
return
}
// Sync remote updates to cache if applicable
remote.fetch(id: id) {
// Validate if any updates occurred and return
guard case .success(let element) = $0,
element.modifiedAt > cacheElement.modifiedAt else {
observer.on(.completed)
return
}
// Update local storage with updated data
self.store.createOrUpdate(element) {
guard case .success = $0 else {
observer.on(.completed)
return
}
// Callback handler again if updated
observer.on(.next($0))
observer.on(.completed)
}
}
}
}
}
}
Then I would use it like this?
authorWorker.fetch(1).subscribe { [weak self] in
guard let element = $0.element else {
// Handle error how?
return
}
self?.myLabel.text = element.name
}
Is this the right approach or is there a more recommended way to do this? Is it also worth converting the underlying remote and local stores to observable as well, or does it make sense not to convert all things to observable all the time?

New Answer
Based on the comment, I see that you want something much more elaborate than my first answer, so here you go.
func worker<T: Equatable>(store: Observable<T>, remote: Observable<T>) -> (value: Observable<T>, store: Observable<T>) {
let sharedStore = store.share(replay: 1)
let sharedRemote = remote.share(replay: 1)
let value = Observable.merge(sharedStore, sharedRemote)
.distinctUntilChanged()
.takeUntil(sharedRemote.materialize().filter { $0.isStopEvent })
let store = Observable.zip(sharedStore, sharedRemote)
.filter { $0.0 != $0.1 }
.map { $0.1 }
return (value: value, store: store)
}
Here is the code above being used in your AuthorWorker class:
extension AuthorWorker {
func fetch(id: Int) -> Observable<AuthorType> {
let (_value, _store) = worker(store: store.fetch(id: id), remote: remote.fetch(id: id))
_ = _store
.subscribe(onNext: store.createOrUpdate)
return _value
}
}
And here is a test suite proving it works properly:
class Tests: XCTestCase {
var scheduler: TestScheduler!
var emission: TestableObserver<String>!
var storage: TestableObserver<String>!
var disposeBag: DisposeBag!
override func setUp() {
super.setUp()
scheduler = TestScheduler(initialClock: 0)
emission = scheduler.createObserver(String.self)
storage = scheduler.createObserver(String.self)
disposeBag = DisposeBag()
}
func testHappyPath() {
let storeProducer = scheduler.createColdObservable([.next(10, "store"), .completed(11)])
let remoteProducer = scheduler.createColdObservable([.next(20, "remote"), .completed(21)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "store"), .next(20, "remote"), .completed(21)])
XCTAssertEqual(storage.events, [.next(20, "remote"), .completed(21)])
}
func testSameValue() {
let storeProducer = scheduler.createColdObservable([.next(10, "store"), .completed(11)])
let remoteProducer = scheduler.createColdObservable([.next(20, "store"), .completed(21)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "store"), .completed(21)])
XCTAssertEqual(storage.events, [.completed(21)])
}
func testRemoteFirst() {
let storeProducer = scheduler.createColdObservable([.next(20, "store"), .completed(21)])
let remoteProducer = scheduler.createColdObservable([.next(10, "remote"), .completed(11)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "remote"), .completed(11)])
XCTAssertEqual(storage.events, [.next(20, "remote"), .completed(21)])
}
func testRemoteFirstSameValue() {
let storeProducer = scheduler.createColdObservable([.next(20, "store"), .completed(21)])
let remoteProducer = scheduler.createColdObservable([.next(10, "store"), .completed(11)])
let (value, store) = worker(store: storeProducer.asObservable(), remote: remoteProducer.asObservable())
disposeBag.insert(
value.subscribe(emission),
store.subscribe(storage)
)
scheduler.start()
XCTAssertEqual(emission.events, [.next(10, "store"), .completed(11)])
XCTAssertEqual(storage.events, [.completed(21)])
}
}
Previous Answer
I'd be inclined to aim for a usage like this:
let result = authorWorker.fetch(id: 1)
.share()
result
.map { $0.description }
.catchErrorJustReturn("")
.bind(to: myLabel.rx.text)
.disposed(by: disposeBag)
result
.subscribe(onError: { error in
// handle error here
})
.disposed(by: disposeBag)
The above can be accomplished if you have something like the below for example:
extension AuthorWorker {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable.merge(store.fetch(id: id), remote.fetch(id: id))
.distinctUntilChanged()
}
}
extension AuthorStore {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable.create { observer in
self.fetch(id: id, completion: { result in
switch result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
}
})
return Disposables.create()
}
}
}
extension AuthorRemote {
func fetch(id: Int) -> Observable<AuthorType> {
return Observable.create { observer in
self.fetch(id: id, completion: { result in
switch result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
observer.onError(error)
}
})
return Disposables.create()
}
}
}

Related

Show error response in alert RxSwift using Driver

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
)
}
}

Cannot convert Result<Bool, Error> to expected condition type 'Bool'. Swift

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)
}

Transform callback approach to reactive with Combine

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()
}

How to write unit tests for Firestore

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.

RxSwift pagination

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.