How to use Combine to show elastic search results using network while falling back on cache in Swift - swift

I have a function which returns a list of Items using elastic search and falls back on realm cache. I'm wondering how can I use Combine to achieve the same.
I am trying to do something like this where I have a publisher for each store but I am getting stuck on the sorting them by score.
func search(for text: String) -> AnyPublisher<[Item], Error> {
return store.search(with: text)
// Invalid syntax *
.map { searchResults in
let sorted = cacheStore.search(with: text)
.map { items in
items
.map { item in (item, searchResults.first { $0.id == item.id }?.score ?? 0) }
.sorted { $0.1 > $1.1 } // by score
.map { $0.0 } // to item
}
return sorted.eraseToAnyPublisher()
}
// *
.catch { _ in cacheStore.search(with: text) }
.eraseToAnyPublisher()
}
This is the original function.
func search(for text: String, completion: #escaping (Result<[Item], Error>) -> Void) {
store.search(with: text) {
// Search network via elastic search or fall back to cache search
// searchResults is of type [(id: Int, score: Double)] where id is item.id
guard let searchResult = $0.value, $0.isSuccess else {
return self.cacheStore.search(with: text, completion: completion)
}
self.cacheStore.fetch(ids: searchResult.map { $0.id }) {
guard let items = $0.value, $0.isSuccess else {
return self.cacheStore.search(with: text, completion: completion)
}
let scoredItems = items
.map { item in (item, searchResult.first { $0.id == item.id }?.score ?? 0) }
.sorted { $0.1 > $1.1 } // by score
.map { $0.0 } // to item
completion(.success(scoredItems))
}
}
}

I think what you are aiming for is something like the Playground below.
Most of the playground is code is code that mocks up searches using Futures. The particularly relevant section is:
return searchNetwork(key: key)
.map { key,value in cache[key] = value; return value }
.catch {_ in searchCache(key: key) }
.eraseToAnyPublisher()
If the network request from searchNetwork succeeds then the value passes through the map which adds it to the cache and returns the value from the network. If searchNetwork fails then catch will substitute the publisher that searches the cache.
import Foundation
import Combine
var cache = [
"one" : "for the money",
"two" : "for the show"
]
enum SearchError: Error {
case cacheMiss
case networkFailure
}
func searchCache(key : String) -> AnyPublisher<String, SearchError>
{
return Future<String, SearchError> { fulfill in
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
if let value = cache[key] {
fulfill(.success(value))
} else {
fulfill(.failure(.cacheMiss))
}
}
}.eraseToAnyPublisher()
}
func searchNetwork(key: String) -> AnyPublisher<(String, String), SearchError> {
return Future<(String, String), SearchError> { fulfill in
fulfill(.failure(.networkFailure))
}.eraseToAnyPublisher()
}
func search(for key: String) -> AnyPublisher<String, SearchError> {
return searchNetwork(key: key)
.map { key,value in cache[key] = value; return value }
.catch {_ in searchCache(key: key) }
.eraseToAnyPublisher()
}
let searchForOne = search(for: "one").sink(
receiveCompletion: { debugPrint($0) },
receiveValue: { print("Search for one : \($0)") }
)
let searchForThree = search(for: "three").sink(
receiveCompletion: { debugPrint($0) },
receiveValue: { print("Search for three : \($0)") }
)

I figured out the solution by doing something like this:
let cachedPublisher = cacheStore.search(with: text)
let createPublisher: (Item) -> AnyPublisher<Item, Error> = {
return Just($0).eraseToAnyPublisher()
}
return store.search(with: request)
.flatMap { Item -> AnyPublisher<[Item], Error> in
let ids = searchResults.map { $0.id }
let results = self.cacheStore.fetch(ids: ids, filterActive: true)
.flatMap { items -> AnyPublisher<[Item], Error> in
let sorted = items
.map { item in (item, searchResults.first { $0.id == item.id }?.score ?? 0) }
.sorted { $0.1 > $1.1 } // by score
.map{ $0.0 } // to item
return Publishers.mergeMappedRetainingOrder(sorted, mapTransform: createPublisher) // Helper function that calls Publishers.MergeMany
}
return results.eraseToAnyPublisher()
}
.catch { _ in cachedPublisher }
.eraseToAnyPublisher()

Related

How to map response from Single tuple

