Facebook login using RxSwift - facebook

I'm trying to implement the following RxSwift example:
Login in with facebook in my application -> retrieve the user information -> retrieve user's profile photo.
I have these three functions and they must be executed in this order: requestAccess() -> fetchUserInformation() -> fetchUserPhoto()
func requestAccess() -> Observable<(ACAccount)> {
return create { observer -> Disposable in
let accountStore = ACAccountStore()
let accountType = accountStore.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierFacebook)
let dictionary: [NSObject : AnyObject] = [ACFacebookAppIdKey:"***APPID***", ACFacebookPermissionsKey:["public_profile", "email", "user_friends"]]
accountStore.requestAccessToAccountsWithType(accountType, options: dictionary) { granted, error in
if granted == false || error != nil {
sendError(observer, error ?? UnknownError)
} else {
let accounts = accountStore.accountsWithAccountType(accountType)
let account = accounts.last as! ACAccount
sendNext(observer, account)
sendCompleted(observer)
}
}
return AnonymousDisposable({})
}
}
func fetchUserInformation(account: ACAccount) -> Observable<User> {
return create { observer -> Disposable in
let url = NSURL(string: "https://graph.facebook.com/me")
let request = SLRequest(forServiceType: SLServiceTypeFacebook, requestMethod: .GET, URL: url, parameters: nil)
request.account = account
request.performRequestWithHandler { (data, response, error) -> Void in
if data == nil || response == nil {
sendError(observer, error ?? UnknownError)
} else {
let result: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil)
let user = User()
user.updateInformationWithJSON(result! as! JSONObject)
sendNext(observer, user)
sendCompleted(observer)
}
}
return AnonymousDisposable({})
}
}
func fetchUserPhoto(user: User) -> Observable<AnyObject> {
return create { observer -> Disposable in
let url = NSURL(string: "https://graph.facebook.com/***myid***/picture")
let params = ["redirect":"false", "height":"200", "width":"200"]
let request = SLRequest(forServiceType: SLServiceTypeFacebook, requestMethod: .GET, URL: url, parameters: params)
request.account = SocialController.account
request.performRequestWithHandler { (data, response, error) -> Void in
if data == nil || response == nil {
sendError(observer, error ?? UnknownError)
} else {
let result: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: nil)
sendNext(observer, result!)
sendCompleted(observer)
}
}
return AnonymousDisposable({})
}
}
I already tried to implement this flow but it doesn't feel right. What is the best way to solve this problem?
requestAccess()
>- subscribeNext { account in
fetchUserInformation(account)
>- map { user in return UserViewModel(model: user) }
>- subscribeNext { viewModel in self.viewModel = viewModel }
}

Have you tried using flatMap?
It's an equivalent to then in the JavaScript bluebird or Q world. The difference between map and flatMap is that flatMap must return an Observable<T> which will then be unwrapped in the following block/closure.
requestAccess()
>- flatMap{ account in
return fetchUserInformation(account)
}
>- map { user in
return UserViewModel(model:user)
}
>- subscribeNext { viewModel in
self.viewModel = viewModel
}
Tidbit #1: Consider using unowned self when referencing self to avoid a retain cycle.
Tidbit #2: These two are pretty much the same thing.
flatMap { return just("hello") }
>- subscribeNext{ greeting in println(greeting) }
map { return "hello" }
>- subscribeNext{ greeting in println(greeting) }

private
func requestFacebookAccess(_ viewController: UIViewController) -> Observable<LoginManagerLoginResult?> {
return Observable.create { observer -> Disposable in
let loginManager = LoginManager()
loginManager.logIn(permissions: ["public_profile", "email"], from: viewController, handler: { result, error in
observer.onNext(result)
observer.onCompleted()
})
return Disposables.create()
}
}

Related

How can I call a function from a Swift file and use it in a ViewController?

