MongoDB Realm SwiftUI continue session / auto login - mongodb

I'm using SwiftUI 2 with a MongoDB synced Realm (10.5.2) and I'm currently stuck with how to continue a session once a user reopens the app.
As an authentication I use email and password via a publisher and I know that I can check if the user is logged in with this function:
app.currentUser!.isLoggedIn
which also returns true for my user. However I don't know how I can get the publisher $0 from the
app.login(credentials: .emailPassword(email: username, password: password))
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {
self.state.shouldIndicateActivity = false
switch $0 {
case .finished:
break
case .failure(let error):
self.state.error = error.localizedDescription
}
}, receiveValue: {
self.state.cardPublisher.send($0)
})
.store(in: &state.cancellables)
method where I use
self.state.cardPublisher.send($0)
to fetch my data. So my question is how do I get the $0 if the user restarts the app.
Sorry if that's a stupid question but I'm quite new to combine.
Any help is much appreciated :)
If it's helpful for you, I currently have these two publishers:
var cardPublisher = PassthroughSubject<RealmSwift.User, Error>()
let cardRealmPublisher = PassthroughSubject<Realm, Error>()
which are used like this:
cardPublisher
.receive(on: DispatchQueue.main)
.flatMap { user -> RealmPublishers.AsyncOpenPublisher in
self.shouldIndicateActivity = true
var realmConfig = user.configuration(partitionValue: "teamID=123")
realmConfig.objectTypes = [Card.self, Dog.self]
return Realm.asyncOpen(configuration: realmConfig)
}
.receive(on: DispatchQueue.main)
.map {
self.shouldIndicateActivity = false
return $0
}
.subscribe(cardRealmPublisher)
.store(in: &self.cancellables)
cardRealmPublisher
.sink(receiveCompletion: { result in
if case let .failure(error) = result {
self.error = "Failed to log in and open realm: \(error.localizedDescription)"
}
}, receiveValue: { realm in
self.cardRealm = realm
self.loadData()
})
.store(in: &cancellables)

I managed to solve it myself and it was actually quite easy. For anyone who might need it as well just use this method once the app loads:
if app.currentUser!.isLoggedIn {
app.currentUser.publisher
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: {
self.shouldIndicateActivity = false
switch $0 {
case .finished:
break
case .failure(let error):
self.error = error.localizedDescription
}
}, receiveValue: {
self.error = nil
self.cardPublisher.send($0)
})
.store(in: &self.cancellables)
} else {
// Regular Login
}

Related

Apple Combine: How to tell that a subscriber stopped listening to a publisher [duplicate]

I have this simple subscription where my subject is eminting strings. Just for curiosity I would like to know if my subscription is cancelled.
Afaik a pipeline that has been cancelled will not send any completions.
Are there some ways do achieve this?
The use case would be that I can cancel all subscriptions and receive a completion on this. Where I can clean up stuff a reflect this probably.
PlaygroundPage.current.needsIndefiniteExecution = true
var disposeBag: Set<AnyCancellable> = .init()
let subject = PassthroughSubject<String, Never>()
subject.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Failed with: \(error.localizedDescription)")
case .finished:
print("Finished")
}
}) { string in
print(string)
}.store(in: &disposeBag)
subject.send("A")
disposeBag.map { $0.cancel() }
subject.send("B")
It is possible via handling events
subject
.handleEvents(receiveCancel: {
print(">> cancelled") // << here !!
})
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Failed with: \(error.localizedDescription)")
case .finished:
print("Finished")
}
}) { string in
print(string)
}.store(in: &disposeBag)

Map, catch error and assign with Combine in SwiftUI

I learn to use Combine and I wrote this code:
serviceAgent.podcasts()
.sink(
receiveCompletion: { cpl in
switch cpl {
case .failure(let error):
print(error.localizedDescription)
case .finished:
return
}
},
receiveValue: { [weak self] (model) in
self?.items = model.map({ podcast in
let item = PodcastCardItemViewModel(podcast)
item.clickPublisher.sink { value in
self?.clickPublisher.send(value)
}.store(in: &(self!.cancelBag))
return item
})
}
).store(in: &cancelBag)
I'm pretty sure it's possible to simplify this code with map, catch and assign but I don't know how.
Any ideas?
Thanks
Advanced Combine
This is some of the hard stuff with combine. But it is doable. Without seeing the rest of your code, I don't know if this will work 100% correctly. But I was able to get this to compile on my machine.
serviceAgent
.podcasts()
.map(PodcastCardItemViewModel.init)
.flatMap { $0.clickPublisher }
.collect()
.eraseToAnyPublisher()
.sink(
receiveCompletion: { cpl in
switch cpl {
case .failure(let error):
print(error.localizedDescription)
case .finished:
return
}
},
receiveValue: { [weak self] value in
self?.clickPublisher.send(value)
}
).store(in: &cancelBag)

