On iOS (14/15) I'm trying to pass security scoped bookmarks to user picked files on iCloud Drive between devices but whatever I try: I cannot get urls to be restored on another device running the same app.
The app is a UIDocument based app, the code below is in a UIViewController that display the document. The document is created like so:
Document* document = [[Document alloc] initWithFileURL:documentURL];
and then passed on to the ViewController
The url that's going to be bookmarked is picked using a plain UIDocumentPickerViewController
This is how I create a security scoped bookmark:
// Toggle this to create either a document scoped url or an app scoped url
static BOOL DOC_SCOPE = NO;
- (void) documentPicker:(UIDocumentPickerViewController*)controller didPickDocumentsAtURLs:(NSArray <NSURL *>*)urls {
if (urls.count > 0) {
NSURL* url = urls.firstObject;
NSURL* docURL = self.document.fileURL;
BOOL closeSource = [docURL startAccessingSecurityScopedResource];
BOOL doClose = [url startAccessingSecurityScopedResource];
NSData* bookmark = [url bookmarkDataWithOptions:0 includingResourceValuesForKeys:nil relativeToURL:DOC_SCOPE?docURL:nil error:nil];
if (doClose)
[url stopAccessingSecurityScopedResource];
if (closeSource)
[docURL stopAccessingSecurityScopedResource];
NSString* encoded = [NSString stringWithFormat:#"\n[%#]\n", [bookmark base64EncodedStringWithOptions:0]];
At this point I insert the encoded bookmark data in the document data and save the document.
When opening the linked document the bookmark and url are restored like so:
openLink:(NSString*)encoded {
NSData* bookmark = [[NSData alloc] initWithBase64EncodedString:encoded options:0];
NSURL* docURL = self.document.fileURL;
BOOL closeSource = [docURL startAccessingSecurityScopedResource];
NSError* error = nil;
NSURL* url = [NSURL URLByResolvingBookmarkData:bookmark options:0 relativeToURL:DOC_SCOPE?docURL:nil bookmarkDataIsStale:nil error:&error];
if (error != nil)
NSLog(#"%#", error.localizedFailureReason);
if (url != nil) {
BOOL doClose = [url startAccessingSecurityScopedResource];
// here use the url to access linked file
if (doClose)
[url stopAccessingSecurityScopedResource];
}
if (closeSource)
[docURL stopAccessingSecurityScopedResource];
}
To the project's entitlements I have added
com.apple.security.files.bookmarks.app-scope = 1
com.apple.security.files.bookmarks.document-scope = 1
When on the same device I can the restore the bookmark data and get access to the restored URL OK, but when opening the same file on another device, [NSURL URLByResolvingBookmarkData...] always sets an error:
Error Domain=NSCocoaErrorDomain Code=257 "The file couldn’t be opened because you don’t have permission to view it."
This is both the case for app scope book marks and document scope book marks.
Any idea what's missing / how to get this working?
I want the user to select a file from files app and I've to read the contents of that file, modify it and write it in the same location.
I was trying to open the file using the following code:
UIDocumentPickerViewController *documentProvider;
documentProvider = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:[NSArray arrayWithObjects:#"public.comma-separated-values-text", nil] inMode: UIDocumentPickerModeOpen];
documentProvider.delegate = self;
documentProvider.modalPresentationStyle = UIModalPresentationOverFullScreen;
Delegate function:
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url
{
NSLog(#"%#", url.absoluteString);
}
But when I try to view or edit the file, it's showing error that I don't have view/write permission.
The URL that I received is file:///private/var/mobile/Library/Mobile%20Documents/com~apple~CloudDocs/filename.csv
Is there any way to get permission for the files in iCloud? I found few third party apps which can modify the contents of the file.
You need to access the file via a security scoped bookmark. Like such:
- (void) documentPicker: (UIDocumentPickerViewController *) controller
didPickDocumentAtURL: (NSURL *) url
{
if (controller.documentPickerMode == UIDocumentPickerModeOpen)
{
BOOL isAccess = [url startAccessingSecurityScopedResource];
if(!isAccess)
{
return;
}
NSError * fileOpenError = nil;
NSString * fileContents = [NSString stringWithContentsOfURL: url
encoding: NSUTF8StringEncoding
error: &fileOpenError];
// FileContents should now be set properly.
[url stopAccessingSecurityScopedResource];
}
}
I am trying to get path of legal.txt in my project. However, whatever I do. I am getting a null path back. it is in the Policies directory. How do I get it's path as a string. I need to send that path to a webview controller. Can someone help.
-(void)linkPressedOnTextView:(PPLinksTextView *)tv url:(NSURL *)url{
if ([url.host isEqualToString:#"sign_up"]) {
GetStartedController *getStartedVC = [[GetStartedController alloc] init];
[self.navigationController pushViewController:getStartedVC animated:YES];
}
else if([url.host isEqualToString:#"consentMedicalSevices"]){
PPNavigationViewController *vc = [[PPNavigationViewController alloc]init];
[vc openPolicies:#"Policies/privacy.txt"];
Will this work. I need a way to direct user to a webview once he clicks on the link and privacy text opens up.
From apple doc:
pathForResource:ofType:inDirectory:
Returns the full pathname for the resource file identified by the specified name and extension and residing in a given bundle directory.
NSString *filePath = [NSBundle pathForResource:#"file" ofType:#"txt" inDirectory:[[NSBundle mainBundle] bundlePath]];
NSLog(#"File Path: %#", filePath);
Output:
File Path: /Users/Ashok/Library/Developer/CoreSimulator/Devices/F629F99F-C745-46EB-8A11-01BC9FF1E592/data/Containers/Bundle/Application/EB4EA25F-65E6-4CA8-B8C2-C7C0C64C6C0F/Sample.app/file.txt
I use this code to save some PDF data to a file, send it to another app using the "Open In" menu, then delete the file when that's done:
- (void)openIn:(NSData *)fileData {
// save the PDF data to a temporary file
NSString *fileName = [NSString stringWithFormat:#"%#.pdf", self.name];
NSString *filePath = [NSString stringWithFormat:#"%#/Documents/%#", NSHomeDirectory(), fileName];
BOOL result = [fileData writeToFile:filePath atomically:TRUE];
if (result) {
NSURL *URL = [NSURL fileURLWithPath:filePath];
UIDocumentInteractionController *controller = [[UIDocumentInteractionController interactionControllerWithURL:URL] retain];
controller.delegate = self;
[controller presentOpenInMenuFromBarButtonItem:self.openInButton animated:TRUE];
}
}
- (void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller {
// when the document interaction controller finishes, delete the temporary file
NSString *fileName = [NSString stringWithFormat:#"%#.pdf", self.name];
NSString *filePath = [NSString stringWithFormat:#"%#/Documents/%#", NSHomeDirectory(), fileName];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
}
This has worked fine until iOS 8. Now, the file is created and I can verify that it contains the correct content, the Open In menu appears, I can select an app, and the delegate method runs and cleans up the file. But instead of iOS switching to the selected app and copying the file into it as it did before, the Open In menu simply closes when I select an app, and the file is not copied.
This works if I give the UIDocumentInteractionController an existing file. It also works if I use the provided fileData but change the destination filename to the filename of an existing file. This suggests a permissions problem -- as if new files are created in iOS 8 with default permissions that UIDocumentInteractionController can't read.
Does anyone know what's happening and how I can work around it?
It looks like the order of operations has changed slightly in iOS 8. DidDismissOpenInMenu used to run after the file was finished sending, but now it runs after the file begins sending. This means my cleanup code was sometimes running before the file was finished sending, leaving no file to send. I figured this out after noticing that smaller files were being sent okay; apparently the processing for smaller files was finishing before my cleanup code got them, but the processing for larger files was not.
To ensure the correct timing, but also clean up files that are created when the user opens the DocumentInteractionController and then dismisses the controller without doing anything, I changed my methods like this:
- (void)openIn:(NSData *)fileData {
// save the PDF data to a temporary file
NSString *fileName = [NSString stringWithFormat:#"%#.pdf", self.name];
NSString *filePath = [NSString stringWithFormat:#"%#/Documents/%#", NSHomeDirectory(), fileName];
BOOL result = [fileData writeToFile:filePath atomically:TRUE];
if (result) {
self.sendingFile = FALSE;
NSURL *URL = [NSURL fileURLWithPath:filePath];
UIDocumentInteractionController *controller = [[UIDocumentInteractionController interactionControllerWithURL:URL] retain];
controller.delegate = self;
[controller presentOpenInMenuFromBarButtonItem:self.openInButton animated:TRUE];
}
}
- (void)documentInteractionController:(UIDocumentInteractionController *)controller willBeginSendingToApplication:(NSString *)application {
// the user chose to send the file, so we shouldn't clean it up until that's done
self.sendingFile = TRUE;
}
- (void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller {
if (!self.sendingFile) {
// the user didn't choose to send the file, so we can clean it up now
[self openInCleanup];
}
}
- (void)documentInteractionController:(UIDocumentInteractionController *)controller didEndSendingToApplication:(NSString *)application {
// the user chose to send the file, and the sending is finished, so we can clean it up now
[self openInCleanup];
self.sendingFile = FALSE;
}
- (void)openInCleanup {
// delete the temporary file
NSString *fileName = [NSString stringWithFormat:#"%#.pdf", self.name];
NSString *filePath = [NSString stringWithFormat:#"%#/Documents/%#", NSHomeDirectory(), fileName];
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
}
Update for iOS 11
Before iOS 11, it seems that the operating system kept a copy of the file available until the receiving app was finished reading it, even though my cleanup function ran as soon as the file was sent out from my app. In iOS 11, this changed and the receiving app fails to read the file because my app deletes it before that's done. So now instead of saving the temporary file to Documents and using the openInCleanup method to delete it immediately, I'm saving the temporary file to tmp and emptying the tmp folder next time the app launches. This approach should also work with older iOS versions. Just remove openInCleanup, change Documents to tmp in the paths, and add this to applicationDidFinishLaunching:
// clear the tmp directory, which will contain any files saved for Open In
NSString *tmpDirectory = [NSString stringWithFormat:#"%#/tmp", NSHomeDirectory()];
NSArray *tmpFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tmpDirectory error:NULL];
for (NSString *tmpFile in tmpFiles) {
[[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:#"%#/%#", tmpDirectory, tmpFile] error:NULL];
}
After reading this post, I already hoped to have found the solution to a similar problem:
For me, as of iOS 8, sharing was only working with Mail.app. It was failing for Dropbox, etc.
Turns out it was something else:
On my interactionController I was setting an annotation like this:
interactionController.annotation = #"Some text"
For unknown reasons, this prevented Dropbox to open at all. There were no error messages or anything. Removing this line solved the issue.
I am sharing an audio recording via the UIActivityViewController. When the audio file shares via email or iMessage, it shows the underlying name of the audio file without any apparent way of changing it.
NSArray *activityItems = [NSArray arrayWithObjects:self.audioPlayer.url, nil];
UIActivityViewController *avc = [[UIActivityViewController alloc]
initWithActivityItems:activityItems
applicationActivities:nil];
[self presentViewController:avc
animated:YES completion:nil];
If I don't use the UIActivityViewController and just use MFMessageComposeViewController directly, than I can use
[composer addAttachmentURL:self.audioPlayer.url withAlternateFilename:#"Piano Song.m4a"];
Is it possible to have an alternate file name with the UIActivityViewController?
You can create a hard link to the file (so that you don't have to copy the actual file) with any name you want in the temporary directory and pass it to the UIActivityViewController instead of the file.
- (NSURL *)createLinkToFileAtURL:(NSURL *)fileURL withName:(NSString *)fileName {
NSFileManager *fileManager = [NSFileManager defaultManager];
// create a path in the temp directory with the fileName
NSURL *tempDirectoryURL = [[NSFileManager defaultManager] temporaryDirectory];
NSURL *linkURL = [tempDirectoryURL URLByAppendingPathComponent:fileName];
// if the link already exists, delete it
if ([fileManager fileExistsAtPath:linkURL.path]) {
NSError *error;
[fileManager removeItemAtURL:linkURL error:&error];
if (error) {
// handle the error
}
}
// create a link to the file
NSError *error;
BOOL flag = [fileManager linkItemAtURL:fileURL toURL:linkURL error:&error];
if (!flag || error) {
// handle the error
}
return linkURL;
}
Use it like this:
NSURL *fileURL = ...;
NSString *desiredName = ...;
NSURL *linkURL = [self createLinkToFileAtURL:fileURL withName:desiredName];
UIActivityViewController *viewController = [[UIActivityViewController alloc] initWithActivityItems:#[linkURL] applicationActivities:nil];
[self presentViewController:viewController animated:YES completion:nil];
Hope this helps! Good luck!
Very nice, timaktimak. Thank you.
Here is the same in Swift:
private func createLinkToFile(atURL fileURL: URL, withName fileName: String) -> URL? {
let fileManager = FileManager.default // the default file maneger
let tempDirectoryURL = fileManager.temporaryDirectory // get the temp directory
let linkURL = tempDirectoryURL.appendingPathComponent(fileName) // and append the new file name
do { // try the operations
if fileManager.fileExists(atPath: linkURL.path) { // there is already a hard link with that name
try fileManager.removeItem(at: linkURL) // get rid of it
}
try fileManager.linkItem(at: fileURL, to: linkURL) // create the hard link
return linkURL // and return it
} catch let error as NSError { // something wrong
print("\(error)") // debug print out
return nil // and signal to caller
}
}
No, not possible. Could you not just rename the file before sharing it?