UIActivityViewController "Save to Files" saves multiple files when only 1 file is required

2.2k views Asked by At

My app is able to provide a variety of UIActivityItem types (text, data, rich text, print page renderer, etc) for sharing for a variety of purposes, including printing, copy/paste, and saving as a file. For the purpose of copy/paste, it needs to include plain text, attributed string, data (JSON Data) and JSON string).

However, because several data types are provided, the UIActivityViewController's "Save to Files" option results in several files being saved - one for each of the item types that can be saved as a file.

If I reduce it to just one UIActivityItem, then copy/paste functionality is severely reduced so that it would not work with all of the various different pasteboard types it should (eg, my app's custom JSON data format AND with plain text AND attributed string).

So I'm attempting to use a UIActivityItemProvider subclass to overcome the issue, but I still cannot figure out how to get only one file to be saved, instead of multiple files (one for each item type). Is this even possible?

Relevant parts of my UIActivityItemProvider subclass are below.

(Note that I'm using multiple instances of a single subclass for this, but the same problem would occur with using different subclasses.)

class RatsActivityItemProvider: UIActivityItemProvider {

    var rats: [Rat]

    init(placeholderItem: Any, rats: [Rat]) {
        RatsActivityItemProvider.selectedOption = nil
        self.rats = rats
        super.init(placeholderItem: placeholderItem)
    }

    class func allProviders(forRats rats: [Rat]) -> [RatsActivityItemProvider] {
        var providers: [RatsActivityItemProvider] = []
        providers.append(RatsActivityItemProvider(placeholderItem: NSAttributedString(), rats: rats))
        providers.append(RatsActivityItemProvider(placeholderItem: RatPrintPageRenderer(), rats: rats))
        providers.append(RatsActivityItemProvider(placeholderItem: [:] as [String:Any], rats: rats))
        providers.append(RatsActivityItemProvider(placeholderItem: Data(), rats: rats))
        return providers
    }

    override var item: Any {
        print("\(activityType!.rawValue as Any) - \(type(of: placeholderItem!))")
        switch activityType {
        case UIActivity.ActivityType.print:
            return RatPrintPageRenderer(rats)
        case UIActivity.ActivityType.copyToPasteboard:
            var pasteboardDict: [String:Any] = attrString.pasteables()
            //  (Add custom types to dictionary here)
            return pasteboardDict
        default:
            //  "Save To Files" activity is not available in UIActivity.ActivityType so check the raw value instead
            if activityType?.rawValue.contains("com.apple.CloudDocsUI.AddToiCloudDrive") ?? false {
                //
                //  HOW TO HAVE ONLY ONE OF THE PROVIDERS RETURN A VALUE HERE???
                //
            }
        }
    }

}

When I run this and choose "Save to Files" I get the following output (one line from each of the providers):

com.apple.CloudDocsUI.AddToiCloudDrive - NSConcreteAttributedString
com.apple.CloudDocsUI.AddToiCloudDrive - RecipePrintPageRenderer
com.apple.CloudDocsUI.AddToiCloudDrive - __EmptyDictionarySingleton
com.apple.CloudDocsUI.AddToiCloudDrive - _NSZeroData

...and a file gets created for each of these, if I simply pass back the item for that data type.

1

There are 1 answers

0
Son of a Beach On BEST ANSWER

Well, I've found that the solution was two-fold...

Direct Answer To Question (but not ideal behaviour):

In each of the switch cases (and the if within the default case), I needed to check that the activityType matches the placeholderItem. If it is not a suitable match, then return the (empty) placeholderItem as-is (except that in the "Save to Files" case, even the empty print page renderer placeholder resulted in a file being written! so instead of returning the placeholder there, return an empty array).

This worked, and resulted in a single file being written to a location of the users choice, which answers the original question. The user even gets the chance to provide a name for the file. But the default name is not good at all - just the data type (eg, "text" or "data", depending on what was being saved to the file).

Solution that Gives More Flexibility: Create a custom UIActivity that will write the file to a location chosen by the user using a UIDocumentPickerViewController. Eg, The activity can have a title like, "Export to ").

This proved to be a lot more flexible, and let me use a default file name (and extension!) that made a lot more sense, based on the data being passed in. It also has the potential for me to add further improvements to behaviour later (eg, I might be able to use an alert to get the user to choose between a couple of different file formats (to use for different purposes).

This does not replace the "Save to Files" action, so I end up with both of them, and still needed to fix my "Save to Files" action's behaviour as described above.

It leaves me with both an "Export" and a "Save" action, which work similarly, but one is more flexible and intuitive than the other, and I can make them use different file formats by default.

My new code (for the default part of the outer switch) is below...

        default:
            if activityType?.rawValue == "com.stuff.thing.activity.export" {
                if placeholderItem is [Rat] {
                    return rats
                } else {
                   return placeholderItem!
               }
            } else if activityType?.rawValue == "com.apple.CloudDocsUI.AddToiCloudDrive" {
                if placeholderItem is Data {
                    return attrString
                } else {
                    //  Don't return the placeholder item here!  Some (eg print page renderer) can result in a file being written!
                    return [] as [Any]
                }
            }

            return attrString
        }