.sink is not returning the promise values from a Future Publisher

I have this code in lrvViewModel.swift
func getVerificationID (phoneNumber: String) -> Future<String?, Error> {
return Future<String?, Error> { promise in
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationID, error) in
if let e = error {
promise(.failure(e))
return
}
print("verification worked")
self.defaults.set(verificationID, forKey: "authVerificationID")
return promise(.success(verificationID))
}
}
}
and then i call and subscribe to the Publisher in another file like this
let _ = lrvViewModel.getVerificationID(phoneNumber: (lrvViewController?.textField.text)!)
.sink(receiveCompletion: {
print("Error worked")
// does a bunch of stuff
}, receiveValue: {
print("completion worked")
// does a bunch of stuff
})
I don't get any buildtime errors, but whenever I run the app the GetVerificationID function runs fine (prints "Verification worked"), but the code within .sink doesn't run (I don't get any print statements). What's going on?
Edit:
My solution was to give up on combine and go back to RXSwift where the code is simply:
var validateObs = PublishSubject<Any>()
func getVerificationID (phoneNumber: String) {
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationID, error) in
if let e = error {
print("v error")
self.validateObs.onError(e)
return
}
self.defaults.set(verificationID, forKey: "authVerificationID")
self.validateObs.onCompleted()
}
}
and
lrvViewModel.getVerificationID(phoneNumber: (lrvViewController?.textField.text)!)
let _ = lrvViewModel.validateObs.subscribe(onError: {
let e = $0
print(e.localizedDescription)
// do stuff
}, onCompleted: {
// do stuff
})
Was hoping to not rely on a dependency but RxSwift implementation was much easier.
If someone knows the solution to the Combine Future problem please post! I would still like to know wtf is happening. It's very possible (and likely) I'm just using combine wrong.
Edit 2:
Was using combine wrong. I can duplicate the code I had with RXSwift like this:
let verifyPub = PassthroughSubject<Any, Error>()
func getVerificationID (phoneNumber: String) {
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationID, error) in
if let e = error {
self.verifyPub.send(completion: .failure(e))
return
}
print("verification worked")
self.defaults.set(verificationID, forKey: "authVerificationID")
self.verifyPub.send(completion: .finished)
}
}
and
let subs = Set<AnyCancellable>()
let pub = lrvViewModel.verifyPub
.sink(receiveCompletion: { completion in
if case let .failure(error) = completion {
print("Error worked")
// do stuff
} else {
print("completion worked")
// do stuff
}
}, receiveValue: { _ in
print("this will never happen")
}).store(in: &subs)
I didnt' understand that in combine there are only two results to a sink, a completion or a value, and that completion is split up into multiple cases. Whereas in RxSwift you have OnNext, OnComplete, and OnError.
Shoutout to the book on Combine from raywanderlich.com. Good stuff.
What's going on is that your .sink is not followed by a .store command, so the pipeline goes out of existence before any value has a chance to come down it.
Your assignment of the pipeline to the empty _ effectively masks the problem. The compiler tried to warn you, and you shut it down.

Chaining Request in Combine

I'm struggling with combine two requests. I need id from the first one, then start the second one with this first id from the received list. I can't find a nice solution to do this with Swift Combine.
my first request looks like that CarSerview.shared.getCategories() -> AnyPublisher<[Category], CarError> :
private func getCategories() {
CarSerview.shared.getCategories()
.receive(on: DispatchQueue.main)
.map { car in
return car.id
}
.replaceError(with: [])
.assign(to: \.carsIds, on: self)
.store(in: &cancellable)
}
and the second one looks like that CarSerview.shared.getCar() -> AnyPublisher<Car, CarError>:
private func getCar(_ category: CategoryObject) {
SPService.shared.getExcursions(category)
.receive(on: DispatchQueue.main)
.map { car in
return car.cars.compactMap { $0.name }
}
.sink(receiveCompletion: { error in
print(error)
}, receiveValue: { [weak self] result in
self?.cars = result
})
}
how can in chain this two request in one?
Cannot test but the idea is to use .map + .switchToLatest, so can be like as follows
private func getCars() {
CarSerview.shared.getCategories()
.map { car in
return SPService.shared.getExcursions(car.id)
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.map { car in
return car.cars.compactMap { $0.name }
}
.sink(receiveCompletion: { error in
print(error)
}, receiveValue: { [weak self] result in
self?.cars = result
})
}

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