Facebook ios sdk (obj-c to swift) issue - ios

I am trying to bring obj-c code to swift (facebook ios sdk), but autocomplete(intellisense) does not work in handler and I get an error (marked in code) : Set NSObject does not have a member named 'containsObject'
#IBAction func loginWithFacebook(sender: AnyObject) {
/*
FBSDKLoginManager *login = [[FBSDKLoginManager alloc] init];
[login logInWithReadPermissions:#[#"email"] handler:^(FBSDKLoginManagerLoginResult *result, NSError *error) {
if (error) {
// Process error
} else if (result.isCancelled) {
// Handle cancellations
} else {
// If you ask for multiple permissions at once, you
// should check if specific permissions missing
if ([result.grantedPermissions containsObject:#"email"]) {
// Do work
}
}
}];
*/
let fbLoginManager = FBSDKLoginManager()
fbLoginManager.logInWithReadPermissions(["email"], handler: {
result, error in
if ((error) != nil){
}
else if (result.isCancelled){
} else {
if(result.grantedPermissions.containsObject("email")){ //<-- error here
}
}
})
}

Because Swift 1.2 automatically casts all NSSet objects (the ones that coming from external libs/sdks/frameworks etc...) to Set structure you need to call contains instead of containsObject for such things (doc).

Related

How can I observe when user authenticates the app with biometrics?

Simply in code I use it like this:
let context = LAContext()
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "jjj") { success, error in
print(error)
print(success)
}
}
Then user can see:
Everything is fine until user tap Cancel. Then I display label:
"Please use biometrics to authenticate". NOW I need to get a callback after user was authenticated at any time after first try was cancelled. How can I detect this?
You don’t need a “callback” for this. If the user refuses authentication in response to the dialog, the only way authentication can happen is in Settings, i.e. outside your app. So just check for authentication every time your app comes to the foreground.
Try with code Obj-C, I think that Swift is the same logic
self.context = [[LAContext alloc] init];
[self.context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:strMessage
reply:^(BOOL success, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
if (error.code == LAErrorUserFallback) {
//Do some thing
}else if (error.code == LAErrorAuthenticationFailed) {
//User authen failed
}else if (error.code == LAErrorUserCancel) {
//User cancel
}else{
//Something wrong...
}
return;
}
if (success) {
//Success
} else {
//Failed
return;
}
});
}];

FBSDKLoginManager not handling logInWithPublishPermissions: correctly

