Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

File Provider iOS11 startProvidingItem not invoked

I'm implementing a File Provider Extension for iOS 11.

Dispite watching the conference at https://developer.apple.com/videos/play/wwdc2017/243/ and navigating through Apple's Documentation, I still can't seem to understand how to implement some of the methods for NSFileProviderExtension and NSFileProviderEnumerator objects.

I successfully implemented NSFileProviderItem, having all of them listed in the Navite iOS 11 Files App. However, I can't trigger any document based app to open upon selecting a file.

I overrided all the methods for the NSFileProviderExtension. Some are still empty, but I placed a breakpoint to check whenever they are called.

The NSFileProviderExtension looks something like this:

class FileProviderExtension: NSFileProviderExtension {
    var db : [FileProviderItem]  = [] //Used "as" a database
...

    override func item(for identifier: NSFileProviderItemIdentifier) throws -> NSFileProviderItem {
        for i in db {
            if i.itemIdentifier.rawValue == identifier.rawValue {
                return i
            }
        }
        throw NSError(domain: NSCocoaErrorDomain, code: NSNotFound, userInfo:[:])
    }

    override func urlForItem(withPersistentIdentifier identifier: NSFileProviderItemIdentifier) -> URL? {
        guard let item = try? item(for: identifier) else {
            return nil
        }

        // in this implementation, all paths are structured as <base storage directory>/<item identifier>/<item file name>
        let manager = NSFileProviderManager.default
        let perItemDirectory = manager.documentStorageURL.appendingPathComponent(identifier.rawValue, isDirectory: true)

        return perItemDirectory.appendingPathComponent(item.filename, isDirectory:false)
    }

    // MARK: - Enumeration
    func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {
        var maybeEnumerator: NSFileProviderEnumerator? = nil

        if (containerItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
            maybeEnumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
            self.db = CustomData.getData(pid: containerItemIdentifier)

        } else if (containerItemIdentifier == NSFileProviderItemIdentifier.workingSet) {
            // TODO: instantiate an enumerator for the working set
        } else {

        }
        guard let enumerator = maybeEnumerator else {
            throw NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])
        }
        return enumerator
    }

My enumerateItems looks something like so:

class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {

    override func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) {

        let itens = CustomData.getData(pid: enumeratedItemIdentifier)
        observer.didEnumerate(itens)
        observer.finishEnumerating(upTo: nil)

    }

The static function CustomData.getData is used for testing. It returns an array of NSFileProviderItem with the desired properties. It should be replaced with a database, as explained in the conference.

class CustomData {


    static func getData(pid : NSFileProviderItemIdentifier) -> [FileProviderItem] {
        return [
            FileProviderItem(uid: "0", pid: pid, name: "garden", remoteUrl : "https://img2.10bestmedia.com/Images/Photos/338373/GettyImages-516844708_54_990x660.jpg"),
            FileProviderItem(uid: "1", pid: pid, name: "car", remoteUrl : "https://static.pexels.com/photos/170811/pexels-photo-170811.jpeg"),
            FileProviderItem(uid: "2", pid: pid, name: "cat", remoteUrl : "http://www.petmd.com/sites/default/files/what-does-it-mean-when-cat-wags-tail.jpg"),
            FileProviderItem(uid: "3", pid: pid, name: "computer", remoteUrl : "http://mrslamarche.com/wp-content/uploads/2016/08/dell-xps-laptop-620.jpg")
        ]
    }

}

The problem is, when the user presses a document, urlForItem is successfully called but nothing happens upon returning the item url.

What am I doing wrong? I can't find any examples on the internet.

Cheers

-nls

like image 347
nfls Avatar asked Oct 11 '17 09:10

nfls


2 Answers

Turns out, I did not correctly implement providePlaceholder(at url:).

It is now solved.

Cheers

-nls

EDIT:

In order to list the items in your file provider, the method enumerator(for:) should be implemented. This method will receive a containerItemIdentifier, as if telling you "what folder the user is trying to access". It returns a NSFileProviderEnumerator object, that should also be implemented by you.

Here is an example of how a simple enumerator(for:) method should look like:

class FileProviderExtension: NSFileProviderExtension {

    override func enumerator(for containerItemIdentifier: NSFileProviderItemIdentifier) throws -> NSFileProviderEnumerator {

        var enumerator: NSFileProviderEnumerator? = nil

        if (containerItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {
            enumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
        }
        else {
            enumerator = FileProviderEnumerator(enumeratedItemIdentifier: containerItemIdentifier)
        }
        if enumerator == nill {
            throw NSError(domain: NSCocoaErrorDomain, code: NSFeatureUnsupportedError, userInfo:[:])
        }
        return enumerator
    }

    (...)

}

Again, as I said, the FileProviderEnumerator should be implemented by you. The important method here is the enumerateItems(for observer:, startingAt page:)

Here it is how it should look:

class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {

