18

I am trying to use UIActivityItemProvider to share a file from within my app via email attachment. I also need to populate the subject line of the email and to specify the name of the attachment to be something different than the name of the file stored on the device.

Here is the code that I'm using. The problem is that the attachment is missing from the email.

@interface ItemProvider:UIActivityItemProvider
@property (nonatomic, strong) NSURL *filepath;
@property (nonatomic, strong) NSString *emailBody;
@property (nonatomic, strong) NSString *emailSubject;
@end

@implementation ItemProvider

- (id)initWithPlaceholderItem:(id)placeholderItem
{
    //Initializes and returns a provider object with the specified placeholder data
    return [super initWithPlaceholderItem:placeholderItem];
}

- (id)item
{
    //Generates and returns the actual data object
    return [NSDictionary dictionary];
}

// The following are two methods in the UIActivityItemSource Protocol
// (UIActivityItemProvider conforms to this protocol) - both methods required
#pragma mark UIActivityItemSource

//- Returns the data object to be acted upon. (required)
- (id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(NSString *)activityType
{


    if ([activityType isEqualToString:UIActivityTypeMail]) {
        return @{@"body":self.emailBody, @"url":self.filepath};
    }


    return @{@"body":self.emailBody, @"url":self.filepath};
}

//- Returns the placeholder object for the data. (required)
//- The class of this object must match the class of the object you return from the above method
- (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController
{
    return @{@"body":self.emailBody, @"url":self.filepath};
}

-(NSString *) activityViewController:(UIActivityViewController *)activityViewController subjectForActivityType:(NSString *)activityType {
    return self.emailSubject;
}

@end

And then in my viewController I do this:

      ItemProvider *provider = [[ItemProvider alloc] initWithPlaceholderItem:@{@"body":emailBody, @"url":filePath}];
    provider.emailBody = emailBody;
    provider.emailSubject = info.title;
    provider.filepath = filePath;
    NSArray *activityItems = @[provider];

    // Build a collection of custom activities (if you have any)
//    NSMutableArray *customActivities = [[NSMutableArray alloc] init];


    UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil];

    [self presentViewController:activityController animated:YES completion:nil];
RawMean
  • 7,547
  • 3
  • 52
  • 79

2 Answers2

34

For those still stumbling upon a solution for this, there is a more elegant solution for customizing UIActivityViewController. To address the original question, the reason the attachment is not showing up is because it is supposed to be a separate UIActivityItemProvider object.

So the solution is to create two UIActivityItemProvider subclasses, one to wrap the 'emailBody' and 'emailSubject' and another to wrap the attachment. The benefit to using a UIActivityItemProvider for the attachment is that you have the opportunity to delay processing the attachment until it is needed, rather than doing so before presenting UIActivityViewController.

Implement the AttachmentProvider class to provide the attachment like so:

@implementation AttachmentProvider : UIActivityItemProvider

- (id)item {
    if ([self.activityType isEqualToString:UIActivityTypeMail]) {

        /* Replace with actual URL to a file. Alternatively
         * you can also return a UIImage.
         */

        return [NSData dataWithContentsOfURL:dataURL];
    }
    return nil;
}

@end

Implement EmailInfoProvider class to provider the email body and subject class like so:

@implementation EmailInfoProvider : UIActivityItemProvider

- (id)item {
    return @"Your email body goes here";
}

- (NSString *)activityViewController:(UIActivityViewController *)activityViewController subjectForActivityType:(NSString *)activityType {
    if ([activityType isEqualToString:UIActivityTypeMail]) {
        return @"Your subject goes here";
    }
    return nil;
}

@end

You can then create a UIActivityViewController with both these items in your viewController like so:

- (void)shareAction {

    AttachmentProvider *attachment  = [[AttachmentProvider alloc] init];
    EmailInfoProvider *emailContent = [[EmailInfoProvider alloc] init];

    // You can provider custom -(id)init methods to populate EmailInfoProvider

    UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:@[attachment, emailContent] applicationActivities:nil];
    [self presentViewController:activityController animated:YES completion:nil];
}
ZGl6YXNt
  • 1,705
  • 16
  • 15
