handle nested observables in RxSwift - ios

I'm trying to combine facebook login with a rest call, so when the user is logged in it should make an authenticate call to the server, where the server makes the graph calls, however I'm a bit confused to how I nest the calls with RxSwift? so far I have a FacebookProvider class with following method
func login() -> Observable<String> {
return Observable.create({ observer in
let loginManager = LoginManager()
//LogOut before
loginManager.logOut()
//Set Login Method
loginManager.loginBehavior = .native
//Login Closure
loginManager.logIn([ .publicProfile, .userFriends, .email], viewController: self.parentController) { loginResult in
switch loginResult {
case .failed(let error):
print(error)
observer.onError(FacebookError.NoConnection(L10n.networkError))
case .cancelled:
print("User cancelled login.")
case .success(_, let declinedPermissions, let accessToken):
print("Logged in!")
guard declinedPermissions.count > 0 else {
observer.onError(FacebookError.DeclinedPermission(L10n.declinedPermission))
return
}
observer.onNext(accessToken.authenticationToken)
observer.onCompleted()
}
}
return Disposables.create()
})
}
Then I have a LoginViewModel with this model
public func retrieveUserData() -> Observable<User> {
return Network.provider
.request(.auth(fbToken: Globals.facebookToken)).retry(5).debug().mapObject(User.self)
}
then I in my UIViewController do this
facebookProvider.validate().subscribe({ [weak self] response in
switch response {
case .error(_):
// User is not logged in push to loginController
break
case .next():
//user is logged in retrieveUserData before proceeding
self?.loginViewModel.retrieveUserData().subscribe { event in
switch event {
case .next(let response):
print(response)
case .error(let error):
print(error)
case .completed:
print("completed")
}
}.addDisposableTo(self?.disposeBag)
break
case .completed:
//data is retrieved and can now push to app
break
}
}).addDisposableTo(disposeBag)
Validate
public func rx_validate() -> Observable<String> {
return Observable.create({ observer in
//Check if AccessToken exist
if AccessToken.current == nil {
observer.onError(FacebookError.NotLoggedIn)
} else {
observer.onNext(Globals.accessToken)
}
observer.onCompleted()
return Disposables.create()
})
}

You will want to use flatMap
The closure passed to flatMap will return an observable. flatMap will then take care of un-nesting it, meaning if the closure returns a value of type Observable<T>, and you call flatMap on a value of type Observable<U>, the resulting observable will be Observable<T> (an not Observable<Observable<T>>
In this particular case, the code would look like this:
facebookProvider.validate().flatMap { [weak self] _ in
return self?.loginViewModel.retrieveUserData()
}.subscribe { event in
switch event {
// ...
}
}.addDisposableTo(disposeBag)
On a side note, you should probably update func retrieveUserData() to accept the token as a parameter, instead of fetching it from your Globals structure.
The resulting code would look similar to this
public func retrieveUserData(token: String) -> Observable<User> {
return Network.provider
.request(.auth(fbToken: token)).retry(5).debug().mapObject(User.self)
}
in viewController
facebookProvider.validate().flatMap { [weak self] token in
return self?.loginViewModel.retrieveUserData(token: token)
}.subscribe { event in
switch event {
// ...
}
}.addDisposableTo(disposeBag)

Related

RxSwift practice to show/hide ActivityIndicator

Let's say we have this pseudocode representing a network request call and show/hide an activity indicator, using RxSwift:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest()
.subscribe(onNext: {
self.isLoading.accept(false)
}, onError: {
self.isLoading.accept(false)
})
}
The function executeRequest returns either an Observable or Single.
I am not feeling comfortable with having to write twice the same code, for onNext/onSuccess and onError, basically doing the same.
I am looking for suggestions to minimize/improve turning off the activity indicator, like for example handling all events of the request in a single statement and avoid using the subscribe function. Or maybe there are other suggestions?
I use ActivityIndicator from RxSwift Example app, which makes it really convenient, especially if your loading multiple things in parallel as it maintains a count of running subscriptions and emit false only when this count is equal to 0:
let isLoading = ActivityIndicator()
func performRequests() {
self.network
.executeFirstRequest()
.trackActivity(isLoading)
.subscribe {
// ...
}
self.network
.executeSecondRequest()
.trackActivity(isLoading)
.subscribe {
// ...
}
}
You can use another method to subscribe, which passes Event in case of Observer or SingleEvent in case of Single:
subscribe(on: (Event<T>) -> Void)
subscribe(observer: (SingleEvent<T>) -> Void)
Observer Example:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest().subscribe {
switch $0 {
case let .error(error):
print(error)
case let .next:
print("good")
case .completed:
print("also good")
}
isLoading.accept(false)
}
}
Single Example:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest().subscribe {
switch $0 {
case let .error(error):
print(error)
case let .next:
print("good")
}
isLoading.accept(false)
}
}