I have a Swift file that gets details about the user that is currently logged in/signed up named CognitoUserPoolController.swift
import Foundation
import AWSCognitoIdentityProvider
class CognitoUserPoolController {
let userPoolRegion: AWSRegionType = "Private Info"
let userPoolID = "Private Info"
let appClientID = "Private Info"
let appClientSecret = "Private Info"
var userPool:AWSCognitoIdentityUserPool?
var currentUser:AWSCognitoIdentityUser? {
get {
return userPool?.currentUser()
}
}
static let sharedInstance: CognitoUserPoolController = CognitoUserPoolController()
private init() {
let serviceConfiguration = AWSServiceConfiguration(region: userPoolRegion, credentialsProvider: nil)
let poolConfiguration = AWSCognitoIdentityUserPoolConfiguration(clientId: appClientID,
clientSecret: appClientSecret,
poolId: userPoolID)
AWSCognitoIdentityUserPool.register(with: serviceConfiguration,
userPoolConfiguration: poolConfiguration,
forKey:"AWSChat")
userPool = AWSCognitoIdentityUserPool(forKey: "AWSChat")
AWSDDLog.sharedInstance.logLevel = .verbose
}
func login(username: String, password:String, completion:#escaping (Error?)->Void) {
let user = self.userPool?.getUser(username)
let task = user?.getSession(username, password: password, validationData:nil)
task?.continueWith(block: { (task: AWSTask<AWSCognitoIdentityUserSession>) -> Any? in
if let error = task.error {
completion(error)
return nil
}
completion(nil)
return nil
})
}
func signup(username: String, password:String, emailAddress:String, completion:#escaping (Error?, AWSCognitoIdentityUser?)->Void) {
var attributes = [AWSCognitoIdentityUserAttributeType]()
let emailAttribute = AWSCognitoIdentityUserAttributeType(name: "email", value: emailAddress)
attributes.append(emailAttribute)
print(emailAttribute.value!)
let task = self.userPool?.signUp(username, password: password, userAttributes: attributes, validationData: nil)
task?.continueWith(block: {(task: AWSTask<AWSCognitoIdentityUserPoolSignUpResponse>) -> Any? in
if let error = task.error {
completion(error, nil)
return nil
}
guard let result = task.result else {
let error = NSError(domain: "Private Info",
code: 100,
userInfo: ["__type":"Unknown Error", "message":"Cognito user pool error."])
completion(error, nil)
return nil
}
completion(nil, result.user)
return nil
})
}
func confirmSignup(user: AWSCognitoIdentityUser, confirmationCode:String, completion:#escaping (Error?)->Void) {
let task = user.confirmSignUp(confirmationCode)
task.continueWith { (task: AWSTask<AWSCognitoIdentityUserConfirmSignUpResponse>) -> Any? in
if let error = task.error {
completion(error)
return nil
}
completion(nil)
return nil
}
}
func resendConfirmationCode(user: AWSCognitoIdentityUser, completion:#escaping (Error?)->Void) {
let task = user.resendConfirmationCode()
task.continueWith { (task: AWSTask<AWSCognitoIdentityUserResendConfirmationCodeResponse>) -> Any? in
if let error = task.error {
completion(error)
return nil
}
completion(nil)
return nil
}
}
func getUserDetails(user: AWSCognitoIdentityUser, completion:#escaping (Error?, AWSCognitoIdentityUserGetDetailsResponse?)->Void) {
let task = user.getDetails()
task.continueWith(block: { (task: AWSTask<AWSCognitoIdentityUserGetDetailsResponse>) -> Any? in
if let error = task.error {
completion(error, nil)
return nil
}
guard let result = task.result else {
let error = NSError(domain: "Private Info",
code: 100,
userInfo: ["__type":"Unknown Error", "message":"Cognito user pool error."])
completion(error, nil)
return nil
}
completion(nil, result)
return nil
})
}
}
After a user successfully signs up they are presented with HomeViewController. In HomeViewController I try to print an attribute email value like this but it does not work
import UIKit
import AWSCognitoIdentityProvider
class HomeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let userpoolController = CognitoUserPoolController.sharedInstance
userpoolController.getUserDetails(user: userpoolController.currentUser!) { (error: Error?, details:AWSCognitoIdentityUserGetDetailsResponse?) in
view.backgroundColor = .green // This line of code works, but below this line it does not.
if let loggedInUserAttributes = details?.userAttributes {
self.view.backgroundColor = .systemPink
for attribute in loggedInUserAttributes {
if attribute.name?.compare("email") == .orderedSame {
print ("Email address of logged-in user is \(attribute.value!)")
}
}
}
}
}
}
The background color successfully changes to green but does not change to pink (That was to see if the code was working.) Inside of the if let statement is where the code is not working and there are not any errors. How can I properly fix this?

flatMap Not returning onCompleted