I have a response create with single that return tuples, i want to get if the value $0.0 return false it should get request from API. And if the value is $0.1 return true, it should go to next viewController. I already try this, but i'm stuck because xcode give a yellow warning that the variable in requestEmail is unused. it means i used driver wrong.
this is my code
class LoginVM: ViewModelType {
struct Input {
let emailText: Driver<String>
let validate: Driver<Void>
}
struct Output {
let loading: Driver<Bool>
let gotoLoginScreen: Driver<Void>
let errorNoInternetConnection: Driver<Void>
}
private let manageSession: ManageSessionProtocol
private let manageAccountDetail: ManageAccountDetailProtocol
init(manageSession: ManageSessionProtocol, manageAccountDetail: ManageAccountDetailProtocol) {
self.manageSession = manageSession
self.manageAccountDetail = manageAccountDetail
}
func transform(input: Input) -> Output {
let errorRouter = ErrorRouter()
let checkEmailRequest = input.validate
.withLatestFrom(input.emailText)
.filter { $0.isValidEmail }
.map { emailText in
self.manageSession.checkEmail(email: emailText)
.rerouteError(errorRouter)
.asDriver(onErrorJustReturn: (false, false))
}
let requestEmail = checkEmailRequest
.flatMapLatest { $0.asSharedSequence() }
.map { args in
if args.0 == false {
self.manageSession.requestEmailLoginChallenge()
.rerouteError(errorRouter)
.asDriver(onErrorJustReturn: .init(workflowId: 0, lastAttempt: false, lastState: 0, nextRequestInSec: 0))
}
}
let openPassword = checkEmailRequest
.flatMapLatest { $0.asSharedSequence() }
.map { args in
if args.1 == true {
}
}
return Output(loading: <#Driver<Void>#>, gotoLoginScreen: <#T##Driver<Void>#>, errorNoInternetConnection: <#Driver<Void>#>)
}
}
You want to do something like this:
let checkEmailRequest = input.validate
.withLatestFrom(input.emailText)
.filter { $0.isValidEmail }
.flatMapLatest { [manageSession] emailText in
manageSession.checkEmail(email: emailText)
.rerouteError(errorRouter)
.asDriver(onErrorJustReturn: (false, false))
}
let requestEmail = checkEmailRequest
.filter { $0.0 == false }
.flatMapLatest { [manageSession] _ in
manageSession.requestEmailLoginChallenge()
.rerouteError(errorRouter)
.asDriver(onErrorJustReturn: .init(workflowId: 0, lastAttempt: false, lastState: 0, nextRequestInSec: 0))
}
let openPassword = checkEmailRequest
.filter { $0.1 == true }

Am I using firebase api incorrectly?

Whenever paging in tableview, the view model is running fetchDataRx. It works well, but I don't think it's right to sort the entire data every time you paginate and call the addSnapShotListener. If my code is not correct, how can I correct it?
// MARK: ViewController.swift
timeLineTableView.rx.didScroll
.withLatestFrom(viewModel.activated)
.subscribe(onNext: { [weak self] isActivated in
if !isActivated {
guard let self = self else { return }
let position = self.timeLineTableView.contentOffset.y
if position > self.timeLineTableView.contentSize.height - 100 - self.timeLineTableView.frame.size.height {
self.viewModel.fetchPosts.onNext(())
}
}
})
.disposed(by: disposeBag)
//MARK: ViewModel.swift
let fetchPosts: AnyObserver<Void>
let fetching = PublishSubject<Void>()
fetchPosts = fetching.asObserver()
fetching
.do(onNext: { _ in activating.onNext(true) })
.withLatestFrom(posts)
.map { $0.count }
.flatMap{ (count) -> Observable<[post]> in
fireBaseService.fetchDataRx(startIdx: count) }
.map { $0.map { ViewPost(post: $0) } }
.do(onNext: { _ in activating.onNext(false) })
.do(onError: { err in error.onNext(err) })
.subscribe(onNext: { newPosts in
let oldData = posts.value
posts.accept(oldData + newPosts)
})
.disposed(by: disposeBag)
//MARK: FirebaseService.swift
protocol FirebaseServiceProtocol {
func fetchDataRx(startIdx: Int) -> Observable<[post]>
func fetchData(startIdx: Int, completion: #escaping (Result<[post], Error>) -> Void)
}
class FireBaseService: FirebaseServiceProtocol {
func fetchDataRx(startIdx: Int) -> Observable<[post]> {
return Observable.create { (observer) -> Disposable in
self.fetchData(startIdx: startIdx) { result in
switch result {
case .success(let data):
observer.onNext(data)
case .failure(let error):
observer.onError(error)
}
observer.onCompleted()
}
return Disposables.create()
}
}
func fetchData(startIdx: Int, completion: #escaping (Result<[post], Error>) -> Void) {
let db = Firestore.firestore()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
if startIdx == 0 {
DispatchQueue.global().async {
let first = db.collection("lolCourt")
.order(by: "date")
.limit(to: 8)
var nextPosts = [post]()
first.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in querySnapshot!.documents {
guard let url = document.data()["url"] as? String else {
continue
}
guard let champion1 = document.data()["champion1"] as? String else {
continue
}
guard let champion1Votes = document.data()["champion1Votes"] as? Double else {
continue
}
guard let champion2 = document.data()["champion2"] as? String else {
continue
}
guard let champion2Votes = document.data()["champion2Votes"] as? Double else {
continue
}
guard let text = document.data()["text"] as? String else {
continue
}
guard let date = document.data()["date"] as? Double else {
continue
}
nextPosts.append(post(url: url,
champion1: champion1,
champion1Votes: champion1Votes,
champion2: champion2,
champion2Votes: champion2Votes,
text: text,
date: formatter.string(from: Date(timeIntervalSince1970: date))))
}
}
completion(.success(nextPosts))
}
}
}
else {
DispatchQueue.global().async {
let first = db.collection("lolCourt")
.order(by: "date")
.limit(to: startIdx)
first.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error retrieving : \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
return
}
let next = db.collection("lolCourt")
.order(by: "date")
.start(afterDocument: lastSnapshot)
.limit(to: 8)
var nextPosts = [post]()
next.getDocuments() { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in querySnapshot!.documents {
guard let url = document.data()["url"] as? String else {
continue
}
guard let champion1 = document.data()["champion1"] as? String else {
continue
}
guard let champion1Votes = document.data()["champion1Votes"] as? Double else {
continue
}
guard let champion2 = document.data()["champion2"] as? String else {
continue
}
guard let champion2Votes = document.data()["champion2Votes"] as? Double else {
continue
}
guard let text = document.data()["text"] as? String else {
continue
}
guard let date = document.data()["date"] as? Double else {
continue
}
nextPosts.append(post(url: url,
champion1: champion1,
champion1Votes: champion1Votes,
champion2: champion2,
champion2Votes: champion2Votes,
text: text,
date: formatter.string(from: Date(timeIntervalSince1970: date))))
}
}
completion(.success(nextPosts))
}
}
}
}
}
}
I think you are doing it wrong... I would expect to see something more like this:
class FireBaseService {
func getPage<T>(query: Query? = nil, build: #escaping ([String: Any]) -> T) -> Observable<([T], nextPage: Query?)> {
Observable.create { observer in
let db = Firestore.firestore()
let page = query ?? db.collection("lolCourt")
.order(by: "date")
.limit(to: 8)
let listener = page
.addSnapshotListener { snapshot, error in
guard let snapshot = snapshot else { observer.onError(error ?? RxError.unknown); return }
let items = snapshot.documents.map { build($0.data()) }
if let lastSnapshot = snapshot.documents.last {
let next = page
.start(afterDocument: lastSnapshot)
observer.onSuccess((items, nextPage: next))
}
else {
observer.onSuccess((items, nextPage: nil))
}
}
return Disposables.create { listener.remove() }
}
}
}
Use the above in your favorite state machine system. Here is an example using my CLE library.
// in view controller
let fireBaseService = FireBaseService()
let activityIndicator = ActivityIndicator()
let errorRouter = ErrorRouter()
func getPage(nextPage: Query?) -> Observable<([Post?], nextPage: Query?)> {
fireBaseService.getPage(query: nextPage, build: Post.init(dict:))
.rerouteError(errorRouter)
.trackActivity(activityIndicator)
}
let posts = cycle(
inputs: [
getPage(nextPage: nil).map(ViewModel.Input.response),
timeLineTableView.rx.reachedBottom(offset: 20).map(to: ViewModel.Input.next)
],
initialState: ([Post?](), nextPage: Query?.none),
environment: getPage(nextPage:),
reduce: ViewModel.reduce(state:input:getPage:)
)
.map { $0.0.compactMap { $0 } }
and the view model:
enum ViewModel {
enum Input {
case response([Post?], nextPage: Query?)
case next
}
static func reduce(state: inout ([Post?], nextPage: Query?), input: Input, getPage: #escaping (Query) -> Observable<([Post?], nextPage: Query?)>) -> Observable<Input> {
switch input {
case let .response(posts, nextPage):
state.0 += posts
state.nextPage = nextPage
case .next:
guard let nextPage = state.nextPage else { break }
return getPage(nextPage)
.map(Input.response)
}
return .empty()
}
}