iOS: Calling AWSMobileClient initialize() makes getUserAttributes() not invoking callback

Here is a code snippet I am trying to get it work but without success so far. initialize() works fine but then getUserAttributes() is not triggering the callback. Not just getUserAttributes(), even other AWS calls such as getTokens() not triggering either. Believe, some where down inside AWS code, it is getting blocked. If I comment out initialize() then getUserAttributes() callback gets invoked. Tried various options with DispatchQueue/DispatchGroup, no help.
AWSMobileClient pod version 2.12.7.
import Foundation
import AWSMobileClient
struct AWSUser {
static let serialQueue = DispatchQueue(label: "serialQueue")
static let group = DispatchGroup()
static func initialize() -> Void {
DispatchQueue.global(qos: .background).async {
AWSInitialize()
getAWSUserAttributes()
}
}
static func AWSInitialize() -> Void {
group.enter()
AWSMobileClient.default().initialize { (userState, error) in
// error handling ...
switch userState {
case .signedIn:
//getAWSUserAttributes()
break
default:
break
}
group.leave()
}
}
static func getAWSUserAttributes() {
group.wait()
group.enter()
AWSMobileClient.default().getUserAttributes { (attrs, error) in
// NEVER REACHED!!!
// BUT WORKS IF AWSMobileClient.default().initialize() is commented out
group.leave()
}
}
}
For Getting Callback or trigger any event of AWSMobile Client, Make sure you have implemented below code in AppDelegate or respective view controller. If this method implement then function is trigger...
//Initialised Use Pool
func intializeUserPool() -> Void {
AWSDDLog.sharedInstance.logLevel = .verbose // TODO: Disable or reduce log level in production
AWSDDLog.add(AWSDDTTYLogger.sharedInstance) // TTY = Log everything to Xcode console
//Important for event handler
initializeAWSMobileClient()
}
// Add user state listener and initialize the AWSMobileClient
func initializeAWSMobileClient() {
AWSMobileClient.default().initialize { (userState, error) in
print("Initialise userstate:\(String(describing: userState)) and Info:\(String(describing: error))")
if let userState = userState {
switch(userState){
case .signedIn: // is Signed IN
print("Logged In")
print("Cognito Identity Id (authenticated): \(String(describing: AWSMobileClient.default().identityId))")
case .signedOut: // is Signed OUT
print("Logged Out")
print("Cognito Identity Id (unauthenticated): \(String(describing: AWSMobileClient.default().identityId))")
case .signedOutUserPoolsTokenInvalid: // User Pools refresh token INVALID
print("User Pools refresh token is invalid or expired.")
default:
self.signOut()
}
} else if let error = error {
print(error.localizedDescription)
}
}
//Register State
self.addUserStateListener() // Register for user state changes
}
// AWSMobileClient - a realtime notifications for user state changes
func addUserStateListener() {
AWSMobileClient.default().addUserStateListener(self) { (userState, info) in
print("Add useruserstate:\(userState) and Info:\(info)")
switch (userState) {
case .signedIn:
print("Listener status change: signedIn")
DispatchQueue.main.async {
self.getSession()
}
case .signedOut:
print("Listener status change: signedOut")
case .signedOutFederatedTokensInvalid:
print("Listener status change: signedOutFederatedTokensInvalid")
default:
print("Listener: unsupported userstate")
}
}
}

Combine Future block called multiple times when using Flatmap and multiple subscribers