I'm implementing v4.1 of the SDK for iOS and when I try to call for publishPermissions:, I get no callback.
For some reason everything works perfectly when I run logInWithReadPermissions:, but when I run logInWithPublishPermissions: it never hits my response handler. Nothing happens.
To test things out, I reset my loginManager before running logInWithPublishPermissions:, and to my surprise it worked then (aka NSLog(#"RESULT") is called).
Am I missing something about how the loginManager works? Shouldn't I be able to use it without resetting it?
// FacebookController.m
#implementation FacebookController
FBSDKLoginManager *loginManager;
static FacebookController *_shared = nil;
- (id)init {
self = [super init];
if (self != nil) {
userData = [[NSMutableDictionary alloc] init];
loginManager = [[FBSDKLoginManager alloc] init];
}
return self;
}
+ (id)getInstance {
if (!_shared) {
_shared = [[self alloc] init];
}
return _shared;
}
- (bool)hasPublishPermissions {
FBSDKAccessToken *accessToken = [FBSDKAccessToken currentAccessToken];
if(accessToken != NULL){
NSSet *permissions = [accessToken permissions];
if([permissions containsObject:#"publish_actions"]){
return TRUE;
}
}
return FALSE;
}
- (void)requestPublishPermissionsWithDelegate:(id)aDelegate {
if(![self hasPublishPermissions]){
// FOR SOME REASON IT WORKS IF I RESET LOGIN MANAGER AS FOLLOWS
// loginManager = [[FBSDKLoginManager alloc] init];
[loginManager logInWithPublishPermissions:#[#"publish_actions"] handler:^(FBSDKLoginManagerLoginResult *result, NSError *error) {
NSLog(#"RESULT: %#", result);
}];
}
}
- (void)connectToFacebookWithDelegate:(id)aDelegate {
FBSDKAccessToken *accessToken = [FBSDKAccessToken currentAccessToken];
if(accessToken != nil){
[aDelegate performSelector:#selector(facebookSignedIn)];
} else {
[loginManager logInWithReadPermissions:#[#"email"] handler:^(FBSDKLoginManagerLoginResult *result, NSError *error) {
if (error) {
// Process error
NSLog(#"ERROR");
} else if (result.isCancelled) {
// Handle cancellations
NSLog(#"CANCELLED");
} else {
NSLog(#"SUCCESS");
[aDelegate performSelector:#selector(facebookSignedIn)];
}
}];
}
}
#end
Edit #1:
Including videos of it working and not working for the given scenarios:
Not working (loginManager reinitialization commented out):
https://dl.dropboxusercontent.com/u/14277258/not-working.mov
Working (loginManager reinitialized):
https://dl.dropboxusercontent.com/u/14277258/working.mov
Your video stack trace indicates you're calling the request for publish permissions inside the handler for your initial login. This should be avoided:
You're causing another login after the user has already granted you some permissions - it's not very good for the user to have to see another login dialog immediately after completing one.
You're asking for publish permissions when you don't need it - this may violate Facebook developer policies and again is not the best user experience. Instead you should asking for publish only when you need it (i.e., at the time of sharing).
If you really insist, you can dispatch your second login call asynchronously so that the first request finishes entirely but I wouldn't recommend it. We can probably update the SDK to detect this and log though so it's not as confusing.

ios facebook sdk 4.0 login error code 304

I've just updated facebook sdk v4.0
and according the tutorial of Using Custom Login UIs
-(IBAction)facebookLoginClick:(id)sender {
FBSDKLoginManager *login = [[FBSDKLoginManager alloc] init];
[login logInWithReadPermissions:#[#"email"] handler:^(FBSDKLoginManagerLoginResult *result, NSError *error) {
if (error) {
// Process error
} else if (result.isCancelled) {
// Handle cancellations
} else {
// If you ask for multiple permissions at once, you
// should check if specific permissions missing
if ([result.grantedPermissions containsObject:#"email"]) {
// Do work
}
}
}];
}
BUT the result is always nil and error code is 304, am I missing something?
I had a similar problem.
After initialising FBSDKLoginManager I added a line to flush out the data and the (Facebook)Token:
FBSDKLoginManager *loginmanager= [[FBSDKLoginManager alloc]init];
[loginmanager logOut];
Thus, exactly as the OP asks, "am I missing something"?
Yes, the following standard example code which is seen everywhere, is simply wrong:
-(IBAction)facebookLoginClick:(id)sender
{
FBSDKLoginManager *login = [[FBSDKLoginManager alloc] init];
---- ONE MAGIC LINE OF CODE IS MISSING HERE ----
[login logInWithReadPermissions:#[#"email"]
handler:^(FBSDKLoginManagerLoginResult *result, NSError *error)
{
if (error) {...}
else if (result.isCancelled) {...}
else { // (NB for multiple permissions, check every one)
if ([result.grantedPermissions containsObject:#"email"])
{ NSLog(#"%#",result.token); }
}
}];
}
you must do this:
-(IBAction)facebookLoginClick:(id)sender
{
FBSDKLoginManager *login = [[FBSDKLoginManager alloc] init];
[login logOut]; //ESSENTIAL LINE OF CODE
[login logInWithReadPermissions:#[#"email"]
handler:^(FBSDKLoginManagerLoginResult *result, NSError *error)
{
if (error) {...}
else if (result.isCancelled) {...}
else { // (NB for multiple permissions, check every one)
if ([result.grantedPermissions containsObject:#"email"])
{ NSLog(#"%#",result.token); }
}
}];
}
Otherwise, very simply, the app will not work if the user happens to change FB accounts on the device. (Unless they happen to for some reason re-install the app!)
Once again - the popular sample code above simply does not work (the app goes in to an endless loop) if a user happens to change FB accounts. The logOut call must be made.
This happened to me when I changed the Facebook AppID (in the Info.plist file) switching form a test Facebook app to a production Facebook app. If your app sends the Facebook token to a server that generate a JWT for for instance, Facebook SDK still persists the fbToken (FBSDKAccessToken.currentAccessToken()) persisted unless it's told to remove it.
That may also happen in production when a user logs out of the app, a log out API request will log the user out and reset your JWT. But even before sending the log out API request, the client will need to tell the FBSDKLoginManager instance to log out.
if let currentAccessToken = FBSDKAccessToken.currentAccessToken() where currentAccessToken.appID != FBSDKSettings.appID()
{
loginManager.logOut()
}
I had this, it occurred when changing the FB app details in Xcode while an iOS app was running. You need to delete the app from the device and then republish with the new FB app setup. Just recompiling is not enough to clear the old FB settings
Swift 3.0
func loginWithFacebook
{
let loginManager = FBSDKLoginManager()
if let currentAccessToken = FBSDKAccessToken.current(), currentAccessToken.appID != FBSDKSettings.appID()
{
loginManager.logOut()
}
loginManager.logIn(withReadPermissions: ["public_profile","email"], from: self, handler: { (result, error) in
if error != nil {
print("error\(String(describing: error))")
}
else if (result?.isCancelled)! {
}
else {
print(FBSDKAccessToken.current())
}
})
}
You can use the following code to solve your problem :
[FBSDKAccessToken refreshCurrentAccessToken:^(FBSDKGraphRequestConnection *connection, id result, NSError *error){}
swift 4.0, 4.2, 5.0
FBSDKLoginManager().logOut()
According to the docs, 304 FBSDKLoginUserMismatchErrorCode "Indicates a failure to request new permissions because the user has changed".
Make sense in the scenario #Adrian999 mentioned.
Swift 4.0 and FBSDKCoreKit 4.33
let loginManager = LoginManager()
if let currentAccessToken = AccessToken.current, currentAccessToken.appId != SDKSettings.appId
{
loginManager.logOut()
}
Actually, the reason is very simple, you can try following step to reappear this error code easily.
The "Facebook login fail" may have two reasons:
you get the user info from facebook fail.
you get the user info from facebook success,but upload to your own server fail.
The code in FBSDKLoginManager.m is:
- (void)validateReauthentication:(FBSDKAccessToken *)currentToken withResult:(FBSDKLoginManagerLoginResult *)loginResult
{
FBSDKGraphRequest *requestMe = [[FBSDKGraphRequest alloc] initWithGraphPath:#"me"
parameters:nil
tokenString:loginResult.token.tokenString
HTTPMethod:nil
flags:FBSDKGraphRequestFlagDoNotInvalidateTokenOnError | FBSDKGraphRequestFlagDisableErrorRecovery];
[requestMe startWithCompletionHandler:^(FBSDKGraphRequestConnection *connection, id result, NSError *error) {
NSString *actualID = result[#"id"];
if ([currentToken.userID isEqualToString:actualID]) {
[FBSDKAccessToken setCurrentAccessToken:loginResult.token];
[self invokeHandler:loginResult error:nil];
} else {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[FBSDKInternalUtility dictionary:userInfo setObject:error forKey:NSUnderlyingErrorKey];
NSError *resultError = [NSError errorWithDomain:FBSDKLoginErrorDomain
code:FBSDKLoginUserMismatchErrorCode
userInfo:userInfo];
[self invokeHandler:nil error:resultError];
}
}];
}
when currentToken.userID is not equal actualID, the FBSDKLoginUserMismatchErrorCode will throw.
Now, this issue will reappear when user A login facebook fail,but you do not have [FacebookSDKManager logOut] after the fail, the app will cache the accessToken for the user A , and then user A change facebook account to user B, when user B login facebook again,it will reappear this issue.
I was also stuck on this issue for a while. then i created object for FBSDKLoginManager in .h file and in viewDidLoad of .m file , initialize it and set logout property.
_fbLogin= [[FBSDKLoginManager alloc]init];
[_fbLogin logOut];
This helped me and hope helps you as well.
All the best.
Thanks
Single Sign On
I've also found that this issue happens if you have an app that does not require single sign on (i.e. an app which multiple users will create content and share from one device).
You can change single sign on in your app settings https://developers.facebook.com/apps/your-app-id/settings/.
For me in Objective-C following worked
FBSDKLoginManager *login = [[FBSDKLoginManager alloc] init];
if ([FBSDKAccessToken currentAccessToken]) {
[login logOut];
}
and issue was user was trying to login as another user as suggested by #Vineeth Joseph
Check the state of your FBSession:
isOpen
Indicates whether the session is open and ready for use.
#property (readonly) BOOL isOpen;
Declared In: FBSession.h
https://developers.facebook.com/docs/reference/ios/3.0/class/FBSession/
304 indicates that nothing has changed. Probably you are somehow already authenticated and so getting nil with 304 indicating nothing has changed.

Mocking CKContainer for Unit Testing

I am using CloudKit in my application and am trying to mock CKContainer to test my Managers. Here is what i tried:
func testAccountStatus() {
class MockCloudContainer: CKContainer {
override func accountStatusWithCompletionHandler(completionHandler: ((CKAccountStatus, NSError!) -> Void)!)
{
completionHandler(CKAccountStatus.NoAccount, NSError())
}
}
let loginManager = LoginManager.sharedInstance
let expectation = expectationWithDescription("iCloudStatus")
var isTestFinished = false
loginManager.iCloudStatusWithCompletionHandler { (status, error) -> Void in
if (isTestFinished) {
return
}
expectation.fulfill()
XCTAssertTrue(status.isEqualToString("NoAccount"), "Status is Available")
}
waitForExpectationsWithTimeout(5, { error in
isTestFinished = true
XCTAssertNil(error, "Error")
})
But i am getting error while compiling the code
:0: error: cannot override 'init' which has been marked unavailable
What is best way I am using mock object to test my LoginManager class ?
Thanks
Currently there is no way to mock the CKContainer. The only solution would be to create a thin data access layer between your app and the CloudKit code and then mock that layer. But still you would not be able to unit test that thin layer itself.
I have an idea for this problem. But I'm not good at swift so I provide my code in Objective-C
Firstly , try create CKContainerMock in your testClass like this:
#interface CKContainerMock : CKContainer
#property (nonatomic, copy) void (^mCompletionHandlerMock)(CKAccountStatus accountStatus,NSError *error);
- (void)accountStatusWithCompletionHandler:(void (^)(CKAccountStatus accountStatus, NSError *error))completionHandler
#end
#implementation CKContainerMock
- (void)accountStatusWithCompletionHandler:(void (^)(CKAccountStatus accountStatus, NSError *error))completionHandler {
self.mCompletionHandlerMock = completionHandler;
}
#end
In your testFunction
Example:
Success case:
-(void) testCKContainerResultSucces {
LoginManager loginManager = [LoginManager sharedInstance];
CKContainerMock *ckContainerMock = [[CKContainerMock alloc] init];
[loginManager setCKContainer:ckContainerMock]; // Change your ckContainer object to mockObject.
[loginManager iCloudStatusWithCompletionHandler:^(CKAccountStatus accountStatus,NSError *error) {
XCAssert(accountStatus == CKAccountStatusAvailable);
}];
loginManager.ckContainer.mCompletionHandlerMock(CKAccountStatusAvailable,nil);
}
Error case:
-(void) testCKContainerResultError {
LoginManager loginManager = [LoginManager sharedInstance];
CKContainerMock *ckContainerMock = [[CKContainerMock alloc] init];
[loginManager setCKContainer:ckContainerMock]; //Change your ckContainer object to mockObject.
[loginManager iCloudStatusWithCompletionHandler:^(CKAccountStatus accountStatus,NSError *error) {
XCAssertNotNil(error);
}];
loginManager.ckContainer.mCompletionHandlerMock(nil,[NSError errorWithDomain:NSPOSIXErrorDomain code:22 userInfo:nil]);
}
Greate mock example for you
Hope this help.

Any working sample code for CKDiscoverAllContactsOperation for IOS8 beta CloudKit?

I have been playing with icloud in the ios 8 beta, and the CloudKitAtlasAnIntroductiontoCloudKit sample project has been very helpful.
https://developer.apple.com/library/prerelease/ios/samplecode/CloudAtlas/Introduction/Intro.html
But I wanted to use the CKDiscoverAllContactsOperation class and I cannot find any sample code for it anywhere at all and the online documentation is not very helpful.
https://developer.apple.com/library/prerelease/ios/documentation/CloudKit/Reference/CKDiscoverAllContactsOperation_class/index.html
If anyone has managed to successfully use CKDiscoverAllContactsOperation could you please help point me in the right direction or show a working example of how it should be called?
I have tried this to see if I could even get an response from iCloud but nothing:
- (void)queryForRecordsOtherUsersInAddressBookcompletionHandler:(void (^)(NSArray *records))completionHandler {
CKDiscoverAllContactsOperation *discoverAllContactsOperation= [[CKDiscoverAllContactsOperation alloc] init];
[discoverAllContactsOperation setContainer:_container];
NSMutableArray *results = [[NSMutableArray alloc] init];
discoverAllContactsOperation.discoverAllContactsCompletionBlock = ^(NSArray *userInfos, NSError *operationError) {
[results addObjectsFromArray:userInfos];
};
discoverAllContactsOperation.discoverAllContactsCompletionBlock=^(NSArray *userInfos, NSError *operationError){
if (operationError) {
// In your app, handle this error with such perfection that your users will never realize an error occurred.
NSLog(#"An error occured in %#: %#", NSStringFromSelector(_cmd), operationError);
abort();
} else {
dispatch_async(dispatch_get_main_queue(), ^(void){
completionHandler(results);
});
}
};
}
and calling with this...
[self.cloudManager queryForRecordsOtherUsersInAddressBookcompletionHandler:^(NSArray *records ) {
if (records.count==0){
NSLog(#"Login name not found");
return;
}
//self.results= records;
//_loggedInRecord = self.results[0];
//NSLog(#"%#,%#",_loggedInRecord[#"lastName"],_loggedInRecord[#"firstName"]);
// [self performSegueWithIdentifier:#"loggedInSegue" sender:self ];
}];
I know the code shouldn't really do anything. Again I was just looking for a response from iCloud.
Here is what I am using. self.container is a CKContainer set with [CKContainer defaultContainer] in the init.
-(void)queryForAllUsers: (void (^)(NSArray *records))completionHandler {
CKDiscoverAllContactsOperation *op = [[CKDiscoverAllContactsOperation alloc] init];
[op setUsesBackgroundSession:YES];
op.queuePriority = NSOperationQueuePriorityNormal;
[op setDiscoverAllContactsCompletionBlock:^(NSArray *userInfos, NSError *error) {
if (error) {
NSLog(#"An error occured in %#: %#", NSStringFromSelector(_cmd), error);
//abort();
} else {
// NSLog(#"Number of records in userInfos is: %ld", (unsigned long)[userInfos count]);
dispatch_async(dispatch_get_main_queue(), ^(void){
completionHandler(userInfos);
});
}
}];
[self.container addOperation:op];
}
Before you can use the CKDiscoverAllContactsOperation operation, you first need to request for permission.
Pls use the method requestApplicationPermission:completion:
func discoverAllContacts() {
let container = CKContainer.defaultContainer()
//Request for user permission
container.requestApplicationPermission([.UserDiscoverability]) { [weak self] status, error in
switch status {
case .Granted where error == nil:
let operation = self?.discoverAllContactsOperation { usersInfo in
//do something here
}
if let operationExists = operation {
//Assuming there is a NSOperationQueue property called operationQueue
self?.operationQueue.addOperation(operationExists)
}
default:
break
}
}
}
func discoverAllContactsOperation(completionHandler: ([CKDiscoveredUserInfo]?) -> ()) -> NSOperation {
let operation = CKDiscoverAllContactsOperation()
operation.discoverAllContactsCompletionBlock = { usersInfo, error in
if error == nil {
print("Discoverd all contacts = \(usersInfo)")
completionHandler(usersInfo)
}
else {
print("Discoverd all contacts error = \(error)")
completionHandler(nil)
}
}
return operation
}

Resources