I have created below function with chaining of multiple observables however whatever I do it does not seem to call completed ? it only return the following:
(facebookSignInAndFetchData()) -> subscribed
(facebookSignInAndFetchData()) -> Event next(())
even though when I debug the observables individually they all return completed
here is my chaining function
func facebookSignInAndFetchData() {
observerFacebook.flatMap { (provider: FacebookProvider) in
return provider.login()
}.flatMap { token in
return self.loginViewModel.rx_authenticate(token: token)
}.flatMap {
return self.loginViewModel.fetchProfileData()
}.debug().subscribe(onError: { error in
//Guard unknown ErrorType
guard let err = error as? AuthError else {
//Unknown error message
self.alertHelper.presentAlert(L10n.unknown)
return
}
//error message handling
switch err {
case .notLoggedIn:
print("not logged in")
break
default:
self.alertHelper.presentAlert(err.description)
}
}, onCompleted: {
self.goToInitialController()
}).addDisposableTo(self.disposeBag)
}
rx_authenticate
func rx_authenticate(token: String) -> Observable<Void> {
return Observable.create({ observer in
let credentials = SyncCredentials.facebook(token: token)
SyncUser.logIn(with: credentials, server: URL(string: Globals.serverURL)!, onCompletion: { user, error in
//Error while authenticating
guard error == nil else {
print("error while authenticating: \(error!)")
observer.onError(AuthError.unknown)
return
}
//Error while parsing user
guard let responseUser = user else {
print("error while authenticating: \(error!)")
observer.onError(AuthError.unknown)
return
}
//Authenticated
setDefaultRealmConfiguration(with: responseUser)
//next
observer.onNext()
//completed
observer.onCompleted()
})
return Disposables.create()
})
}
fetchProfileData
func fetchProfileData() -> Observable<Void> {
return Observable.create({ observer in
//Fetch facebookData
let params = ["fields" : "name, picture.width(480)"]
let graphRequest = GraphRequest(graphPath: "me", parameters: params)
graphRequest.start {
(urlResponse, requestResult) in
switch requestResult {
case .failed(_):
//Network error
observer.onError(AuthError.noConnection)
break
case .success(let graphResponse):
if let responseDictionary = graphResponse.dictionaryValue {
guard let identity = SyncUser.current?.identity else {
//User not logged in
observer.onError(AuthError.noUserIdentity)
return
}
//Name
let name = responseDictionary["name"] as! String
//Image dictionary
let pictureDic = responseDictionary["picture"] as! [String: Any]
let dataDic = pictureDic["data"] as! [String: Any]
let imageHeight = dataDic["height"] as! Int
let imageWidth = dataDic["width"] as! Int
let url = dataDic["url"] as! String
//Create Person object
let loggedUser = Person()
loggedUser.id = identity
loggedUser.name = name
//Create photo object
let photo = Photo()
photo.height = imageHeight
photo.width = imageWidth
photo.url = url
//Append photo object to person object
loggedUser.profileImage = photo
//Save userData
let realm = try! Realm()
try! realm.write {
realm.add(loggedUser, update: true)
}
//next
observer.onNext()
//completed
observer.onCompleted()
} else {
//Could not retrieve responseData
observer.onError(AuthError.noResponse)
}
}
}
return Disposables.create()
})
}
observerFacebook
//FacebookProvider
private lazy var observerFacebook: Observable<FacebookProvider>! = {
self.facebookButton.rx.tap.map {
return FacebookProvider(parentController: self)
}
}()
The chain starts with calling observerFacebook, which returns an observable that will emit values everytime facebookButton is tapped.
This observable will only complete when facebookButton gets released, most probably when the view controller holding it is removed from screen.
The rest of the chain will map or flatMap, but never force completion as another tap will trigger the whole chain again.
The easy way to solve this would be to add a call to take(1) on facebookButton.rx.tap, so that the function would be defined like so:
private lazy var observerFacebook: Observable<FacebookProvider>! = {
self.facebookButton.rx.tap
.take(1)
.map {
return FacebookProvider(parentController: self)
}
}()
Now, observerFacebook will complete after the first tap and you should see a call to onCompleted.
Note that you'll need to resubscribe to the chain on errors if you want to perform it again when another tap comes in.

App freezes when requesting access to addressbook

func getContacts() {
let store = CNContactStore()
if CNContactStore.authorizationStatus(for: .contacts) == .notDetermined {
store.requestAccess(for: .contacts, completionHandler: { (authorized: Bool, error: NSError?) -> Void in
if authorized {
self.retrieveContactsWithStore(store: store)
}
} as! (Bool, Error?) -> Void)
} else if CNContactStore.authorizationStatus(for: .contacts) == .authorized {
self.retrieveContactsWithStore(store: store)
}
}
func retrieveContactsWithStore(store: CNContactStore) {
do {
let groups = try store.groups(matching: nil)
let predicate = CNContact.predicateForContactsInGroup(withIdentifier: groups[0].identifier)
//let predicate = CNContact.predicateForContactsMatchingName("John")
let keysToFetch = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactEmailAddressesKey] as [Any]
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch as! [CNKeyDescriptor])
self.objects = contacts
DispatchQueue.main.async(execute: { () -> Void in
self.myTableView.reloadData()
})
} catch {
print(error)
}
}
I was trying to retrieve contacts from address book, but whenever I go to the view calling getContacts(), the app freezes. It wouldn't proceed anymore, but it didn't crash either. I wonder what went wrong here?
Your code for the call to requestAccess isn't correct. The syntax for the completion handler isn't valid. You need this:
func getContacts() {
let store = CNContactStore()
let status = CNContactStore.authorizationStatus(for: .contacts)
if status == .notDetermined {
store.requestAccess(for: .contacts, completionHandler: { (authorized: Bool, error: Error?) in
if authorized {
self.retrieveContactsWithStore(store: store)
}
})
} else if status == .authorized {
self.retrieveContactsWithStore(store: store)
}
}
Also note the change to use the status variable. This is cleaner and easier to read than calling authorizationStatus over and over. Call it once and then check the value over and over as needed.

