Share attachment from Mail App with Share extension in iOS
So finally I found an answer on my question! Just in case if somebody will meet the same problem..
First of all I have to use PREDICATE statement (Subquery) in Info.plist instead of key NSExtensionActivationSupportsAttachmentsWithMaxCount. Like:
<key>NSExtensionActivationRule</key>
<string>SUBQUERY (
extensionItems,
$extensionItem,
SUBQUERY (
$extensionItem.attachments,
$attachment,
(
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.png"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.jpeg-2000"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.tiff"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.compuserve.gif"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.microsoft.bmp"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.microsoft.word.doc"
)
).@count == 1 // Important! to activate extension only on 1 chosen image
).@count == 1
</string>
Second: Properly get all attachments using necessary TypeIdentifier (UTI):
if let content = extensionContext!.inputItems.first as? NSExtensionItem {
if let contents = content.attachments as? [NSItemProvider] {
for attachment in contents{
attachment.loadItemForTypeIdentifier("public.item", options: nil) { data, error in
let url = data as! NSURL
let fileExtension = url.pathExtension as String!
let fileName = self.generateImageName() as String
if let fileData = NSData(contentsOfURL: url) {
self.uploadFile("\(fileName).\(fileExtension)", data: fileData)
}
}
}
}
}
"public.item" - is universal UTI to support all kind of file extensions listed in your NSExtensionActivationRule string.
You can get necessary UTI on https://developer.apple.com
Good Luck with developing of action extensions! Any questions are welcome!
Code to share file path/file between a share extension and iOS app
UPDATE
The app was accepted in the review using NSUSERDEFAULTS
as the message passing mechanism. Here are the steps.
1.) Share extension:
#import "ShareViewController.h"
#import <MobileCoreServices/UTCoreTypes.h>
//Macro to hide post dialog or not, if defined, will be hidden, comment during debugging
#define HIDE_POST_DIALOG
@interface ShareViewController ()
@end
@implementation ShareViewController
NSUInteger m_inputItemCount = 0; // Keeps track of the number of attachments we have opened asynchronously.
NSString * m_invokeArgs = NULL; // A string to be passed to your AIR app with information about the attachments.
NSString * APP_SHARE_GROUP = @"group.com.schemename.nameofyourshareappgroup";
const NSString * APP_SHARE_URL_SCHEME = @"schemename";
CGFloat m_oldAlpha = 1.0; // Keeps the original transparency of the Post dialog for when we want to hide it.
- (BOOL)isContentValid {
// Do validation of contentText and/or NSExtensionContext attachments here
return YES;
}
- ( void ) didSelectPost
{
#ifdef HIDE_POST_DIALOG
return;
#endif
[ self passSelectedItemsToApp ];
// Note: This call is expected to be made here. Ignore it. We'll tell the host we are done after we've invoked the app.
// [ self.extensionContext completeRequestReturningItems: @[] completionHandler: nil ];
}
- ( void ) addImagePathToArgumentList: ( NSString * ) imagePath
{
assert( NULL != imagePath );
// The list of arguments we will pass to the AIR app when we invoke it.
// It will be a comma-separated list of file paths: /path/to/image1.jpg,/path/to/image2.jpg
if ( NULL == m_invokeArgs )
{
m_invokeArgs = imagePath;
}
else
{
m_invokeArgs = [ NSString stringWithFormat: @"%@,%@", m_invokeArgs, imagePath ];
}
}
- ( NSString * ) saveImageToAppGroupFolder: ( UIImage * ) image
imageIndex: ( int ) imageIndex
{
assert( NULL != image );
NSData * jpegData = UIImageJPEGRepresentation( image, 1.0 );
NSURL * containerURL = [ [ NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier: APP_SHARE_GROUP ];
NSString * documentsPath = containerURL.path;
// Note that we aren't using massively unique names for the files in this example:
NSString * fileName = [ NSString stringWithFormat: @"image%d.jpg", imageIndex ];
NSString * filePath = [ documentsPath stringByAppendingPathComponent: fileName ];
[ jpegData writeToFile: filePath atomically: YES ];
//Mahantesh -- Store image url to NSUserDefaults
NSUserDefaults *defaults=[[NSUserDefaults alloc] initWithSuiteName:@"group.com.schemename.nameofyourshareappgroup"];
[defaults setObject:filePath forKey:@"url"];
[defaults synchronize];
return filePath;
}
- ( void ) passSelectedItemsToApp
{
NSExtensionItem * item = self.extensionContext.inputItems.firstObject;
// Reset the counter and the argument list for invoking the app:
m_invokeArgs = NULL;
m_inputItemCount = item.attachments.count;
// Iterate through the attached files
for ( NSItemProvider * itemProvider in item.attachments )
{
// Check if we are sharing a Image
if ( [ itemProvider hasItemConformingToTypeIdentifier: ( NSString * ) kUTTypeImage ] )
{
// Load it, so we can get the path to it
[ itemProvider loadItemForTypeIdentifier: ( NSString * ) kUTTypeImage
options: NULL
completionHandler: ^ ( UIImage * image, NSError * error )
{
static int itemIdx = 0;
if ( NULL != error )
{
NSLog( @"There was an error retrieving the attachments: %@", error );
return;
}
// The app won't be able to access the images by path directly in the Camera Roll folder,
// so we temporary copy them to a folder which both the extension and the app can access:
NSString * filePath = [ self saveImageToAppGroupFolder: image imageIndex: itemIdx ];
// Now add the path to the list of arguments we'll pass to the app:
[ self addImagePathToArgumentList: filePath ];
// If we have reached the last attachment, it's time to hand control to the app:
if ( ++itemIdx >= m_inputItemCount )
{
[ self invokeApp: m_invokeArgs ];
}
} ];
}
}
}
- ( void ) invokeApp: ( NSString * ) invokeArgs
{
// Prepare the URL request
// this will use the custom url scheme of your app
// and the paths to the photos you want to share:
NSString * urlString = [ NSString stringWithFormat: @"%@://%@", APP_SHARE_URL_SCHEME, ( NULL == invokeArgs ? @"" : invokeArgs ) ];
NSURL * url = [ NSURL URLWithString: urlString ];
NSString *className = @"UIApplication";
if ( NSClassFromString( className ) )
{
id object = [ NSClassFromString( className ) performSelector: @selector( sharedApplication ) ];
[ object performSelector: @selector( openURL: ) withObject: url ];
}
// Now let the host app know we are done, so that it unblocks its UI:
[ super didSelectPost ];
}
#ifdef HIDE_POST_DIALOG
- ( NSArray * ) configurationItems
{
// Comment out this whole function if you want the Post dialog to show.
[ self passSelectedItemsToApp ];
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
return @[];
}
#endif
#ifdef HIDE_POST_DIALOG
- ( void ) willMoveToParentViewController: ( UIViewController * ) parent
{
// This is called at the point where the Post dialog is about to be shown.
// Make it transparent, so we don't see it, but first remember how transparent it was originally:
m_oldAlpha = [ self.view alpha ];
[ self.view setAlpha: 0.0 ];
}
#endif
#ifdef HIDE_POST_DIALOG
- ( void ) didMoveToParentViewController: ( UIViewController * ) parent
{
// Restore the original transparency:
[ self.view setAlpha: m_oldAlpha ];
}
#endif
#ifdef HIDE_POST_DIALOG
- ( id ) init
{
if ( self = [ super init ] )
{
// Subscribe to the notification which will tell us when the keyboard is about to pop up:
[ [ NSNotificationCenter defaultCenter ] addObserver: self selector: @selector( keyboardWillShow: ) name: UIKeyboardWillShowNotification object: nil ];
}
return self;
}
#endif
#ifdef HIDE_POST_DIALOG
- ( void ) keyboardWillShow: ( NSNotification * ) note
{
// Dismiss the keyboard before it has had a chance to show up:
[ self.view endEditing: true ];
}
#endif
@end
In the openURL method of your application delegate
//Slartibartfast -- For the case where we are opening app from an extension
NSString *STATIC_FILE_HANDLE = @"file://";
//If app is opened from share extension, do the following
/*
1.) Get path of shared file from NSUserDefaults
2.) Get data from file and store in some variable
3.) Create a new accesible unique file path
4.) Dump data created into this file.
*/
NSUserDefaults *defaults=[[NSUserDefaults alloc] initWithSuiteName:YOURAPP_STATIC_APP_GROUP_NAME];
NSString *path=nil;
if(defaults)
{
[defaults synchronize];
path = [defaults stringForKey:@"url"];
}
if(path.length != 0)
{
NSData *data;
//Get file path from url shared
NSString * newFilePathConverted = [STATIC_FILE_HANDLE stringByAppendingString:path];
url = [ NSURL URLWithString: newFilePathConverted ];
data = [NSData dataWithContentsOfURL:url];
//Create a regular access path because this app cant preview a shared app group path
NSString *regularAccessPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *uuid = [[NSUUID UUID] UUIDString];
//Copy file to a jpg image(ignore extension, will convert from png)
NSString *uniqueFilePath= [ NSString stringWithFormat: @"/image%@.jpg", uuid];
regularAccessPath = [regularAccessPath stringByAppendingString:uniqueFilePath];
NSString * newFilePathConverted1 = [STATIC_FILE_HANDLE stringByAppendingString:regularAccessPath];
url = [ NSURL URLWithString: newFilePathConverted1 ];
//Dump existing shared file path data into newly created file.
[data writeToURL:url atomically:YES];
//Reset NSUserDefaults to Nil once file is copied.
[defaults setObject:nil forKey:@"url"];
}
//Do what you want
}
Thanks to EasyNativeExtensions for pointers
MacOS Share Extension: can't get activation types working
Started working after a system reboot
Related Topics
How to Use Searchbar for API'S
Add Gps Metadata Dictionary to Image Taken with Avfoundation in Swift
How to Know a Device Is Available to Use Capturetextfromcamera API
When Should I Use Anyobject Insted of Uibutton in Swift
Adding Custom Game Logic to Scene Kit (Swift)
Programmatically Set Uipageviewcontroller Transition Style to Scroll
How Do Ruler Apps Stay Accurate on All Devices
Disable Vertical Scroll in Uiscrollview Swift
How to Get the Original Filename of a Sprite/Texture in Swift
Geolocation by Iphone's Ip Address
Moya/Alamofire - Url Encoded Params with Same Keys
Fbsdkcorekit.Framework/Fbsdkcorekit: No Matching Architecture in Universal Wrapper
Swift Extension and Enum for Color Schemes
My Uiviews Muck-Up When I Combine Uipangesturerecognizer and Autolayout
How to Programmatically Check and Open an Existing App in iOS 8