Combine pipeline for object with image

I have built a simplified combine pipeline in my Xcode Playground, to make car objects [CarWithImage] from an array of cars, [Car]. That seems to work fine. But I would like the pipeline to check each car object for imageString, and if it isn't nil fetch it with the function getImage(_:). I have commented that code out, because I get the error type of expression is ambiguous without more context and I don't know how to fix that. I would also like to introduce a delay in the pipeline to more realistically simulate a network download of cars and images, and set the CarWithImage image property to nil if the image fetching fails.
I have a Xcode Playground repository on GitHub where you can test out my code. First page is with the original class, the second page is with trying out compactMap: Cars Playground
The code will run in Xcode Playground:
import UIKit
import Combine
struct Car {
let name: String
let imageString: String?
}
struct CarWithImage {
let name: String
let image: UIImage?
}
final class CarClass {
let myCars = [Car(name: "Tesla", imageString: "car"), Car(name: "Volvo", imageString: nil)]
let delayCar = 4
let delayImage = 6
func getVehicles() -> AnyPublisher<[CarWithImage], Error> {
myCars.publisher
.flatMap { car in
// if let imageString = car.imageString {
// getImage(imageString)
// .flatMap { image in
// return Just(CarWithImage(name: car.name, image: image))
// }
// }
return Just(CarWithImage(name: car.name, image: nil))
}
.collect()
.flatMap { cars in
cars.publisher.setFailureType(to: Error.self)
}
.collect()
.eraseToAnyPublisher()
}
func getImage(_ string: String) -> AnyPublisher<UIImage, Error> {
Just(UIImage(systemName: string)!)
.flatMap { image in
Just(image).setFailureType(to: Error.self)
}
.eraseToAnyPublisher()
}
}
let carClass = CarClass()
carClass.getVehicles()
.sink(receiveCompletion: { print($0)}) { cars in
cars.forEach { car in
let haveImage = car.image != nil
let string = haveImage ? "and it have an image" : ""
print("The car is", car.name, string)
}
}
// This is just to check that the getImage function works
carClass.getImage("car")
.sink(receiveCompletion: { print($0)}) { image in
print("Got image", image)
}
After suggestion to use compactMap, I have modified the class, but now I only get cars when the car have an image:
final class CarClass {
let myCars = [Car(name: "Tesla", imageString: "bolt.car"), Car(name: "Volvo", imageString: nil)]
let delayCar = 4
let delayImage = 6
func getVehicles() -> AnyPublisher<[CarWithImage], Error> {
myCars.publisher
.flatMap { car in
self.getImage(car.imageString)
.compactMap { $0 }
.flatMap { image in
return Just(CarWithImage(name: car.name, image: image))
}
}
.collect()
.flatMap { cars in
cars.publisher.setFailureType(to: Error.self)
}
.collect()
.eraseToAnyPublisher()
}
func getImage(_ string: String?) -> AnyPublisher<UIImage?, Error> {
guard let imageString = string else { return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher() }
return Just(UIImage(systemName: imageString))
.flatMap { image in
Just(image).setFailureType(to: Error.self)
}
.eraseToAnyPublisher()
}
}
To simulate the delay you can use the Delay Publisher:
func getImage(_ string: String) -> AnyPublisher<UIImage, Error> {
Just(UIImage())
.flatMap { image in
Just(image).setFailureType(to: Error.self)
}
.delay(for: .seconds(1), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}
To check for a nil imageString you can use the CompactMap Publisher. The tricky part is if you get the image in FlatMap, you also need to pass the Car down the pipeline so that you have access to it's name in order to construct the CarWtihImage. You can use the Zip Publisher for just that.
Note that you need to limit the concurrent number of Publishers with FlatMap or you won't get the delay that you were after.
typealias CarAndImagePublisher = Publishers
.Zip<AnyPublisher<Car, Error>, AnyPublisher<UIImage, Error>>
func getVehicles() -> AnyPublisher<[CarWithImage], Error> {
myCars.publisher
.compactMap { car -> Car? in
return car.imageString != nil ? car : nil
}
.flatMap(maxPublishers: .max(1)) { car -> CarAndImagePublisher in
guard let imageString = car.imageString else { fatalError() }
print(imageString)
return Publishers.Zip(
Just(car).setFailureType(to: Error.self).eraseToAnyPublisher(),
getImage(imageString)
)
}
.map { value -> CarWithImage in
print(value.0.name)
return CarWithImage(name: value.0.name, image: value.1)
}
.collect()
.eraseToAnyPublisher()
}
I finally got the code working the way I wanted. It is in Cars4 page of my Playground. If a Car have a property with an image string, the CarWithImage is populated with the image, else the image property is set to nil. To prove that the cars have image or not, we print out at the end of the Playground the name of the cars. The following shows the working code:
import UIKit
import Combine
struct Car {
let name: String
let imageString: String?
}
struct CarWithImage {
let name: String
let image: UIImage?
}
final class CarClass {
let myCars = [Car(name: "Tesla", imageString: "bolt.car"), Car(name: "Volvo", imageString: nil)]
let delayCar = 4
let delayImage = 6
func getVehicles() -> AnyPublisher<[CarWithImage], Error> {
myCars.publisher
.flatMap { car in
self.getImage(car.imageString)
.flatMap { image -> Just<CarWithImage> in
guard let image = image else { return Just(CarWithImage(name: car.name, image: nil)) }
return Just(CarWithImage(name: car.name, image: image))
}
}
.collect()
.flatMap { cars in
cars.publisher.setFailureType(to: Error.self)
}
.collect()
.eraseToAnyPublisher()
}
func getImage(_ string: String?) -> AnyPublisher<UIImage?, Error> {
guard let imageString = string else { return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher() }
return Just(UIImage(systemName: imageString))
.flatMap { image in
Just(image).setFailureType(to: Error.self)
}
.eraseToAnyPublisher()
}
}
let carClass = CarClass()
carClass.getVehicles()
.sink(receiveCompletion: { print($0)}) { cars in
cars.forEach { car in
let haveImage = car.image != nil
let string = haveImage ? "and it have an image" : ""
print("The car is", car.name, string)
}
}
Here's an option. This one will emit all the CarWithImage's as a single Publisher containing an Array of objects.
func example(cars: [Car]) -> AnyPublisher<[CarWithImage], Never> {
zip(cars.map { car in
getImageOrNil(car.imageString)
.map { CarWithImage(name: car.name, image: $0) }
})
.eraseToAnyPublisher()
}
func getImageOrNil(_ string: String?) -> AnyPublisher<UIImage?, Never> {
return getImage(string)
.catch { _ in Just(nil).eraseToAnyPublisher() }
.eraseToAnyPublisher()
}
func getImage(_ string: String?) -> AnyPublisher<UIImage?, Error> {
guard let imageString = string else { return Just(nil).setFailureType(to: Error.self).eraseToAnyPublisher() }
return Just(UIImage(systemName: imageString))
.flatMap { image in
Just(image).setFailureType(to: Error.self)
}
.eraseToAnyPublisher()
}
func zip<S, Output, Failure>(_ array: S) -> AnyPublisher<[Output], Failure>
where S: Collection, S.Element: Publisher, S.Element.Output == Output, S.Element.Failure == Failure {
guard let first = array.first else {
return Just([]).setFailureType(to: Failure.self).eraseToAnyPublisher()
}
let rest = array.dropFirst()
return first.map { [$0] }.zip(zip(rest)).map { $0.0 + $0.1 }.eraseToAnyPublisher()
}
After posting the above, I looked at your Cars4 code and that helped clean mine up. Using collect instead of zip.
func getImages(for cars: [Car]) -> AnyPublisher<[CarWithImage], Never> {
return cars.publisher
.flatMap { car in
getImageOrNil(car.imageString)
.map { CarWithImage(name: car.name, image: $0) }
}
.collect()
.eraseToAnyPublisher()
}

RxSwift withLatestFrom with resultSelector doesn't compile

I have a Driver of type Bool and a BehaviorRelay of type Page (which is a custom enum).
enum Page {
case option1(CustomClass1, CustomClass2)
case option2(CustomClass3)
case option3(CustomClass4)
var property1: CustomClass2? {
switch self {
case .option1(_, let custom):
return custom
case .option2, .option3:
return nil
}
}
}
I have the Driver<Bool> in another ViewModel.
class ViewModel1 {
struct Output {
let hasItems: Driver<Bool>
}
let output: Output
init() {
let hasItemsRelay: BehaviorRelay<Bool> = BehaviorRelay<Bool>(value: false)
self.output = Output(
hasItems: hasItemsRelay.asDriver()
)
}
}
And I have a BehaviorRelay<Page?> in my base class.
class ViewModel2 {
let currentPageRelay: BehaviorRelay<Page?> = BehaviorRelay<Page?>(value: nil)
init() {
self.currentPageRelay = BehaviorRelay<Page?>(value: nil)
}
}
In ViewModel2 class I'm trying to catch an event on the hasItems driver of ViewModel1.Input and when I get an event, I need the current value of currentPageRelay and later on do stuff with it. So basically withLatestFrom is the thing I need to use.
class ViewModel2 {
private func test() {
let customViewModel: ViewModel1 = ViewModel1()
customViewModel
.output
.hasItems
.withLatestFrom(currentPageRelay) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.unwrap()
.drive(onNext: { (property1, hasItems) in
// do stuff
}
.disposed(by: disposeBag)
}
}
Xcode completely loses it on the withLatestFrom. No code completion and it gives the following compile error:
Expression type '(Bool, _)' is ambiguous without more context
I'm completely in the dark about this one. I've already tried everything, providing the correct classes in the parameter list below it, so that it knows what to expect etc, but no luck so far.
I think because .withLatestFrom requires both types it operates on to be of the same observable trait. So both should be either Observable, Driver, Signal, etc.
If you want to keep your Driver in your viewModel a Driver you could add an .asObservable() after the .hasItems:
class ViewModel2 {
let currentPageRelay: BehaviorRelay<Page?> = BehaviorRelay<Page?>(value: nil)
let disposeBag = DisposeBag()
init() {
// self.currentPageRelay = BehaviorRelay<Page?>(value: nil)
}
private func test() {
let customViewModel: ViewModel1 = ViewModel1()
customViewModel
.output
.hasItems
.asObservable()
.withLatestFrom(currentPageRelay) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.asDriver(onErrorJustReturn: nil)
.drive(onNext: {
guard let (property1, hasItems) = $0 else {
return
}
// do stuff
})
.disposed(by: disposeBag)
}
}
Or add a .asDriver() to currentPageRelay in the withLatestFrom(..):
customViewModel
.output
.hasItems
.withLatestFrom(currentPageRelay.asDriver()) { ($0, $1) }
.map { (hasItems, page) -> (CustomClass2, Bool)? in
guard let property1 = page?.property1 else { return nil }
return (property1, hasItems)
}
.drive(onNext: {
guard let (property1, hasItems) = $0 else {
return
}
// do stuff
})
.disposed(by: disposeBag)

How to check Observable count in rxswift?

I have this method return User observable
internal func getUser() -> Observable<User> {
let result = withRealm("getting user") { realm -> Observable<User> in
let realm = try Realm()
let user = realm.objects(User.self).first
return Observable.from(optional: user)
}
return result ?? .empty()
}
Now i want to have another method that returns me if user is loggedin, how it will be done?
private var _isLoggedIn: Observable<User> {
return getUser().count > 0 //error
}
I would suggest moving to enum to get rid of optionals on make it easier to manage:
enum LoginState {
case loggedIn(user: User)
case loggedOut
}
internal func getLoginState() -> Observable<LoginState> {
let user = withRealm("getting user") { realm -> User? in
let realm = try Realm()
return realm.objects(User.self).first
}
if let user = user {
return Observable.just(.loggedIn(user))
} else {
return Observable.just(.loggedOut)
}
}
private var _isLoggedIn: Observable<Bool> {
return getLoginState().map {
switch $0 {
case .loggedIn: return true
case .loggedOut: return false
}
}
}