I've been successfully using BrightFutures in my apps mainly for async network requests. I decided it was time to see if I could migrate to Combine. However what I find is that when I combine two Futures using flatMap with two subscribers my second Future code block is executed twice. Here's some example code which will run directly in a playground:
import Combine
import Foundation
extension Publisher {
func showActivityIndicatorWhileWaiting(message: String) -> AnyCancellable {
let cancellable = sink(receiveCompletion: { _ in Swift.print("Hide activity indicator") }, receiveValue: { (_) in })
Swift.print("Busy: \(message)")
return cancellable
}
}
enum ServerErrors: Error {
case authenticationFailed
case noConnection
case timeout
}
func authenticate(username: String, password: String) -> Future<Bool, ServerErrors> {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}
func downloadUserInfo(username: String) -> Future<String, ServerErrors> {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> some Publisher {
return authenticate(username: username, password: password).flatMap { (isAuthenticated) -> Future<String, ServerErrors> in
guard isAuthenticated else {
return Future {$0(.failure(.authenticationFailed)) }
}
return downloadUserInfo(username: username)
}
}
let future = authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
let cancellable2 = future.showActivityIndicatorWhileWaiting(message: "Please wait downloading")
let cancellable1 = future.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) { (output) in
print("received userInfo: '\(output)'")
}
The code simulates making two network calls and flatmaps them together as a unit which either succeeds or fails.
The resulting output is:
Calling server to authenticate
Busy: Please wait downloading
Downloading user info
Downloading user info <---- unexpected second network call
Hide activity indicator
received userInfo: 'decoded user data'
Completed without errors.
The problem is downloadUserInfo((username:) appears to be called twice. If I only have one subscriber then downloadUserInfo((username:) is only called once. I have an ugly solution that wraps the flatMap in another Future but feel I missing something simple. Any thoughts?
When you create the actual publisher with let future, append the .share operator, so that your two subscribers subscribe to a single split pipeline.
EDIT: As I've said in my comments, I'd make some other changes in your pipeline. Here's a suggested rewrite. Some of these changes are stylistic / cosmetic, as an illustration of how I write Combine code; you can take it or leave it. But other things are pretty much de rigueur. You need Deferred wrappers around your Futures to prevent premature networking (i.e. before the subscription happens). You need to store your pipeline or it will go out of existence before networking can start. I've also substituted a .handleEvents for your second subscriber, though if you use the above solution with .share you can still use a second subscriber if you really want to. This is a complete example; you can just copy and paste it right into a project.
class ViewController: UIViewController {
enum ServerError: Error {
case authenticationFailed
case noConnection
case timeout
}
var storage = Set<AnyCancellable>()
func authenticate(username: String, password: String) -> AnyPublisher<Bool, ServerError> {
Deferred {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}.eraseToAnyPublisher()
}
func downloadUserInfo(username: String) -> AnyPublisher<String, ServerError> {
Deferred {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}.eraseToAnyPublisher()
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> AnyPublisher<String, ServerError> {
let authenticate = self.authenticate(username: username, password: password)
let pipeline = authenticate.flatMap { isAuthenticated -> AnyPublisher<String, ServerError> in
if isAuthenticated {
return self.downloadUserInfo(username: username)
} else {
return Fail<String, ServerError>(error: .authenticationFailed).eraseToAnyPublisher()
}
}
return pipeline.eraseToAnyPublisher()
}
override func viewDidLoad() {
super.viewDidLoad()
authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
.handleEvents(
receiveSubscription: { _ in print("start the spinner!") },
receiveCompletion: { _ in print("stop the spinner!") }
).sink(receiveCompletion: {
switch $0 {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) {
print("received userInfo: '\($0)'")
}.store(in: &self.storage)
}
}
Output:
start the spinner!
Calling server to authenticate
Downloading user info
received userInfo: 'decoded user data'
stop the spinner!
Completed without errors.

Swift iOS Facebook SDK loop on login with safari view controller

I'm trying to add login with facebook in iOS app written in swift.
the problem here is that when I fill the login proccess in SSFSafariViewController and click Done it re-opens the login dialog in SSFSafariViewController and keeps doing that for ever.
here is my code:
// Loads user data if he is logged in, othewise asks him to login
static func login(ViewController controller: UIViewController) -> Bool {
// If user has logged in before: dont ask him to login again
// use previus login data
if(AccessToken.current != nil){
// Load user data
create();
return true;
}
// If user is'nt logged in: ask him to login
else {
var result: Bool = false;
let loginManager = LoginManager()
loginManager.logIn([ .publicProfile, .userFriends, .email], viewController: controller) { loginResult in
switch loginResult {
case .failed(let error):
print(error)
result = false;
case .cancelled:
result = false;
case .success( _, _, _):
result = true;
}
}
// If user has logged in: laod his data
if(result){
create();
}
return result;
}
}
I have solved my problem, it looks like is was a problem with the delegate where SSFSafariViewController presented by Facebook SDK wasn't calling back! and the SDK Never got the result of the SSFSafariViewController so it was trying to resolve the problem by re-presenting the login view SSFSafariViewController and that what cased the loop over and over again.
so the solution was to add this function to AppDelegate:
#available(iOS 9.0, *)
func application(application: UIApplication,openURL url: NSURL, options: [String: AnyObject]) -> Bool {
return FBSDKApplicationDelegate.sharedInstance().application(application, open: url as URL!, sourceApplication: UIApplicationOpenURLOptionsKey.sourceApplication.rawValue, annotation: UIApplicationOpenURLOptionsKey.annotation);
}
then every thing works out!
Try this:
You can return value while you used closure inside the method . so you need to create function with closure as below :
Note: you need to solve bracket error
func login(ViewController controller: UIViewController, callback: (_ isResult: Bool) -> Void) -> Void {
if(AccessToken.current != nil){
// Load user data
callback(true)
}
// If user is'nt logged in: ask him to login
else {
var result: Bool = false;
let loginManager = LoginManager()
loginManager.logIn([ .publicProfile, .userFriends, .email], viewController: controller) { loginResult in
switch loginResult {
case .failed(let error):
print(error)
callback(false)
case .cancelled:
callback(false)
case .success( _, _, _):
callback(true)
}
}
}
Calling method
self.login(ViewController: self) { (isResult) in
// isResult your boolean
if(isResult){
create();
}
}

RxSwift callback return first before result

I am using Firebase FirAuth API and before the API return result, Disposables.create() has been returned and it's no longer clickable (I know this might due to no observer.onCompleted after the API was called. Is there a way to wait for it/ listen to the result?
public func login(_ email: String, _ password: String) -> Observable<APIResponseResult> {
let observable = Observable<APIResponseResult>.create { observer -> Disposable in
let completion : (FIRUser?, Error?) -> Void = { (user, error) in
if let error = error {
UserSession.default.clearSession()
observer.onError(APIResponseResult.Failure(error))
observer.on(.completed)
return
}
UserSession.default.user.value = user!
observer.onNext(APIResponseResult.Success)
observer.on(.completed)
return
}
DispatchQueue.main.async {
FIRAuth.auth()?.signIn(withEmail: email, password: password, completion: completion)
}
return Disposables.create()
}
return observable
}
You are correct in your assumption that an onError / onCompletion event terminate the Observable Sequence. Meaning, the sequence won't emit any more events, in any case.
As a sidenote to that, You don't need to do .on(.completed) after .onError() , since onError already terminates the sequence.
the part where you write return Disposables.create() returns a Disposable object, so that observable can later be added to a DisposeBag that would handle deallocating the observable when the DisposeBag is deallocated, so it should return immediately, but it will not terminate your request.
To understand better what's happening, I would suggest adding .debug() statements around the part that uses your Observable, which will allow you to understand exactly which events are happening and will help you understand exactly what's wrong :)
I had the same issue some time ago, I wanted to display an Alert in onError if there was some error, but without disposing of the observable.
I solved it by catching the error and returning an enum with the cases .success(MyType) and .error(Error)
An example:
// ApiResponseResult.swift
enum ApiResponseResult {
case error(Error)
case success(FIRUser)
}
// ViewModel
func login(...) -> Observable<ApiResponseResult> {
let observable = Observable.create { ... }
return observable.catchError { error in
return Observable<ApiResponseResult>.just(.error(error))
}
}
// ViewController
viewModel
.login
.subscribe(onNext: { result in
switch result {
case .error(let error):
// Alert or whatever
break
case .success(let user):
// Hurray
break
}
})
.addDisposableTo(disposeBag)

Resources