在我的iOS应用程序中支持“在...中打开”菜单项,适用于Mail和Safari浏览器。

49

我需要通过UIDocumentInteractionController类中的"打开方式..."实现从Safari和Mail应用程序打开文档。我该怎么做?

2个回答

97
我知道对于一个初学者或现在的中等水平的程序员来说,这非常令人沮丧。通过邮件和Safari应用程序进行文件I/O涉及到应用程序内部非常有趣的命名约定。因此,让我们通过一个iPhone的Xcode项目来动手实践。打开Xcode(本教程将使用4.2版本),选择“单视图”应用程序模板(或创建一个空项目,然后添加一个带有.xib的单视图)。

Screenshot showing Xcode template selection sheet

在新创建的应用程序中,将视图控制器(和相关的xib)重命名为OfflineReaderViewController,然后我们将进入代码。(我们将接触每个文件,但不包括前缀头文件和main.m,所以请确保你需要所有的文件!)
进入AppDelegate头文件并粘贴以下代码:
#import <UIKit/UIKit.h>

@class OfflineReaderViewController;

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@property (strong, nonatomic) OfflineReaderViewController *viewController;

@end

然后进入委托的 .m 文件,并粘贴以下代码:

#import "AppDelegate.h"
#import "OfflineReaderViewController.h"

@implementation AppDelegate

@synthesize window;
@synthesize viewController;

-(BOOL)application:(UIApplication *)application 
           openURL:(NSURL *)url 
 sourceApplication:(NSString *)sourceApplication 
        annotation:(id)annotation 
{    
    // Make sure url indicates a file (as opposed to, e.g., http://)
    if (url != nil && [url isFileURL]) {
        // Tell our OfflineReaderViewController to process the URL
        [self.viewController handleDocumentOpenURL:url];
    }
    // Indicate that we have successfully opened the URL
    return YES;
}

- (void)dealloc
{
    [window release];
    [viewController release];
    [super dealloc];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
    // Override point for customization after application launch.
    self.viewController = [[[OfflineReaderViewController alloc] initWithNibName:@"ViewController" bundle:nil] autorelease];
    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];
    return YES;
}

- (void)applicationWillResignActive:(UIApplication *)application
{
    /*
     Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
     Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
     */
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    /*
     Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 
     If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
     */
}

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    /*
     Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
     */
}

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    /*
     Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
     */
}

- (void)applicationWillTerminate:(UIApplication *)application
{
    /*
     Called when the application is about to terminate.
     Save data if appropriate.
     See also applicationDidEnterBackground:.
     */
}

@end

这个:

-(BOOL)application:(UIApplication *)application 
               openURL:(NSURL *)url 
     sourceApplication:(NSString *)sourceApplication 
            annotation:(id)annotation 
    {    
        if (url != nil && [url isFileURL]) {
            [self.viewController handleDocumentOpenURL:url];
        }    
        return YES;
    }

这是教程中最重要的部分。将其分解为各个部分:-(BOOL)application:(UIApplication *)application 是我们的示例应用程序;openURL:(NSURL *)url 是要打开的URL;sourceApplication:(NSString *)sourceApplication 是发送链接的应用程序;而 annotation:(id)annotation 则是我们不会涉及的额外功能。
现在,我们必须布局 xib。进入 xib(应该命名为“OfflineReaderViewController”,但对于 xib 来说无关紧要,除非我们调用 initWithNibName:(但我们不会这样做)),并使其看起来像下面的图片:

Screenshot of IB layout

非常重要的一点是进入 UIWebView 的属性并勾选 "Scales Pages To Fit",这样我们才能通过捏合手势放大和缩小网页。暂时不用担心连接,我们很快就会创建它们。
进入 OfflineReaderViewController 头文件并粘贴以下内容:
#import <UIKit/UIKit.h>

@interface OfflineReaderViewController : UIViewController 
<UIDocumentInteractionControllerDelegate> {
    IBOutlet UIWebView *webView;
}

-(void)openDocumentIn;
-(void)handleDocumentOpenURL:(NSURL *)url;
-(void)displayAlert:(NSString *) str;
-(void)loadFileFromDocumentsFolder:(NSString *) filename;
-(void)listFilesFromDocumentsFolder;

- (IBAction) btnDisplayFiles;

@end

现在是 .m 文件:
#import "OfflineReaderViewController.h"

@implementation OfflineReaderViewController

UIDocumentInteractionController *documentController;

-(void)openDocumentIn {    
    NSString * filePath = 
    [[NSBundle mainBundle] 
     pathForResource:@"Minore" ofType:@"pdf"];    
    documentController = 
    [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:filePath]];
    documentController.delegate = self;
    [documentController retain];
    documentController.UTI = @"com.adobe.pdf";
    [documentController presentOpenInMenuFromRect:CGRectZero 
                                           inView:self.view 
                                         animated:YES];
}

-(void)documentInteractionController:(UIDocumentInteractionController *)controller 
       willBeginSendingToApplication:(NSString *)application {

}

-(void)documentInteractionController:(UIDocumentInteractionController *)controller 
          didEndSendingToApplication:(NSString *)application {

}