Async issue in call using ObjectiveDropboxOfficial SDK

I'm experiencing an issue with a function that should return an encrypted file from dropbox, but is instead returning the empty dictionary I initialized to receive the data.
I'm almost certain it's a race condition issue since an async call has to be made to the Dropbox API, but so far I have been unable to resolve the issue using GCD. Any help would be most appreciated:
func loadDropboxAccounts() {
let retreiveDataGroup = dispatch_group_create()
var dictionary = [String:String]()
dispatch_group_enter(retreiveDataGroup)
if DropboxClientsManager.authorizedClient() == nil {
DropboxClientsManager.authorizeFromController(UIApplication.sharedApplication(), controller: self, openURL: {(url: NSURL) -> Void in
UIApplication.sharedApplication().openURL(url)
}, browserAuth: true)
}
if let client = DropboxClientsManager.authorizedClient() {
client.filesRoutes.downloadData("/example/example.txt").response({(result: AnyObject?, routeError: AnyObject?, error: DBError?, fileContents: NSData) -> Void in
if (fileContents.length != 0) {
let cipherTextData = fileContents
let plainTextByteArray = CryptoHelper.accountDecrypt(cipherTextData, fileName: "vault")
let plainTextString = plainTextByteArray.reduce("") { $0 + String(UnicodeScalar($1)) }
let plainTextData = dataFromByteArray(plainTextByteArray)
do {
try dictionary = NSJSONSerialization.JSONObjectWithData(plainTextData, options: []) as! [String:String]
for (key, value) in dictionary {
let t = dictionary[key]
print(t)
}
} catch let error as NSError{
print("loadAccountInfo:", error)
}
} else {
print("\(routeError)\n\(error)\n")
}
}).progress({(bytesDownloaded: Int64, totalBytesDownloaded: Int64, totalBytesExpectedToDownload: Int64) -> Void in
print("\(bytesDownloaded)\n\(totalBytesDownloaded)\n\(totalBytesExpectedToDownload)\n")
})
}
dispatch_group_notify(retreiveDataGroup, dispatch_get_main_queue()) {
return dictionary
}
}
Just for reference, this the pod that I am using in the project:
https://github.com/dropbox/dropbox-sdk-obj-c

Alamofire number of requests one after another

I have a number of requests witch I would like to call one after another without having nested spaghetti code.
I tried it already with a serial dispatch queue
let queue = dispatch_queue_create("label", DISPATCH_QUEUE_SERIAL)
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(1)
}
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(2)
}
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(3)
}
But unfortunately that does not work. The output of this can be 1,3,2 or 3,1,2 or any other combination.
What would be the best approach to get the output 1,2,3 so one after the other.
Ok I ended up writing my own implementation.
I created a class RequestChain wich takes Alamofire.Request as parameter
class RequestChain {
typealias CompletionHandler = (success:Bool, errorResult:ErrorResult?) -> Void
struct ErrorResult {
let request:Request?
let error:ErrorType?
}
private var requests:[Request] = []
init(requests:[Request]) {
self.requests = requests
}
func start(completionHandler:CompletionHandler) {
if let request = requests.first {
request.response(completionHandler: { (_, _, _, error) in
if error != nil {
completionHandler(success: false, errorResult: ErrorResult(request: request, error: error))
return
}
self.requests.removeFirst()
self.start(completionHandler)
})
request.resume()
}else {
completionHandler(success: true, errorResult: nil)
return
}
}
}
And I use it like this
let r1 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
print("1")
}
let r2 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
print("2")
}
let r3 = Alamofire.request(Router.Countries).responseArray(keyPath: "endpoints") { (response: Response<[CountryModel],NSError>) in
print("3")
}
let chain = RequestChain(requests: [r1,r2,r3])
chain.start { (success, errorResult) in
if success {
print("all have been success")
}else {
print("failed with error \(errorResult?.error) for request \(errorResult?.request)")
}
}
Importent is that you are telling the Manager to not execute the request immediately
let manager = Manager.sharedInstance
manager.startRequestsImmediately = false
Hope it will help someone else
I'm using Artman's Signals to notify my app once a result is returned, after which a queue elsewhere can call it's next request:
Alamofire.request( httpMethod, url, parameters: params ).responseJSON
{
( response: Response< AnyObject, NSError > ) in
self._signals.dispatchSignalFor( Key: url, data: response.result )
}
More details here.
One solution is to call your second request in the first one's callback :
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(1)
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(2)
Alamofire.request(Router.Countries).responseString { (response:Response<String, NSError>) in
print(3)
}
}
}