Can I send UIDocuments by email, as an attachment? - ios

I'm working with Xcode.
In my app I save some UIdocuments at that location
[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
I'm searching for a way to share documents, my first option is by email.
Can I send those documents by email, as an attachment? Can I open then with another device with the same app?

You could do like the following.
Create a MFMailComposeViewController and use - (void)addAttachmentData:(NSData*)attachment mimeType:(NSString*)mimeType fileName:(NSString*)filename method to add your attachment.
For example.
MFMailComposeViewController *mailVC = [[MFMailComposeViewController alloc] init];
[mailVC setSubject:#"Shared documents"];
[mailVC setToRecipients:#[#"sample#example.com"]];
[mailVC setMessageBody:#"Here the docs I want to share" isHTML:NO];
[mailComposer addAttachmentData:pdfData mimeType:#"application/pdf" fileName:#"file.pdf"];
[mailVC setMailComposeDelegate:self];
[self presentViewController:mailVC animated:YES completion:nil];
where pdfData is of type NSData. So, you need to transform your document into a NSData.
From Apple doc.
addAttachmentData:mimeType:fileName:
This method attaches the specified data after the message body but
before the user’s signature. You may attach multiple files (using
different file names) but must do so prior to displaying the mail
composition interface. Do not call this method after presenting the
interface to the user.
About the second part of your question. Could you explain what type of document do you need to display?
In the meantime, take a look at Adding "Open In..." option to iOS app.

To send any attachment you need to get the contents into an NSData object. If the document is on disk then this is simple. You just need the path or file URL to the document. Then you can create the NSData object using the path or URL.
If the receiver of the email has the same app and the app is setup to appear in the "Open In" menu for documents of this type, then the user can open the app from the attachment. Your app then just needs to know what to do when it is asked to open a file of this type. There are plenty of existing documentation and questions here on SO that describe how to register an app to open certain file types.

Related

How to share files for open-in-place on iOS

We have an iOS application that manages documents via Core Data. The actual files reside in the app's shared container so that the app's file provider extension can also access them for Files.app support. We want to give the user the option to open these files in third-party apps so that they can edit them in-place instead of sending a copy to the other app.
We provide a UIActivityViewController for sharing files with other apps. We also provide a UIActivity that shows a UIDocumentInteractionController which seems to work better in some cases. We give the UIActivityViewController the document's file URL, the raw text content, and printable data.
This works but all third-party editors are shown as Copy to … instead of Open in …
We've also set the UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace properties to YES in the app's info.plist but they seem to be only relevant for open-in-place when sharing files residing in the app's Documents folder.
Now we've stumbled upon the NSItemProviderFileOptionOpenInPlace option for NSItemProvider. As we're already supporting a file provider extension and from Apple's documentation this seemed like a great place to accomplish just what we want.
Adding a "pure" NSItemProvider works, in a way, but shows fewer options than when also sharing the file URL and text in addition (which is expected). However, when we use -[NSItemProvider registerFileRepresentationForTypeIdentifier:fileOptions:visibility:loadHandler:] with the said option (or just zero, same result) and return the file URL in the loadHandler's completionHandler() nothing is shared anymore. E.g., Mail no longer attaches the file, Messages doesn't show the document for sending.
These are the relevant bits of the code:
NSMutableArray *items = [NSMutableArray array];
NSMutableArray <UIActivity *> *activities = [NSMutableArray array];
NSURL *fileURL = self.record.metadata.fileURL;
NSString *fileUTI = self.record.metadata.uti;
NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithItem: fileURL typeIdentifier: fileUTI];
[itemProvider registerFileRepresentationForTypeIdentifier:fileUTI fileOptions:NSItemProviderFileOptionOpenInPlace visibility:YES loadHandler:^NSProgress * _Nullable(void (^ _Nonnull completionHandler)(NSURL * _Nullable, BOOL, NSError * _Nullable))
{
if (fileURL)
completionHandler(fileURL, YES, nil);
else
completionHandler(nil, YES, [NSError errorWithDomain:NSCocoaErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]);
return nil;
}];
[items addObject:itemProvider];
self.activityViewController = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:activities];
[UIAppDelegate.splitViewController presentViewController:self.activityViewController animated:YES completion:nil];
The using the Share menu the item provider's load handler is correctly called and the file's actual URL returned.
Is that not how NSItemProviderFileOptionOpenInPlace is intended to be used? Or are we using it simply wrong? Apple's description is extremely sparse and we couldn't find any information elsewhere on the internet except for the official documentation.
I've found out what my problem was: Not deep enough understanding of the relationship between the activity view controller and file providers.
As all my files reside in the shared container and are published also through the file provider extension, what I need to share through the activity view controller is the exact same URL that is shared through the file provider extension. Technically then the app that opens the file accesses it through there file provider mechanism.

Send attachments using sms/mms in iOS SDK

In iOS 7, there is support for adding attachments in sms messages via third party applications.
I want to know:
What kind of files are supported as attachments? e.g. .png, .pdf etc.
Can I send NSData through an sms/mms message? e.g. .dat format
Would the recipient of these messages be able to open these attachments in third party applications using iOS's "Open In" feature?
The MFMessageComposeViewController wants the attachment to have the correct extension for the type of image you're uploading.
I verified by testing with a PNG file, and the following variations of adding the attachment data:
[messageController addAttachmentData:imgData typeIdentifier:#"public.data" filename:#"image"];
[messageController addAttachmentData:imgData typeIdentifier:#"public.data" filename:#"image.abc"];
[messageController addAttachmentData:imgData typeIdentifier:#"public.data" filename:#"image.png"];
Only the last option worked.
I didn't need to change the typeIdentifier, although it probably would make sense to choose a UTI that matches the type of data.
According to Apple MFMessageComposeViewController docs, you can do that by creating a MFMessageComposeViewController object, and add the attachment through the following functions:
func addAttachmentURL(URL, withAlternateFilename: String?)
Attaches a specified file to the message.
func addAttachmentData(Data, typeIdentifier: String, filename: String)
Attaches arbitrary content to the message.
(BTW you should check the canSendAttachments before you try to use those functions)

Airdrop error message: "cannot receive all of these items at the same time"

I use the following code to share an image, some text, and a url using UIActivityViewController. Everything works fine except that when the use selects AirDrop, it gets a "cannot receive all of these items at the same time". If I only share the image, then AirDrop works. I need the text and the url for email, Facebook, twitter sharing methods.
Is there a way to keep the text and the URL and make AirDrop only share the image while Facebook, email, twitter methods of sharing continue to use the text and url together with the image that I'm trying to share?
NSString *text = [NSString stringWithFormat:#"I made this image using %# iOS app. Here is the link to download it:", [CloudHelper appName]];
NSURL *url = [NSURL URLWithString:APP_URL];
UIActivityViewController *activityController =
[[UIActivityViewController alloc]
initWithActivityItems:#[text, url, myImage]
applicationActivities:nil];
[self presentViewController:activityController animated:YES completion:nil];
You'd want to create and share three objects that conform to the UIActivityItemSource, where one returns a string, one an image and one a URL. Then when the delegate callback requesting the item is called you check which activity type was selected (Facebook, Twitter, airdrop, etc) and have some return nil if that item doesn't apply.
So in the case of airdrop, only the item source for the image will return a non-nil value. You can take a look at the airdrop sample code to get some examples of how to implement UIActivityItemSource

How to implement Open In apps for plain text

I would like to provide the ability for users to tap the Action button and up pops the usual share sheet, which should include other apps to the right of the Messages, Facebook, etc icons - applications that can work with .txt files, or just an NSString.
I am currently displaying a Share sheet via UIActivityViewController, which is working great but it does not include other apps in the list. From reading other SO questions I concluded it's only possible to get those other apps to appear if you use UIDocumentInteractionController instead. I looked into creating a .txt file in a temp directory to share that file (instead of just sharing an NSString), but only Mail (no Copy) shows up when I tap the Share button. [Do note that if I run it on a real device not the simulator more apps other than Mail will appear and AirDrop too.] When I tap Mail, the app crashes: Unable to get data for URL: The operation couldn’t be completed. (Cocoa error 260.) Something is wrong with the way I'm creating/retrieving the .txt file.
My questions are:
Why is my code resulting in a crash when attempting to share the .txt file?
How can I get the Copy option to appear in the same Share sheet as the one that includes other apps?
To summarize: I need a share sheet that includes: Copy, AirDrop, Messages, Mail, Facebook, Twitter, Pages, Dropbox, etc for a simple string of text. Thanks!
The following lines of code lie inside my IBAction share button tap function:
UIActivityViewController approach:
UIActivityViewController *activityView = [[UIActivityViewController alloc] initWithActivityItems:#[self.myUITextField.text] applicationActivities:nil];
[self presentViewController:activityView animated:YES completion:nil];
Result:
UIDocumentInteractionController approach:
NSString *fileName = [NSString stringWithFormat:#"%#mytextfile.txt", NSTemporaryDirectory()];
[self.myUITextField.text writeToFile:fileName
atomically:NO
encoding:NSStringEncodingConversionAllowLossy
error:nil];
NSURL *textFileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:#"mytextfile.txt"]];
UIDocumentInteractionController *documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:textFileURL];
[documentInteractionController presentOptionsMenuFromBarButtonItem:sender animated:YES];
Result (will show more apps and AirDrop if I run on a real device):
Example of what I want to obtain - minus the 3 extra options at the bottom:
If I cannot obtain the above screenshot with a string (instead of a photo) for some reason, I am willing to implement it how Dropbox has done it. They added an Open In button at the bottom that presents a different sheet that only shows additional apps. Note that I would still need a Copy option on the original sheet.
Question 1: Why is my code resulting in a crash
Cocoa error 260 is an NSFileReadNoSuchFileError according to the Foundation Constants Reference document. Looking at your code, the only way how I can see that file creation might fail is if self.myUITextField is nil.
I suggest that you check this first. If the property is not nil, then check whether writeToFile:atomically:encoding:error: returns an error.
Question 2: How can I get the Copy option to appear
First, assign a delegate to the controller:
documentInteractionController.delegate = self;
Then implement the following two delegate methods:
- (BOOL) documentInteractionController:(UIDocumentInteractionController*)controller canPerformAction:(SEL)action
{
if (#selector(copy:) == action)
return YES;
else
return NO;
}
- (BOOL) documentInteractionController:(UIDocumentInteractionController*)controller performAction:(SEL)action
{
if (#selector(copy:) != action)
return NO;
// Perform the copy: action
return YES;
}
Both methods are marked deprecated since iOS 6, but they still seem to work in iOS 7. Unfortunately, I have no idea how to implement the copy: action without those two methods - and neither does Apple, or so it seems to me, since they do not offer a replacement, and the official Document Interaction Programming Topics for iOS document still happily refers to the methods without indication that they are deprecated.
Anyway, here's a simple but complete implementation of the second delegate method:
- (BOOL) documentInteractionController:(UIDocumentInteractionController*)controller performAction:(SEL)action
{
if (#selector(copy:) != action)
return NO;
NSStringEncoding usedEncoding;
NSError* error;
NSString* fileContent = [NSString stringWithContentsOfURL:controller.URL
usedEncoding:&usedEncoding
error:&error];
UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
[pasteboard setString:fileContent];
return YES;
}

iOS - Load NSURL inside MFMailComposeViewController

Is it possible to load a URL inside the body of MFMailComposeViewController? If not, what is the efficient way where I can load this URL (i.e. HTML) so I can load it in the mail body?
You can load HTML into a MFMailComposeViewController like this:
MFMailComposeViewController *composer = [MFMailComposeViewController new];
[composer setSubject:#"HTML test"];
[composer setMessageBody:#"<html><body><h1>Test</h1></body></html>" isHTML:YES];
However, you can't load an externally hosted website into it. You would have to fetch the HTML document and modify it, so that the links to the assets (images, scripts etc.) are absolute and include the protocol and hostname of the remote host (where you downloaded the website). You then could set this modified source code as the composer's messageBody.

Resources