UIDocumentInteractionController, No File Extension but UTI
Asked Answered
W

1

7

How do I send a file to a different App knowing which UTI the App supports? Lets say the file has no file extension, but I happen to know the UTI of the file.

I tried the following:

// target is a NSURL with the location of the extension less file on the system
// knownUTI is a NSString containing the UTI of the file 
    UIDocumentInteractionController* dic = [UIDocumentInteractionController interactionControllerWithURL:target];  
    [dic retain];

    dic.delegate = self;
    dic.UTI = knownUTI; 
    [dic presentOpenInMenuFromRect:CGRectZero inView:superController.view animated:YES]

It shows the supported App, however, if I select it, it won't switch the App. The delegate calls the

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

but

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

is never called and the Application is never switching.

The target App exports its UTI in the following:

    <key>CFBundleDocumentTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeIconFiles</key>
            <array/>
            <key>CFBundleTypeName</key>
            <string>Migration DocType</string>
            <key>CFBundleTypeRol</key>
            <string>Shell</string>
            <key>LSHandlerRank</key>
            <string>Owner</string>
            <key>LSItemContentTypes</key>
            <array>
                <string>com.mycomp.customstring</string>
            </array>
        </dict>
    </array>

...

<key>UTExportedTypeDeclarations</key>
    <array>
        <dict>
            <key>UTTypeConformsTo</key>
            <array>
                <string>public.data</string>
            </array>
            <key>UTTypeDescription</key>
            <string>My custom UTI</string>
            <key>UTTypeIdentifier</key>
            <string>com.mycomp.customstring</string>
        </dict>
    </array>

As this did not work, I also tried adding a custom extension. Still, it would not work in this way. When adding the custom extension to the file I hand over to the DocumentInteractionController and it works. However, the list of applications shows all other applications supporting the same file extension regardless of the UTI type I supply.

Say I declare 2 UTIs in 2 different applications:

App1 with UTI1: com.mycomp.a  with extension .abc
App2 with UTI2: com.mycomp.b  with extension .abc

When handing the file to the DocumentInteractionController, and setting the UTI to com.mycomp.a it will also show App2 as a possible application being able to handle the file.

I defined a UTI with extension in the following manner:

<key>UTExportedTypeDeclarations</key>
    <array>
        <dict>
            <key>UTTypeConformsTo</key>
            <array>
                <string>public.data</string>
            </array>
            <key>UTTypeDescription</key>
            <string>My UTI Type</string>
            <key>UTTypeIdentifier</key>
            <string>com.mycomp.a</string>
            <key>UTTypeTagSpecification</key>
            <dict>
                <key>public.filename-extension</key>
                <string>abc</string>
                <key>public.mime-type</key>
                <string>application/abc</string>
            </dict>
        </dict>
    </array>

I would really appreciate your help, I'm kind of stuck. So, again the question: How do I send a file to an App with known UTI either without extension or having the same extension as other files of which I don't want to show the applications as choice in DocumentInteractionController?

Thanks

Watersoak answered 12/12, 2011 at 19:44 Comment(0)
W
2

I found a solution to this problem. However, I think it is not a very good one.

During testing I found out that when leaving the file extension away, the UIDocumentInteractionController will show the applications depending on the UTI I specified. When sending the file to the target application nothing would happen. I concluded that I need an file extension to do the final sending.

My approach was to modify the URL property before the file is sent to the target application and supply it the same file but with a file extension the target application accepts. Nevertheless, my application just crashed. I profiled it with Instruments and found that the problem was due to UIDocumentInteractionController overreleasing some proxy object. I also saw that the final overrelease was in a method called _invalidate of UIDocumentInteractionController(Private) category was called which released the object.

As categories cannot be overridden by other categories I decided to swizzle the category method with my own implementation checking if the URL was containing a file extension or not and either redirect the call to the original _invalidate method or just do nothing.

The following codes shows what I did:

#include <objc/runtime.h>

@interface UIDocumentInteractionController(InvalidationRedirect)

-(void)_invalidateMY;
+(void)load;
void Swizzle(Class c, SEL orig, SEL newSEL);
@end

@implementation UIDocumentInteractionController(InvalidationRedirect)

void Swizzle(Class c, SEL orig, SEL newSEL)
{
    Method origMethod = class_getInstanceMethod(c, orig);
    Method newMethod = class_getInstanceMethod(c, newSEL);
    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
        method_exchangeImplementations(origMethod, newMethod);
}

-(void)_invalidateMY{
    @synchronized(self) {
        if(![[[[self.URL lastPathComponent] componentsSeparatedByString:@"."] lastObject] isEqualToString:@"extension"]) {
            [self _invalidateMY];
        }
    }
}

+(void)load
{
    Swizzle([UIDocumentInteractionController class], @selector(_invalidate), @selector(_invalidateMY));
}

@end

This code exchanges the original _invalidate method with _invalidateMY, resulting in every call to _invalidate calling _invalidateMY and vice versa.

The following code shows how I handle the UIDocumentInteractionController:

  // create a file without extension
  NSString *fileName = @"myFile";

  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

  NSString *documentsDirectory = [paths objectAtIndex:0];

  NSURL* target = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/%@", documentsDirectory, fileName]];

  if([[@"THIS IS JUST A TEST STRING" dataUsingEncoding:NSUTF8StringEncoding] writeToURL:target atomically:NO]) {
      NSLog(@"file written successfully");
  }else {
      NSLog(@"Error.. file writing failed");
  }

  UIDocumentInteractionController* dic = [UIDocumentInteractionController interactionControllerWithURL:target];
  [dic retain];
  dic.delegate = self;


  // set the UTI to the known UTI we want to list applications for
  dic.UTI = @"com.mycomp.a";

  [dic presentOpenInMenuFromRect:CGRectZero inView:superController.view animated:YES];

And this code shows the UIDocumentInteractionController's delegate method which exchanges the URL:

- (void)documentInteractionController:(UIDocumentInteractionController *)controller willBeginSendingToApplication:(NSString *)application
{
    NSFileManager *fileMgr = [NSFileManager defaultManager];
    NSError *error;
    NSURL* newTarget = [NSURL URLWithString:[NSString stringWithFormat:@"%@.extension", controller.URL]];
    // rename file to file with extension
    if (![fileMgr moveItemAtURL:controller.URL toURL:newTarget error:&error] && error) {
        NSLog(@"Error moving file: %@", [error localizedDescription]);
    }
    @synchronized(controller) {
        //exchange URL with URL+extension
        controller.URL = newTarget; //<- this results in calling _invalidate
    }
    NSLog(@"%@", [NSString stringWithContentsOfURL:controller.URL encoding:NSUTF8StringEncoding error:nil]);
}

This solution works, but in my opinion it is a dirty hack, there must be a better solution.

Watersoak answered 13/12, 2011 at 17:16 Comment(3)
I found a simpler solution. The code from the willBeginSendingToApplication: can already be executed after the open in menu was successfully called, this erases the need to swizzle the methods!Watersoak
Please provide code for this. I have tried setting the name property but it does not reflect in the 3rd party app.Equality
Yes, a detailed writeup of the real solution would be most helpful.Windhoek

© 2022 - 2024 — McMap. All rights reserved.