    func enumerateItems(for observer: NSFileProviderEnumerationObserver, startingAt page: NSFileProviderPage) { 

        if (enumeratedItemIdentifier == NSFileProviderItemIdentifier.rootContainer) {

            //Creating an example of a folder item
            let folderItem = FileProviderFolder()
            folderItem.parentItemIdentifier = enumeratedItemIdentifier  //<-- Very important
            folderItem.typeIdentifier = "public.folder"
            folderItem.name = "ExampleFolder"
            folderItem.id = "ExampleFolderID"

            //Creating an example of a file item
            let fileItem = FileProviderFile()
            fileItem.parentItemIdentifier = enumeratedItemIdentifier    //<-- Very important
            fileItem.typeIdentifier = "public.plain-text"
            fileItem.name = "ExampleFile.txt"
            fileItem.id = "ExampleFileID"


            self.itemList.append(contentsOf: [folderItem, fileItem])
            observer.didEnumerate(self.itemList)
            observer.finishEnumerating(upTo: nil)

        }
        else {
            //1 > Find directory name using "enumeratedItemIdentifier" property
            //2 > Fetch data from the desired directory
            //3 > Create File or Folder Items 
            //4 > Send items back using didEnumerate and finishEnumerating
        }
    }

    (...)

}

Remember that we were creating these FileProviderEnumerators, giving them the containerItemIdentifier. This property is used to determine what folder the user is trying to access.

Very important note: Each item, File or Folder, should have its parentItemIdentifier property defined. If this property is not set, the items won't appear when the user tries to open the parent folder. Also, as the name suggests, typeIdentifier will hold the Uniform Type Identifier (UTI) for the item.

Finally, the last object we should implement is the NSFileProviderItem. Both File and Folder items are very similar, and should differ in their typeIdentifier property. Here is a very simple example of a folder:

class FileProviderFolder: NSObject, NSFileProviderItem {

    public var id: String?
    public var name: String?

    var parentItemIdentifier: NSFileProviderItemIdentifier
    var typeIdentifier: String

    init() {

    }

    var itemIdentifier: NSFileProviderItemIdentifier {
        return NSFileProviderItemIdentifier(self.id!)
    }

    var filename: String {
        return self.name!
    }
}

The itemIdentifier is very important because, as stated before, this property will provide the directory name for the folder item when trying to enumerate its contents (refer to enumerator(for:) method).

EDIT2

If the user selects a file, the method startProvidingItem(at url:) should be called. This method should perform 3 tasks:

1 - Find the selected item ID (usualy using the provided url, but you can use a database too)

2 - Download the file to the local device, making it available at the specified url. Alamofire does this;

3 - Call completionHandler;

Here is a simple example of this method:

class FileProviderExtension: NSFileProviderExtension {


    override func urlForItem(withPersistentIdentifier identifier: NSFileProviderItemIdentifier) -> URL? {
        // resolve the given identifier to a file on disk
        guard let item = try? item(for: identifier) else {
            return nil
        }
        // in this implementation, all paths are structured as <base storage directory>/<item identifier>/<item file name>
        let perItemDirectory = NSFileProviderManager.default.documentStorageURL.appendingPathComponent(identifier.rawValue, isDirectory: true)
        let allDir = perItemDirectory.appendingPathComponent(item.filename, isDirectory:false)
        return allDir
    }

    override func persistentIdentifierForItem(at url: URL) -> NSFileProviderItemIdentifier? { 
        // exploit that the path structure has been defined as <base storage directory>/<item identifier>/<item file name>, at urlForItem
        let pathComponents = url.pathComponents
        assert(pathComponents.count > 2)
        return NSFileProviderItemIdentifier(pathComponents[pathComponents.count - 2])
    }

    override func startProvidingItem(at url: URL, completionHandler: @escaping (Error?) -> Void) {

        guard
            let itemID = persistentIdentifierForItem(at: url),
            let item = try? self.item(for: itemID) as! FileProviderFile else {
                return
        }

        DownloadfileAsync(
            file: item,
            toLocalDirectory: url,
            success: { (response) in

                // Do necessary processing on the FileProviderFile object
                // Example: setting isOffline flag to True

                completionHandler(nil)
            },
            fail: { (response) in
                completionHandler(NSFileProviderError(.serverUnreachable))
            }
        )

    }

    (...)
}

Note that, to get the ID from the URL, I'm using the recomended method: the URL it self contains the item ID.

This URL is definedin the urlForItem method.

Hope this helps.

-nls

like image 50
nfls Avatar answered Nov 16 '22 03:11

nfls


I thought I'd provide a followup answer, the primary answer is great as a first step. In my case startProvidingItem was not called because I was not storing the files in exactly the directory the system was looking for, that is to say:

<Your container path>/File Provider Storage/<itemIdentifier>/My Awesome Image.png

That is on the slide from WWDC17 on the FileProvider extension, but I did not think it must follow that format so exactly.

I had a directory not named "File Provider Storage" into which I was putting files directly, and startProvidingItem was never called. It was only when I made a directory for the uniqueFileID into which the file was placed, AND renamed my overall storage directory to "File Provider Storage" that startProvidingItem was called.

Also note that with iOS11, you'll need to provide a providePlaceholder call as well to the FileProviderExtension, use EXACTLY the code that is in the docs for that and do not deviate unless you are sure of what you are doing.

like image 38
Kendall Helmstetter Gelner Avatar answered Nov 16 '22 02:11

Kendall Helmstetter Gelner