dbart
  • 4,772
  • 2
  • 19
  • 18
  • You don't even need to create two subclasses. You can put it the same one. `UIActivityItemProvider` implements the `UIActivityItemSource` protocol anyway. – orkoden Nov 18 '15 at 14:24
  • 2
    @orkoden how would you combine email body and attachments in single provider? – Ben Affleck Dec 17 '15 at 13:54
  • 1
    On trying this approach I saw that it limits the activities that are available to you in the activity controller. If I have a single file item in the activity items array, iOS offers me actions to add to iCloud Drive, save to Dropbox, etc. If I add in a second provider with the body/subject data these actions do not appear. This is fine if email is the only concern, but it's something to be wary of. – Craig Jan 04 '17 at 09:53
  • @highmaintenance by adding multiple checks on activityType in (id) item as you're already performing. All this can be done inside of a single provider. You will still need to create two instances of this provider and then assign perhaps a custom internal activity type (such as a new EmailActivityType property), but at least all this code is inside of a single subclass instead of two – strangetimes Apr 02 '19 at 11:44
8

i'm sending email with attachment without ItemProvider. its working well :-)

NSMutableArray *selDocs = [[NSMutableArray alloc] init];
for (Document *theDoc in self.selectedDocs) {
     NSURL *fileUrl = [NSURL fileURLWithPath:theDoc.filePath];       
    [selDocs addObject:fileUrl];
}
NSArray *postItems = [NSArray arrayWithArray:selDocs];

UIActivityViewController *avc = [[UIActivityViewController alloc] initWithActivityItems:postItems applicationActivities:nil];
[avc setValue:@"Your email Subject" forKey:@"subject"];

avc.completionHandler = ^(NSString *activityType, BOOL completed){
    NSLog(@"Activity Type selected: %@", activityType);
    if (completed) {
        NSLog(@"Selected activity was performed.");
    } else {
        if (activityType == NULL) {
            NSLog(@"User dismissed the view controller without making a selection.");
        } else {
            NSLog(@"Activity was not performed.");
        }
    }
};

[self presentViewController:avc animated:YES completion:nil];
thorb65
  • 2,631
  • 2
  • 25
  • 36
  • 1
    Thanks. Your solution works but does not meet the following requirements (I updated the question to make it clear): 1. I need to populate the subject line of the email (that's one reason I resorted to the Provider solution). 2. I need to give a name to the attachment that is different from the filename. – RawMean Dec 14 '13 at 21:01
  • 3
    why you guys dont learn to describe exactly what you want or need? its wasted time for people that wants to help you if you first say i need this and then after receiving answers say but i need more this and this and this ... i have added setting subject... – thorb65 Dec 14 '13 at 21:30
  • You are right about not being specific enough about the requirement. I apologize. [avc setValue:@"Your email Subject" forKey:@"subject"] is not documented, so I think it'll be rejected by Apple. I think that's why Apple has added this API and they want you to use it instead. -(NSString *) activityViewController:(UIActivityViewController *)activityViewController subjectForActivityType:(NSString *)activityType; Also, this still doesn't solve the problem with the attachment with a custom name. – RawMean Dec 14 '13 at 22:11
  • 1
    lol... we have about 30 apps in app store and using the subject in a more than one app... setting mail subject is nothing what apple wants to prevent... and for your attachment title it would be easy to temporary rename your file on disk to the title you want und after loading the file into the mail composer naming it back to the original title... – thorb65 Dec 14 '13 at 22:25
  • If you wanted to add email body text along with the file attachment...is that possible? – Greg Maletic Mar 06 '14 at 19:08
  • I am curious about the subject key. Is there any other key in the UIActivityViewController? Say that if the user selects activityTypeMessage, it would only include text and a url. If it is activityTypeFacebook then show text, url and an image. Is that possible? – SleepNot Nov 11 '14 at 04:01
  • This is not working for Email in simulator, I am using UIActivityViewController for sharing in all Social media installed in mobile. But in email body nothing showing. – Ram S Jun 29 '16 at 11:06
  • you cannot test mail in simulator. physical device only. – thorb65 Jun 30 '16 at 04:27