【问题标题】:Code to share file path/file between a share extension and iOS app在共享扩展和 iOS 应用程序之间共享文件路径/文件的代码
【发布时间】:2015-11-20 14:02:16
【问题描述】:

我为我的应用添加了一个共享扩展,比如 SAMPLE(应用商店中已经存在),称为 SAMPLESHARE。每当用户拍照并尝试分享时,我希望他们通过 Open In 功能的视图控制器,而不是从 Apple 获得 Post 对话,基本上绕过它。所以我试图在共享扩展和我的应用程序之间共享图片,方法是创建一个在应用程序和插件之间共享的应用程序组,然后将文件路径传递给我的应用程序的应用程序委托的 openURL。

所以在我的主应用程序委托中我有

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{

    return [[SAMPLEExternalFileHandler shared] handleExternalFileURL:url];
}

基本上我每次都使用它来检查我是否有需要打开不同流程的 URL 文件路径。

在我的 SHAREEXTENSION 我有

#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.SAMPLE.SAMPLESHAREPLUGIN";
const NSString * APP_SHARE_URL_SCHEME = @"SAMPLE";
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 ];

    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 JPEG
        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

我的扩展名 info.plist 是

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>en</string>
    <key>CFBundleDisplayName</key>
    <string>SAMPLESHARE</string>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>com.org.SAMPLE.$(PRODUCT_NAME:rfc1034identifier)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>$(PRODUCT_NAME)</string>
    <key>CFBundlePackageType</key>
    <string>XPC!</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>1</string>
    <key>NSExtension</key>
    <dict>
        <key>NSExtensionAttributes</key>
         <dict>
        <key>NSExtensionActivationRule</key>
        <dict>
            <key>NSExtensionActivationSupportsImageWithMaxCount</key>
            <integer>1</integer>
        </dict>
    </dict>
        <key>NSExtensionMainStoryboard</key>
        <string>MainInterface</string>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.share-services</string>
    </dict>
</dict>
</plist>

我基本上使用了互联网上的一些公共许可证代码(知名网站),声称已通过应用商店审核流程。

代码中有两种解决方法,一种是从共享扩展中调用 OpenURL(从搜索 SO 看来,如果没有 iOS 8.3 及更高版本的解决方法,这似乎仍然无法正常进行),第二种是隐藏帖子对话以及当任何人点击分享时苹果默认提供的键盘。 这行得通。

我有两个问题

1.) Will this be accepted on the app store? -- basically how are apps like facebook/whatsapp doing it and they are being accepted?
2.) Whenever I run this, it says `NSExtensionActivationRule` if set to `TRUEPREDICATE` will be rejected in review, what should the value be? 

更新:

因此,我浏览了文档,找到了问题 2 的修复方法,并对其进行了更改。现在一切正常,并且没有TRUEPREDICATE,这会在商店中被接受还是有其他方法可以做到这一点?

更新 2:

我现在已经使用NSUserDefaults 将数据从扩展程序传递到应用程序,猜想这也是共享数据的一项要求。

【问题讨论】:

  • 我稍后会更新它。是的,它在审查中被接受
  • 感谢这篇有用的帖子,我花了几个小时搜索有关此主题的信息
  • @VrashabhIrde 我正在尝试做同样的事情,但是对于视频,由于内存限制,我无法将其保存到共享存储中。知道我该怎么做吗?

标签: ios objective-c ios8-share-extension


【解决方案1】:

更新

使用NSUSERDEFAULTS 作为消息传递机制的应用在审核中被接受。以下是步骤。

1.) 分享扩展:

#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
  1. 在您的应用程序委托的 openURL 方法中

            //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
        }
    

感谢EasyNativeExtensions 指点

【讨论】:

【解决方案2】:
  1. 如果您不想显示来自苹果的默认对话框。 不应该从 UIViewController 继承 @interface ShareViewController : SLComposeServiceViewController
  2. 在 Apple 开发者文档中,不允许直接打开包含应用的扩展应用,除了 Today 扩展应用。

【讨论】:

  • 现在有其他应用程序以这种方式使用共享扩展的示例。例如,Pixelmator 使用共享操作与 Snapseed 共享图像,共享操作没有 UI,并且在 extensionContext 上调用 openURL 以以类似于今日扩展显示的方式实现共享。另一个示例,您可以从照片中将许多图像以 pdf 格式共享到 iBooks。如果您选择几张图片,则不会显示任何 UI,如果很多则有一个小的 UIViewcontrollerv 显示进度条,则 iBooks 会打开并在库中显示 PDF。
  • @RocketGarden,我在我的应用中实现了共享扩展,在 iOS 13 中,我的共享扩展应用无法打开主应用以接收共享文件/图像,因为 UIApplication.shared 现在不可用。以前的所有版本都可以正常工作。那么,开发人员如何从不同来源接收/导入数据呢?我想从 gmail 或驱动器或使用苹果共享按钮的任何地方打开我的应用程序的文件,有什么解决方法吗?今天的扩展是否提供相同的功能?
猜你喜欢
  • 2023-03-15
  • 2019-03-29
  • 1970-01-01
  • 2018-07-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-25
相关资源
最近更新 更多