-(void)documentInteractionControllerDidDismissOpenInMenu:
(UIDocumentInteractionController *)controller {

}
-(void) displayAlert:(NSString *) str {
    UIAlertView *alert = 
    [[UIAlertView alloc] initWithTitle:@"Alert" 
                               message:str 
                              delegate:self
                     cancelButtonTitle:@"OK"
                     otherButtonTitles:nil];
    [alert show];
    [alert release];    
}

- (void)handleDocumentOpenURL:(NSURL *)url {
    [self displayAlert:[url absoluteString]];
    NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];        
    [webView setUserInteractionEnabled:YES];    
    [webView loadRequest:requestObj];
}


-(void)loadFileFromDocumentsFolder:(NSString *) filename {
    //---get the path of the Documents folder---   
    NSArray *paths = NSSearchPathForDirectoriesInDomains(  
                                                         NSDocumentDirectory, NSUserDomainMask, YES); 
    NSString *documentsDirectory = [paths objectAtIndex:0];     
    NSString *filePath = [documentsDirectory 
                          stringByAppendingPathComponent:filename];    
    NSURL *fileUrl = [NSURL fileURLWithPath:filePath];        
    [self handleDocumentOpenURL:fileUrl];
}

-(void)listFilesFromDocumentsFolder {    
    //---get the path of the Documents folder---    
    NSArray *paths = NSSearchPathForDirectoriesInDomains(     
                                                         NSDocumentDirectory, NSUserDomainMask, YES); 
    NSString *documentsDirectory = [paths objectAtIndex:0]; 

    NSFileManager *manager = [NSFileManager defaultManager];
    NSArray *fileList =   
    [manager contentsOfDirectoryAtPath:documentsDirectory error:nil];
    NSMutableString *filesStr = 
    [NSMutableString stringWithString:@"Files in Documents folder \n"];
    for (NSString *s in fileList){    
        [filesStr appendFormat:@"%@ \n", s];
    }
    [self displayAlert:filesStr];    
    [self loadFileFromDocumentsFolder:@"0470918020.pdf"];
}

- (IBAction) btnDisplayFiles {
    [self listFilesFromDocumentsFolder];    
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc that aren't in use.
}

#pragma mark - View lifecycle

- (void)viewDidLoad {
    [super viewDidLoad];
    [self openDocumentIn];
}

- (void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // Return YES for supported orientations
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}

@end

