I am integrating Touch ID access in one of my app. I have successfully integrated it. Here is that code:
dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.75 * NSEC_PER_SEC), highPriorityQueue, ^{
dispatch_async(dispatch_get_main_queue(), ^{
LAContext *context = [[LAContext alloc] init];
isTouchExists = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil];
if (isTouchExists) {
NSString * keychainItemIdentifier;
NSString * keychainItemServiceName;
keychainItemIdentifier = #"fingerprintKeychainEntry";
keychainItemServiceName = [[NSBundle mainBundle] bundleIdentifier];
NSData * pwData = [#"the password itself does not matter" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableDictionary * attributes = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
(__bridge id)(kSecClassGenericPassword), kSecClass,
keychainItemIdentifier, kSecAttrAccount,
keychainItemServiceName, kSecAttrService, nil];
CFErrorRef accessControlError = NULL;
SecAccessControlRef accessControlRef = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecAccessControlUserPresence,
&accessControlError);
if (accessControlRef == NULL || accessControlError != NULL)
{
NSLog(#"Cannot create SecAccessControlRef to store a password with identifier “%#” in the key chain: %#.", keychainItemIdentifier, accessControlError);
}
attributes[(__bridge id)kSecAttrAccessControl] = (__bridge id)accessControlRef;
attributes[(__bridge id)kSecUseNoAuthenticationUI] = #YES;
attributes[(__bridge id)kSecValueData] = pwData;
CFTypeRef result;
OSStatus osStatus = SecItemAdd((__bridge CFDictionaryRef)attributes, &result);
if (osStatus != noErr)
{
NSError * error = [[NSError alloc] initWithDomain:NSOSStatusErrorDomain code:osStatus userInfo:nil];
NSLog(#"Adding generic password with identifier “%#” to keychain failed with OSError %d: %#.", keychainItemIdentifier, (int)osStatus, error);
}
//other my code for success
}
});
});
Now, If I remove all the fingerprints from settings in iPhone, This code will work and ask for passcode. So My question is:
how can I come to know that there is no any fingerprints added for Touch ID?
I don't want to show iOS device passcode screen as I have already built passcode screen for my app security. So is there any option to check device have atleast one fingerprint available for Touch ID access?
Thanks in advance.
======== EDIT 1 ===========
It is working on my side also. The issue is I need to check it each time when I am asking for Touch ID. I need to fetch status in viewWillAppear or in applicationDidBecomeActive each time whenever I want to use Touch ID access in app, as I am removing fingers run time, it may not reflecting in my code so I need to fetch each time.
canEvaluatePolicy:error: will be Error : LAErrorTouchIDNotEnrolled
Authentication could not start because Touch ID has no enrolled
fingers.
APPLE DOC Ref.
Try:
LAContext *context = [[LAContext alloc] init];
NSError *error;
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:#"My Reason" reply:^(BOOL success, NSError * _Nullable error) {
}];
}else{
if (error.code == LAErrorTouchIDNotEnrolled) {
NSLog(#"Error: %#", error.localizedDescription);
}
}
If there are no fingerprints registered, canEvaluatePolicy should return false.
Source : https://developer.apple.com/documentation/localauthentication/lacontext/1514149-canevaluatepolicy?language=objc
Related
In my project wanna have a flow like this:
Users record short videos -> they upload the videos on my channel -> end
To achive this result i'm trying to work with the new Google APIs Client Library for Objective-C for REST. It has a poor documentation and the examples are for mac only. Anyway after many errors this is my code:
- (void)doAuthWithoutCodeExchange:(OIDServiceConfiguration *)configuration
clientID:(NSString *)clientID
clientSecret:(NSString *)clientSecret {
NSURL *redirectURI = [NSURL URLWithString:kRedirectURI];
// builds authentication request
OIDAuthorizationRequest *request =
[[OIDAuthorizationRequest alloc] initWithConfiguration:configuration
clientId:clientID
clientSecret:clientSecret
scopes:#[ OIDScopeOpenID, OIDScopeProfile ]
redirectURL:redirectURI
responseType:OIDResponseTypeCode
additionalParameters:nil];
// performs authentication request
AppDelegate *appDelegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
[self logMessage:#"Initiating authorization request %#", request];
appDelegate.currentAuthorizationFlow =
[OIDAuthorizationService presentAuthorizationRequest:request
presentingViewController:self
callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse,
NSError *_Nullable error) {
if (authorizationResponse) {
OIDAuthState *authState =
[[OIDAuthState alloc] initWithAuthorizationResponse:authorizationResponse];
[self setAuthState:authState];
[self logMessage:#"Authorization response with code: %#",
authorizationResponse.authorizationCode];
// could just call [self tokenExchange:nil] directly, but will let the user initiate it.
OIDTokenRequest *tokenExchangeRequest =
[_authState.lastAuthorizationResponse tokenExchangeRequest];
[self logMessage:#"Performing authorization code exchange with request [%#]",
tokenExchangeRequest];
[OIDAuthorizationService performTokenRequest:tokenExchangeRequest
callback:^(OIDTokenResponse *_Nullable tokenResponse,
NSError *_Nullable error) {
if (!tokenResponse) {
[self logMessage:#"Token exchange error: %#", [error localizedDescription]];
} else {
[self logMessage:#"Received token response with accessToken: %#", tokenResponse.accessToken];
}
[_authState updateWithTokenResponse:tokenResponse error:error];
GTMAppAuthFetcherAuthorization *gtmAuthorization =
[[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
// Sets the authorizer on the GTLRYouTubeService object so API calls will be authenticated.
self.youTubeService.authorizer = gtmAuthorization;
// Serializes authorization to keychain in GTMAppAuth format.
[GTMAppAuthFetcherAuthorization saveAuthorization:gtmAuthorization
toKeychainForName:kGTMAppAuthKeychainItemName];
[self uploadVideoFile];
}];
} else {
[self logMessage:#"Authorization error: %#", [error localizedDescription]];
}
}];
}
This method cause this flow:
app send user to google login page in safari -> user log with his credentials -> after login, user is redirect back to my app -> the block success call the method UploadVideo.
This part of the flow seems to work correctly, i obtain a valid token as the log says. The second part is the video upload that consist in two main methods:
- (void)uploadVideoFile {
// Collect the metadata for the upload from the user interface.
// Status.
GTLRYouTube_VideoStatus *status = [GTLRYouTube_VideoStatus object];
status.privacyStatus = #"public";
// Snippet.
GTLRYouTube_VideoSnippet *snippet = [GTLRYouTube_VideoSnippet object];
snippet.title = #"title";
NSString *desc = #"description";
if (desc.length > 0) {
snippet.descriptionProperty = desc;
}
NSString *tagsStr = #"tags";
if (tagsStr.length > 0) {
snippet.tags = [tagsStr componentsSeparatedByString:#","];
}
GTLRYouTube_Video *video = [GTLRYouTube_Video object];
video.status = status;
video.snippet = snippet;
[self uploadVideoWithVideoObject:video
resumeUploadLocationURL:nil];
}
- (void)uploadVideoWithVideoObject:(GTLRYouTube_Video *)video
resumeUploadLocationURL:(NSURL *)locationURL {
NSURL *fileToUploadURL = [NSURL fileURLWithPath:self.VideoUrlCri.path];
NSError *fileError;
NSLog(#"step");
if (![fileToUploadURL checkPromisedItemIsReachableAndReturnError:&fileError]) {
NSLog(#"exit");
return;
}
// Get a file handle for the upload data.
NSString *filename = [fileToUploadURL lastPathComponent];
NSString *mimeType = [self MIMETypeForFilename:filename
defaultMIMEType:#"video/mp4"];
GTLRUploadParameters *uploadParameters =
[GTLRUploadParameters uploadParametersWithFileURL:fileToUploadURL
MIMEType:mimeType];
uploadParameters.uploadLocationURL = locationURL;
GTLRYouTubeQuery_VideosInsert *query =
[GTLRYouTubeQuery_VideosInsert queryWithObject:video
part:#"snippet,status"
uploadParameters:uploadParameters];
query.executionParameters.uploadProgressBlock = ^(GTLRServiceTicket *ticket,
unsigned long long numberOfBytesRead,
unsigned long long dataLength) {
NSLog(#"upload progress");
};
GTLRYouTubeService *service = self.youTubeService;
_uploadFileTicket = [service executeQuery:query
completionHandler:^(GTLRServiceTicket *callbackTicket,
GTLRYouTube_Video *uploadedVideo,
NSError *callbackError) {
if (callbackError == nil) {
NSLog(#"uploaded");
} else {
NSLog(#"error %#",callbackError);
}
}];
}
- (NSString *)MIMETypeForFilename:(NSString *)filename
defaultMIMEType:(NSString *)defaultType {
NSString *result = defaultType;
NSString *extension = [filename pathExtension];
CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
(__bridge CFStringRef)extension, NULL);
if (uti) {
CFStringRef cfMIMEType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType);
if (cfMIMEType) {
result = CFBridgingRelease(cfMIMEType);
}
CFRelease(uti);
}
return result;
}
I obtain a 403 error in NSLog(#"error %#",callbackError); and i can't see error details because the are something like :
data=<7b226572 726f7222 3a7b2265 72726f72 73223a5b 7b22646f 6d61696e 223a2267 6c6f6261 6c222c22 72656173 6f6e223a 22696e73 75666669 ... 61676522 3a22496e 73756666 69636965 6e742050 65726d69 7373696f 6e227d7d>}
In google api console i have created a Client Oauth for my application bundle and an API key, i use those values for my connection, it seems they works correctly because i obtain a valid token. Anyway, there someone who can help me or point me in the right direction about this error? Or there someone who knows a working example about a video upload for IOS and not for MAC ? There something weird in my code? I can't find any help in documentation or google
I'm trying to add the functionality of touchIDAuthenticationAllowableReuseDuration to my app. I use Touch ID to authenticate a user into the app and at the same time recover an item from the keychain. Before I tried to add this there was no problem, it always asked for Touch ID or if not available for the device passcode. Now so far I've managed to make it so that it does the same thing, and when opening the app within the specified timeout it doesn't show the Touch ID prompt as it should, if all I was doing was authenticating the user I'd be done, but the problem that I'm having is that I also want to recover an item from the keychain, and when the prompt is bypassed with success, but once I call SecItemCopyMatching(…) I don't get the item back, instead I keep getting errSecAuthFailed.
I've looked online everywhere and the best thing I've found yet is Apple's sample code KeychainTouchID, but again, it doesn't do both authentication and getting an item from the keychain at the same time, I tried to add that to their code and I kept getting the same error as well.
Has anyone tried something like this before? How did you make it work? This is the code I have right now:
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlTouchIDAny, nil);
NSString *localizedReason = NSLocalizedString(#"Authenticate to access app", nil);
LAContext *context = [[LAContext alloc] init];
context.touchIDAuthenticationAllowableReuseDuration = 5;
[context evaluateAccessControl:sacObject operation:LAAccessControlOperationUseItem localizedReason:localizedReason reply:^(BOOL success, NSError *error) {
if (success) {
NSDictionary *query = #{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: PASSCODE_KEY,
(__bridge id)kSecReturnData: #YES,
(__bridge id)kSecUseOperationPrompt: localizedReason,
(__bridge id)kSecUseAuthenticationUI: (__bridge id)kSecUseAuthenticationUIAllow,
(__bridge id)kSecAttrAccessControl: (__bridge_transfer id)sacObject,
(__bridge id)kSecUseAuthenticationContext: context
};
CFTypeRef dataTypeRef = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)(query), &dataTypeRef);
// This works when using Touch ID / passcode, but I get errSecAuthFailed when the prompt isn't shown because of the reuse duration.
if (status == errSecSuccess) {
NSData *resultData = (__bridge_transfer NSData *)dataTypeRef;
NSString *result = [[NSString alloc] initWithData:resultData encoding:NSUTF8StringEncoding];
self.recoveredString = result;
} else {
self.recoveredString = #"";
}
} else {
self.recoveredString = #"";
CFRelease(sacObject);
}
}];
Don't create LAContext object each time. Just hold onto the LAContext object on which evaluateAccessControl has succeeded. That way you don't need to set touchIDAuthenticationAllowableReuseDuration. If you call evaluateAccessControl on a LAContext object on which evaluateAccessControl has already succeeded then reply callback is called immediately with success without user being asked to auth again
And when you want user to auth again, just invalidate the LAContext object.
I had created the new project which fetches the track of songs, so I tried to pass an array of SPTTracks to the player, please find it below.
self.player = [[SPTAudioStreamingController alloc] initWithClientId:auth.clientID];
self.player.diskCache = [[SPTDiskCache alloc] initWithCapacity:1024 * 1024 * 64];
NSString *trackURI = #"spotify:track:1zHlj4dQ8ZAtrayhuDDmkY";
[SPTTrack trackWithURI:[NSURL URLWithString:trackURI] accessToken:auth.session.accessToken market:#"ES" callback:^(NSError *error, id object) {
if (!error) {
SPTTrack *trackInfo = object;
NSArray *tracks = #[trackInfo];
[self.player playURIs:tracks fromIndex:0 callback:^(NSError *error) {
if (!error) {
} else {
NSLog(#"*** Failed to play track : %#", error);
}
}];
} else {
NSLog(#"Error %#", error);
}
}];
But I get crashes, whenever I run it. Please find error below while it is getting crash :
Simple Track Playback[254:24669] -[__NSCFConstantString absoluteString]: unrecognized selector sent to instance 0x1000c0508
I had also looked it on spotify api spotify_ios_sdk but I had found that one developer had already posted the same issue link.
If anyone has solved these type of issue then please provide your guidance.
Thanks in advanced.
Unfortunately, this method is not "equivalent", because the SPTTrack object returned inside the callback of [SPTTrack trackWithURI.... has many less informations.
I've tried some workaround, and I found that for me the solution is
1) Create a request for an SPTTrack object.
2) Pass that request to SPRequest performRequest callback.
3) Wait for the response, and eventually create a track from data (please find the below complete code).
self.player = [[SPTAudioStreamingController alloc] initWithClientId:auth.clientID];
self.player.diskCache = [[SPTDiskCache alloc] initWithCapacity:1024 * 1024 * 64];
NSString *market = [[NSLocale currentLocale] objectForKey:NSLocaleCountryCode];
NSURLRequest *request = [SPTTrack createRequestForTrack:[NSURL URLWithString:#"spotify:track:1zHlj4dQ8ZAtrayhuDDmkY"]
withAccessToken:auth.session.accessToken
market:market
error:nil];
[[SPTRequest sharedHandler] performRequest:request
callback:^(NSError *error, NSURLResponse *response, NSData *data) {
if (!error) {
NSError *parsingError = nil;
SPTTrack *track = [SPTTrack trackFromData:data
withResponse:response
error:&parsingError];
self.arrURIs = #[track.playableUri];
[self.player playURIs:self.arrURIs fromIndex:0 callback:nil];
}
}];
Thanks "Kry256" for detail explaination.
Trying to Authenticate with Azure Active Directory and fetch mail, calendar data, accessToken is returned successfully:
authority = #"https://login.windows.net/common/oauth2/authorize";
redirectUriString = #"http://xxxxxx.xxxxxxx.com/oauth";
resourceId = #"https://outlook.office365.com";
clientId = #"xxxxxxx-xxxxx-xxx";
-(void) getToken : (BOOL) clearCache completionHandler:(void (^) (NSString*))completionBlock;
{
ADAuthenticationError *error;
authContext = [ADAuthenticationContext authenticationContextWithAuthority:authority
error:&error];
[authContext setValidateAuthority:YES];
NSURL *redirectUri = [NSURL URLWithString:redirectUriString];
if(clearCache){
[authContext.tokenCacheStore removeAllWithError:&error];
if (error) {
NSLog(#"Error: %#", error);
}
}
[authContext acquireTokenWithResource:resourceId
clientId:clientId
redirectUri:redirectUri
completionBlock:^(ADAuthenticationResult *result) {
if (AD_SUCCEEDED != result.status){
// display error on the screen
[self showError:result.error.errorDetails];
}
else{
completionBlock(result.accessToken);
}
}];
}
-(NSArray*)getEventsList
{
__block NSMutableArray * todoList;
[self getToken:YES completionHandler:^(NSString* accessToken){
NSURL *todoRestApiURL = [[NSURL alloc]initWithString:#"https://outlook.office365.com/api/v1.0/me/folders/inbox/messages?$top=2"];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:todoRestApiURL];
NSString *authHeader = [NSString stringWithFormat:#"Bearer %#", #""];
[request addValue:authHeader forHTTPHeaderField:#"Authorization"];
[request addValue:#"application/json; odata.metadata=none" forHTTPHeaderField:#"accept"];
[request addValue:#"fbbadfe-9211-1234-9654-fe435986a1d6" forHTTPHeaderField:#"client-request-id"];
[request addValue:#"Presence-Propelics/1.0" forHTTPHeaderField:#"User-Agent"];
//[request addValue:#"true" forHTTPHeaderField:#"return-client-request-id"];
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (error == nil){
NSArray *scenarios = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
todoList = [[NSMutableArray alloc]initWithArray:scenarios];
//each object is a key value pair
NSDictionary *keyVauePairs;
for(int i =0; i < todoList.count; i++)
{
keyVauePairs = [todoList objectAtIndex:i];
NSLog(#"%#", keyVauePairs);
}
}
NSLog(#"Finished");
//[delegate updateTodoList:TodoList];
}];
}];
return nil; }
Error is returned in response object:
{
error = {
code = ErrorAccessDenied;
message = "Access is denied. Check credentials and try again.";
};
}
I know its late to answer this but it might be helpful for someone like me who was struggling to get the same thing done
I have done this using the office 365 SDK for iOS which has all the inbuilt classes to do your work.
If you download their sample code it will provide you all the details you require to do certain operations (mail, calendar, contacts, one drive).
Before using the SDK make sure you login to Azure AD and register your application and add permissions so that you do not get 403 error code or any access denied message.
I am using the below code to fetch my events details from outlook calendar
[self getClientEvents:^(MSOutlookClient *client) {
NSURLSessionDataTask *task = [[[client getMe] getEvents] read:^(NSArray<MSOutlookEvent> *events, MSODataException *error) {
if (error==nil) {
if (events.count!=0) {
dispatch_async(dispatch_get_main_queue(), ^{
for(MSOutlookEvent *calendarEvent in events){
NSLog(#"name = %#",calendarEvent.Subject);
}
});
}else{
NSLog(#"No events found for today");
}
}
}];
[task resume];
}];
getClientEvents is a method which gives call to the Office 365 SDK and fetches the event details of the user but it first fetches the token for the resource and then makes the call with the acquired token
-(void)getClientEvents : (void (^) (MSOutlookClient* ))callback{
[self getTokenWith : #"https://outlook.office365.com" :true completionHandler:^(NSString *token) {
MSODataDefaultDependencyResolver* resolver = [MSODataDefaultDependencyResolver alloc];
MSODataOAuthCredentials* credentials = [MSODataOAuthCredentials alloc];
[credentials addToken:token];
MSODataCredentialsImpl* credentialsImpl = [MSODataCredentialsImpl alloc];
[credentialsImpl setCredentials:credentials];
[resolver setCredentialsFactory:credentialsImpl];
[[resolver getLogger] log:#"Going to call client API" :(MSODataLogLevel *)INFO];
callback([[MSOutlookClient alloc] initWithUrl:#"https://outlook.office365.com/api/v1.0" dependencyResolver:resolver]);
}];
}
getTokenWith method fetches the token for a resource first and then with the acquired token makes the necessary calls to fetch the events, but before fetching the token it checks in the cache to see if there are any tokens available for the same resource.
// fetch tokens for resources
- (void) getTokenWith :(NSString *)resourceId : (BOOL) clearCache completionHandler:(void (^) (NSString *))completionBlock;
{
// first check if the token for the resource is present or not
if([self getCacheToken : resourceId completionHandler:completionBlock]) return;
ADAuthenticationError *error;
authContext = [ADAuthenticationContext authenticationContextWithAuthority:[[NSUserDefaults standardUserDefaults] objectForKey:#"authority"] error:&error];
NSURL *redirectUri = [NSURL URLWithString:#"YOUR_REDIRECT_URI"];
[authContext acquireTokenWithResource:resourceId
clientId:[[NSUserDefaults standardUserDefaults] objectForKey:#"clientID"]
redirectUri:redirectUri
completionBlock:^(ADAuthenticationResult *result) {
if (AD_SUCCEEDED != result.status){
[self showError:result.error.errorDetails];
}
else{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:result.tokenCacheStoreItem.userInformation.userId forKey:#"LogInUser"];
[userDefaults synchronize];
completionBlock(result.accessToken);
}
}];
}
getCacheToken method: Checks if there are any reusable token for any resources.
-(BOOL)getCacheToken : (NSString *)resourceId completionHandler:(void (^) (NSString *))completionBlock {
ADAuthenticationError * error;
id<ADTokenCacheStoring> cache = [ADAuthenticationSettings sharedInstance].defaultTokenCacheStore;
NSArray *array = [cache allItemsWithError:&error];
if([array count] == 0) return false;
ADTokenCacheStoreItem *cacheItem;
for (ADTokenCacheStoreItem *item in array) {
if([item.resource isEqualToString:resourceId]){
cacheItem = item;
break;
}
}
ADUserInformation *user = cacheItem.userInformation;
if(user == nil) return false;
if([cacheItem isExpired]){
return [self refreshToken:resourceId completionHandler:completionBlock];
}
else
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:user.userId forKey:#"LogInUser"];
[userDefaults synchronize];
completionBlock(cacheItem.accessToken);
return true;
}
}
Using this code and Office 365 SDK in place you can get the outlook events for a particular user, before that make sure you have full permissions in the Azure AD else you may get 0 events as response.
Please note all the methods are from the SDK example apart from the first method to view how to fetch the events i would recommend to download the exchange example from the github.
You can also use MSGraph SDK to fetch calendars and events:
Check this link: Configuration process is same, only fetching events is different(see given code for fetching events):
How to Fetch/Create calender by O365-iOS-Connect?
Note: Above link is used to fetch calendars from outlook the process is same for this but you should use this code after authentication and completed get events action look like this:
- (IBAction)getCalendarsEvents:(id)sender {
[NXOAuth2AuthenticationProvider setClientId:clientId
scopes:#[#"https://graph.microsoft.com/Files.ReadWrite",
#"https://graph.microsoft.com/Calendars.ReadWrite"]];
[[NXOAuth2AuthenticationProvider sharedAuthProvider] loginWithViewController:nil completion:^(NSError *error) {
if (!error) {
[MSGraphClient setAuthenticationProvider:[NXOAuth2AuthenticationProvider sharedAuthProvider]];
self.client = [MSGraphClient client];
// Authentication done
[[[[_client me] events] request] getWithCompletion:^(MSCollection *response, MSGraphUserEventsCollectionRequest *nextRequest, NSError *error){
NSArray *arr = response.value;
MSGraphEvent *event = arr.firstObject;
// Here you will getting outlook events
}];
}
}];
}
How can I add an option to enable and disable iCloud sync for Core Data (iOS7)?
Here are my thoughts / attempts:
To disable iCloud sync:
NSDictionary *options = #{NSPersistentStoreUbiquitousContentNameKey: #"MYStore"};
[NSPersistentStoreCoordinator removeUbiquitousContentAndPersistentStoreAtURL:storeUrl options:options error:&error];
However, I think this may delete all the data from iCloud? I don't want that.
To enable iCloud sync:
NSDictionary *options = #{NSPersistentStoreUbiquitousContentNameKey: #"MYStore"};
[__persistentStoreCoordinator lock];
result = [__persistentStoreCoordinator migratePersistentStore:result toURL:storeUrl options:options withType:NSSQLiteStoreType error:&error];
[__persistentStoreCoordinator unlock];
In this code, I'm attempting to add options with the NSPersistentStoreUbiquitousContentNameKey key so that it starts syncing with iCloud. However, I don't want to move the store to a new location. Is this code proper?
How can I enable / disable iCloud while the app is running?
Below are the methods I use to enable or disable iCloud sync (OS X), essentially the store gets rebuilt each time, which is not the same as enabling and disabling iCloud sync while the app is running. So you wouldn't want to use this for "Pausing" synchronisation.
As I understand it you want something like the following:
If you turned off iCloud sync while the app is running and then perform some updates, you don't want those changes to be synchronised, but later when you turn iCloud sync back on you want subsequent changes to be synchronised.
I could be wrong but I think you might be out of luck using the standard Core Data/iCloud integration for this.
Depending on your requirements perhaps you could just observe the import notifications and discard any changes - but I can't see how you are not going to end up with all kinds of data inconsistencies that might cause subsequent imports to fail.
// Removes the current Document from iCloud and deletes the local copy.
// There does not appear to be any way of only removing ubiquitous content
// and turning off iCloud sync.
// So before we remove it we make a local copy by appending "_Backup" to the filename
// (we should check this filename does not already exist and add a counter or something)
//
- (void)removeMeFromICloud {
//LOG(#"removeMeFromICloud called");
NSError *error;
NSURL *currentURL = self.fileURL;
NSString *fileType = self.fileType;
NSString *extension = [currentURL pathExtension];
NSString *path = [[currentURL URLByDeletingPathExtension] path];
NSString *backupFilename = [NSString stringWithFormat:#"%#_Backup", path];
NSURL *backupFileURL = [[[NSURL alloc] initFileURLWithPath:backupFilename] URLByAppendingPathExtension:extension];
// We only have one store
NSPersistentStore *currentStore = [self.managedObjectContext.persistentStoreCoordinator.persistentStores objectAtIndex:0];
NSDictionary *currentOptions = currentStore.options;
if ([self buildNewStoreAtURL:backupFileURL type:fileType error:&error]) {
//FLOG(#" reset the moc...");
[self.managedObjectContext reset];
[self.managedObjectContext save:&error];
//Set the document title
//FLOG(#" current displayName is %#", self.displayName);
self.fileURL = backupFileURL;
//set the file extension hidden attribute to YES
NSDictionary* fileAttrs = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
forKey:NSFileExtensionHidden];
if(![[NSFileManager defaultManager] setAttributes:fileAttrs
ofItemAtPath:[backupFileURL path]
error:&error])
FLOG(#" unable to set file attributes to hide extension");
// Now remove the old one
bool success = [NSPersistentStoreCoordinator removeUbiquitousContentAndPersistentStoreAtURL:currentURL options:currentOptions error:&error];
if (success) {
FLOG(#" removed document from iCloud");
FLOG(#" using backup copy");
}
else {
FLOG(#" error removing document from iCloud");
FLOG(#" error is %#, %#", error, error.userInfo);
}
} else {
FLOG(#" error creating backup before removing from iCloud");
FLOG(#" error is %#, %#", error, error.userInfo);
}
}
// in order to migrate to the cloud we have to build the database from scratch,
// we can't just open it with iCloud parameters because we have to ensure that
// log files get generated in iCloud that will allow the store to be rebuilt from
// scratch by peer devices.
//
- (void)migrateMeToICloud {
//LOG(#"migrateMeToICloud called");
NSPersistentStoreCoordinator *psc = self.managedObjectContext.persistentStoreCoordinator;
NSArray *stores = psc.persistentStores;
NSError *error;
// Now get current URL and add_iCloud to the document name, use this for the new store name
NSURL *currentURL = self.fileURL;
NSString *extension = [self.fileURL pathExtension];
NSString *currentFilePathName = [[currentURL URLByDeletingPathExtension] path];
NSString *uuidString = [[NSUUID UUID] UUIDString];
NSString *newFilePathName = [NSString stringWithFormat:#"%#_UUID_%#",currentFilePathName, uuidString];
// We must make it NSSQLite so get the right extension...
NSURL *newURL = [[[NSURL alloc] initFileURLWithPath:newFilePathName] URLByAppendingPathExtension:extension];
NSDictionary *options = [self storeOptionsForICloud:self.fileURL];
NSString *ubiquityName = [options valueForKey:NSPersistentStoreUbiquitousContentNameKey];
if ([stores count]) {
NSPersistentStore *store = [stores objectAtIndex:0];
if (store) {
//FLOG(#" starting migration...");
NSPersistentStore *newStore = [psc migratePersistentStore:store toURL:newURL options:options withType:NSSQLiteStoreType error:&error];
//FLOG(#" psc is %#", psc);
//FLOG(#" done migrating...");
if (newStore != nil) {
//FLOG(#" new store seems OK...");
// Set custom metadata so we know if it is synced in iCloud next time we open it
[self setiCloudMetaDataForStore:currentURL ofType:NSSQLiteStoreType iCloud:YES ubiquityName:ubiquityName];
}
else {
FLOG(#" error migrating store");
FLOG(#" error is %#, %#", error, error.userInfo);
}
} else {
FLOG(#" store is nil, nothing to migrate!");
}
}
}
// File is NEVER iCloud enabled when we do this.
// Is we do Save As we pnly build a local store wihout iCloud sync.
// User can select iCloud sync once Save As is done
//
- (bool)buildNewStoreAtURL:(NSURL*)newURL type:(NSString *)typeName error:(NSError **)error {
//FLOG(#"buildNewStoreAtURL:type: called");
NSError *myError;
// We only have one store
NSPersistentStore *currentStore = [self.managedObjectContext.persistentStoreCoordinator.persistentStores objectAtIndex:0];
NSDictionary *currentOptions = currentStore.options;
// We usually would need to create a new UUID for the new document if it is in iCloud.
// But since we create a local copy only we don't need this.
NSMutableDictionary *newOptions = [[NSMutableDictionary alloc] initWithDictionary:currentOptions];
[newOptions setObject:#"DELETE" forKey:#"JOURNAL"];
// Remove any iCloud options (this one includes the unique iCloud UUID
[newOptions removeObjectForKey:NSPersistentStoreUbiquitousContentNameKey];
// Remove Core Data ubiquity metadata
[newOptions setObject:[NSNumber numberWithBool:YES] forKey:NSPersistentStoreRemoveUbiquitousMetadataOption];
NSPersistentStoreCoordinator *psc = self.managedObjectContext.persistentStoreCoordinator;
// Now migrate the store to the new location
NSPersistentStore *newStore = [psc migratePersistentStore:currentStore toURL:newURL options:newOptions withType:typeName error:&myError];
if (newStore) {
// Now set up our custom metadata so we can determine if it has been synced in iCloud next time we open it
NSDictionary *dict = [self getiCloudMetaDataForStore:[psc metadataForPersistentStore:newStore] iCloud:NO ubiquityName:nil];
[psc setMetadata:dict forPersistentStore:newStore];
return YES;
}
else {
FLOG(#" problem creating new document");
FLOG(#" - error is %#, %#", myError, myError.userInfo);
*error = myError;
return NO;
}
}