如果你正在积极观察而不是仅仅复制我告诉你的一切(开个玩笑),那么你就会知道这行代码:[[NSBundle mainBundle] pathForResource:@"Minore" ofType:@"pdf"]; 会导致SIGABRT,因为嗯,文件不存在!所以,拖入任何你从任何地方得到的通用PDF文件(我推荐here,因为谁不喜欢花费大量时间阅读文档?),然后复制它的标题并将其粘贴在后缀(.pdf)被删除的情况下;ofType:@“pdf”部分会为我们处理。完成后,该行应如下所示:[[NSBundle mainBundle] pathForResource:@“//file name//”ofType:@“pdf”];

现在回到xib并连接那些IBOutlets!总之,这是您的“文件所有者”选项卡应该看起来像的:

Screenshot showing established connections

看起来我们已经完成了...但等等!我们还没有做任何事情来启动一个“打开方式”菜单! 好吧,事实证明,在 .plist 文件中需要进行一些操作。打开应用程序的 .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>${PRODUCT_NAME}</string>
    <key>CFBundleExecutable</key>
    <string>${EXECUTABLE_NAME}</string>
    <key>CFBundleIconFiles</key>
    <array/>
    <key>CFBundleIdentifier</key>
    <string>CodaFi.${PRODUCT_NAME:rfc1034identifier}</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>${PRODUCT_NAME}</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>1.0</string>
    <key>LSRequiresIPhoneOS</key>
    <true/>
    <key>UIRequiredDeviceCapabilities</key>
    <array>
        <string>armv7</string>
    </array>
    <key>UISupportedInterfaceOrientations</key>
    <array>
        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>UIFileSharingEnabled</key>
    <true/>
    <key>CFBundleDocumentTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeName</key>
            <string>PDF Document</string>
            <key>LSHandlerRank</key>
            <string>Alternate</string>
            <key>CFBundleTypeRole</key>
            <string>Viewer</string>
            <key>LSItemContentTypes</key>
            <array>
                <string>com.adobe.pdf</string>
            </array>
        </dict>
    </array>
</dict>
</plist>

[旁注:小心在任何plist的源代码中胡乱操作,如果你不知道自己在做什么,可能会从Xcode获得可怕的“该文件已损坏”错误]

如果右键单击并选择“打开方式>属性列表”,它将看起来像这样:

Shot of Xcode plist editor window

这里还有一个非常重要的字段,叫做“应用支持iTunes文件共享”。它必须设置为“YES”,否则您的应用程序将不会在iTunes中显示为支持文件共享。

“文档类型”字段指定了我们的示例可以打开的文档类型。展开箭头以找到其角色和UTI。这些是每种文件都有的唯一标识符(唯一类型标识符;现在很明显这个首字母缩写是什么意思了吧?)。UTI是让Finder替换通用文档图像为该文件类型的漂亮本地化图像的关键(不信,请将不重要的文件扩展名重命名为.ouhbasdvluhb并尝试获取漂亮图片!)如果我想要打开自己的自定义格式(比如.code文件),那么我将在UTI字段中放置类似于com.CodaFi.code(对于没有头绪的人来说,这是反向DNS表示法),文档类型名称将是'CodaFi Document'。处理程序等级和角色应该很简单,因为我们的处理程序等级是备用(因为我们不拥有该文件),我们的角色是查看器(因为我们不需要更重要的东西。我们的示例只是一个查看器,而不是编辑器,所以我们将其保留为此)。

为了以后参考,UTI有官方的系统声明命名方案,当它们来自受尊敬的来源(Oracle、Microsoft,甚至是Apple本身)时,可以在Uniform Type Identifier Reference Guide中找到,但出于追求完美而列出here

现在,让我们运行它!假设您逐字复制并正确地连接了那些可恶的xib,代码应该无错误地构建。现在,当您第一次启动应用程序时,您应该会看到在iBooks中打开文档的选项。取消选择它,代码的真正精华在于打开其他文档!启动Safari并搜索Safari可以快速查看或打开的任何PDF。然后在“打开方式...”菜单中,我们的应用程序显示出来!点击它。您将得到小的切换动画,并弹出一个带有文件位置的警报。当您关闭它时,UIWebView将加载PDF。邮件应用程序具有类似的功能与附件。您还可以将这些PDF调用到您的应用程序中。

就这样,全部都完成了。享受愉快的编码吧!


好的,1/3已经编辑完毕。这部分大约涉及到-(BOOL)application:(UIApplication *)application            openURL:(NSURL *)url  sourceApplication:(NSString *)sourceApplication         annotation:(id)annotation,请告诉我您是否需要这部分内容。如果是的话,我会完成它。(顺便说一句,我无法感谢您给我的赏金,这真是出乎意料之外的惊喜。) - CodaFi
1
谢谢您的回答。但是导入的文档并没有保存在文档目录的根目录中。它被保存在了file:///private/var/mobile/Applications/430B7EF0-40A7-43B8-94BF-5EAC851AF19D/Documents/Inbox/123.pdf中。我该如何将其保存到根目录? - Akshay Nalawade
我通过委托方法收到了URL。但是UIWebView没有加载那个PDF文件...我做错了什么? - user3441799
@CodaFi,当我从文档交互控制器中按下邮件时,我的委托方法没有被调用,请问你能帮我解决这个问题吗? - Verma

6
这个问题有一个很好的答案,可以在这里找到。我已经复制了部分答案以便更清楚,但是你应该参考那个问题获取完整的答案。
文件类型处理是iPhone OS 3.2中新增的功能,与已经存在的自定义URL方案不同。您可以注册您的应用程序来处理特定的文档类型,任何使用文档控制器的应用程序都可以将这些文档的处理交给您自己的应用程序。
要注册支持,您需要在Info.plist中添加以下内容:
<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>CFBundleTypeIconFiles</key>
        <array>
            <string>Document-molecules-320.png</string>
            <string>Document-molecules-64.png</string>
        </array>
        <key>CFBundleTypeName</key>
        <string>Molecules Structure File</string>
        <key>CFBundleTypeRole</key>
        <string>Viewer</string>
        <key>LSHandlerRank</key>
        <string>Owner</string>
        <key>LSItemContentTypes</key>
        <array>
            <string>com.sunsetlakesoftware.molecules.pdb</string>
            <string>org.gnu.gnu-zip-archive</string>
        </array>
    </dict>
</array>

以上示例中使用的UTI之一是系统定义的,但另一个是应用程序特定的UTI。为了使系统上的其他应用程序能够了解它,需要导出应用程序特定的UTI。为此,您需要像以下内容一样在Info.plist文件中添加一个部分:

<key>UTExportedTypeDeclarations</key>
<array>
    <dict>
        <key>UTTypeConformsTo</key>
        <array>
            <string>public.plain-text</string>
            <string>public.text</string>
        </array>
        <key>UTTypeDescription</key>
        <string>Molecules Structure File</string>
        <key>UTTypeIdentifier</key>
        <string>com.sunsetlakesoftware.molecules.pdb</string>
        <key>UTTypeTagSpecification</key>
        <dict>
            <key>public.filename-extension</key>
            <string>pdb</string>
            <key>public.mime-type</key>
            <string>chemical/x-pdb</string>
        </dict>
    </dict>
</array>

这个例子导出了扩展名为.pdb的文件,对应MIME类型为chemical/x-pdb的UTI,标识为com.sunsetlakesoftware.molecules.pdb。这样一来,你的应用程序就能够处理从电子邮件或系统上其他应用程序附加的文档。在邮件中,你可以点击并长按以显示可以打开特定附件的应用程序列表。当附件被打开时,你的应用程序将启动,并需要在-application:didFinishLaunchingWithOptions:应用程序委托方法中处理该文件的处理。似乎从邮件加载的文件会被复制到你的应用程序文档目录下与其所在邮箱相对应的子目录中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接