• Tutorial on iOS File Provider Extension
  • By Ryan Ackermann
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: iWeslie
  • Proofread by: Swants

In this tutorial, you will learn the File Provider extension and how to use it to make your App’s content public.

Introduced in iOS 11, the File Provider uses the iOS File App to access content managed by your App. At the same time, other App can use UIDocumentBrowserViewController or UIDocumentPickerViewController to access your App’s data.

The main tasks of File Provider expansion are:

  • Create placeholder files that represent cloud content.
  • Download or upload the file first when the App accesses the file content.
  • A notification is issued after a file is updated to upload the update to the server.
  • Enumerates stored files and directories.
  • Perform operations on documents, such as renaming, moving, or deleting.

You will use the Heroku button to configure the server to host the file. After the server is set up, you need to configure the extension to enumerate the contents of the server.

start

First, download the resource, then go to the Favart-Starter folder and open favart.xcodeProj. Make sure you have selected Favart’s scheme, then compile and run the App and you should see the following:

The App provides a basic View to tell the user how to enable the File Provider extension, since you don’t actually do anything in the App. Each time you compile and run the App in this tutorial, you will return to the home screen and open the file App to access your extension.

Note: If you want to run this project on a real machine, in addition to setting developer information for the two targets, you also need to edit favart.xcconfig in the Configuration folder. Update the Bundle ID to a unique value.

Example project uses this value for PRODUCT_BUNDLE_IDENTIFIER in build setting in two targets, Provider. Entitlements App Groups identifier, And the Info. The NSExtensionFileProviderDocumentGroup plist. If you don’t update them synchronously in your project, you’ll get blurry and undebuggable build error messages, and using custom build Settings is a smart way to do it.

The sample project already contains the basic components you will use for the File Provider extension:

  • Networkclient. swift contains the network request client used to communicate with the Heroku server.
  • FileProviderExtension. Swift is the File Provider to expand itself.
  • FileProviderEnumerator. Swift, contains the enumerator, used for enumeration directory content.
  • Models are a set of Models needed to complete the extension.

Set up the back end using Heroku

First, you need an instance of your own back-end server. Fortunately, this is easy to do using the Heroku Button. Click the button below to access Heroku’s dashboard.

After you sign up for Heroku’s free account, you will see the following page:

On this page, you can either give your App a name or leave the field blank, and Heroku will automatically generate a name for you. Without having to configure anything else, you can now click the Deploy App button and after a while your back end will be up and running.

After Heroku has finished deploying the App, click View at the bottom. This jumps to the backend URL of your hosted instance. In the root directory, you should see a JSON piece of data that is familiar to you as Hello World! .

Finally, you need to copy the URL of the Heroku instance, but only the domain part of it: {app-name}.herokuapp.com.

In the starter project, open the Provider/NetworkClient. Swift. At the top of the file, you should see a warning telling you to Add your Heroku URL here. Delete this warning and replace the components. Host placeholder string with your URL.

You are now complete with the server configuration. Next, you will define the model on which the File Provider depends.

Define NSFileProviderItem

First, the FileProvider needs a model that follows the NSFileProviderItem protocol. This model provides information about the files managed by the File Provider. The Starter project already defines the FileProviderItem in FileProviderItem.swift, and there are several protocols to follow before using it.

Although the protocol has 27 attributes, we only need four of them. Other optional properties provide the File Provider with detailed information about each File and other capabilities. In this tutorial, you will use four attributes: itemIdentifier, parentItemIdentifier, filename, and typeIdentifier.

ItemIdentifier provides a unique identifier for the model. The File Provider uses the parentIdentifier to track its position in the extended hierarchy.

Filename is the App name displayed in the file. TypeIdentifier is a unified typeIdentifier (UTI).

Before FileProviderItem can follow the NSFileProviderItem protocol, it also needs a way to process data from the back end. MediaItem defines a simple model of back-end data. Instead of using this model directly in FileProviderItem, we use the MediaItemReference to handle some additional logic for the FileProvider extension to fill in the holes.

You will use MediaItemReference in this tutorial for two reasons:

  1. The backend hosted on Heroku is too compact to provideNSFileProviderItemAll the information you need, so you need to get it somewhere else.
  2. The File Provider extension is also simple. A more complete File Provider extension would use something like Core Data to locally persist the Data returned by the back end so that it can reference it at the end of the extension’s life.

To focus the tutorial on the File Provider extension itself, you’ll use the MediaItemReference for a quick start. You’ll need to embed four required fields into the URL object. Then the URL encoded into NSFilProviderItemIdentifier. You don’t need to store anything else manually because NSFileProviderExtension handles it for you.

Open the Provider/MediaItemReference. Swift and add the following code to MediaItemReference:

/ / 1
private let urlRepresentation: URL

/ / 2
private var isRoot: Bool {
    return urlRepresentation.path == "/"
}

/ / 3
private init(urlRepresentation: URL) {
    self.urlRepresentation = urlRepresentation
}

/ / 4
init(path: String, filename: String) {
    let isDirectory = filename.components(separatedBy: ".").count= =1
    let pathComponents = path.components(separatedBy: "/").filter { !$0.isEmpty } + [filename]

    var absolutePath = "/" + pathComponents.joined(separator: "/")
    if isDirectory {
        absolutePath.append("/")
    }
    absolutePath = absolutePath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? absolutePath

    self.init(urlRepresentation: URL(string: "itemReference://\(absolutePath)")! }Copy the code

Here’s a breakdown of the code:

  1. In this tutorial, the URL will containNSFileProviderItemMost of the information you need.
  2. This computed property determines whether the current entry is the root directory of the file system.
  3. You make this initialization method private to prevent calls from outside the model.
  4. You call this initialization method when reading data from the back end. If the file name does not contain a file suffix, it must be a folder, because the initialization method does not automatically infer its type.

Before adding the final initial initializer, replace the import statement at the top of the file with:

import FileProvider
Copy the code

Next add the following initializers below the code:

init? (itemIdentifier:NSFileProviderItemIdentifier) {
    guarditemIdentifier ! = .rootContainerelse {
        self.init(urlRepresentation: URL(string: "itemReference:///")!return
    }

    guard let data = Data(base64Encoded: itemIdentifier.rawValue),
        let url = URL(dataRepresentation: data, relativeTo: nil)
    else {
        return nil
    }

    self.init(urlRepresentation: url)
}
Copy the code

Most extensions will use this initializer. Note the itemReference at the beginning ://. You can handle the root identifier separately to ensure that the path to its URL is set correctly.

For other items, you can convert the original value of the identifier to Base64 encoded data to retrieve the URL. The information in the URL comes from the network request that enumerates the instance for the first time.

Now that the initializer is set up, it’s time to add some properties to the model. First, add the following import at the top of the file:

import MobileCoreServices
Copy the code

This will allow you to access the file type, adding further to the structure:

/ / 1
var itemIdentifier: NSFileProviderItemIdentifier {
    if isRoot {
        return .rootContainer
    } else {
        return NSFileProviderItemIdentifier(rawValue: urlRepresentation.dataRepresentation.base64EncodedString())
    }
}

var isDirectory: Bool {
    return urlRepresentation.hasDirectoryPath
}

var path: String {
    return urlRepresentation.path
}

var containingDirectory: String {
    return urlRepresentation.deletingLastPathComponent().path
}

var filename: String {
    return urlRepresentation.lastPathComponent
}

/ / 2
var typeIdentifier: String {
    guard! isDirectoryelse {
        return kUTTypeFolder as String
    }

    let pathExtension = urlRepresentation.pathExtension
    let unmanaged = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString.nil)
    letretained = unmanaged? .takeRetainedValue()return (retained as String?). ??""
}

/ / 3
var parentReference: MediaItemReference? {
    guard! isRootelse {
        return nil
    }
    return MediaItemReference(urlRepresentation: urlRepresentation.deletingLastPathComponent())
}
Copy the code

Here’s what you need to know to keep in mind:

  1. For each item managed by the FileProvider,itemIdentifierIt has to be unique. If it is the root directory, then it usesNSFileProviderItemIdentifier.rootContainerOtherwise, an identifier is created from the URL.
  2. Here it creates an identifier based on the URL of the extension path, which looks strangeUTTypeCreatePreferredIdentifierForTagIs actually a C function of type UTI that returns the given input.
  3. Parent references are useful when dealing with directory structures. This property represents the folder containing the current reference. It is an optional type because the root directory has no parent.

You’ve added some other properties here that don’t need much explanation, but are very useful in creating NSFileProviderItem. Now that the reference model is created, it’s time to hook everything up to the FileProviderItem.

Open FileProviderItem.swift and add at the top:

import FileProvider
Copy the code

Then at the very bottom of the file add:

// MARK: - NSFileProviderItem

extension FileProviderItem: NSFileProviderItem {
    / / 1
    var itemIdentifier: NSFileProviderItemIdentifier {
        return reference.itemIdentifier
    }

    var parentItemIdentifier: NSFileProviderItemIdentifier {
        returnreference.parentReference? .itemIdentifier ?? itemIdentifier }var filename: String {
        return reference.filename
    }

    var typeIdentifier: String {
        return reference.typeIdentifier
    }

    / / 2
    var capabilities: NSFileProviderItemCapabilities {
        if reference.isDirectory {
            return [.allowsReading, .allowsContentEnumerating]
        } else {
            return [.allowsReading]
        }
    }

    / / 3
    var documentSize: NSNumber? {
        return nil}}Copy the code

FileProviderItem now follows NSFileProviderItem and implements all the required properties. The above code is explained as follows:

  1. Most of the required attributes are mapped to the one you previously added toMediaItemReferenceThe logic.
  2. NSFileProviderItemCapabilitiesRepresents what actions, such as read and delete, can be performed on items in the document browser. For this App, you only need to allow reading and enumerating directories. In a real project, you might use.allowsAllBecause the user expects everything to work.
  3. The document size will not be used in this tutorial; include it to preventNSFileProviderManager.writePlaceholder(at:withMetadata:)Will collapse. This might be a bug in the framework, but in general App file extensions will be provided anyway, rightdocumentSize.

So that’s the model, NSFileProviderItem has a lot more properties, but what you’ve implemented so far is enough.

Enumeration file

Now the model is complete and ready for use. You have to tell the system what’s in your App in order to show the user the item defined by the model.

NSFileProviderEnumerator defines the relationship between the system and App content. You will see later how the system by providing said the current context of NSFileProviderItemIdentifier to request the enumerator. If the user is currently in the root directory, the.rootContainer identifier will be provided. In other directories, the system passes in the identifier of the item defined by your model.

First, build the enumerator in the starter. Open the Provider/FileProviderEnumerator. Swift and add under the path:

private var currentTask: URLSessionTask?
Copy the code

This property stores a reference to the current network request task. This allows you to cancel the request at any time.

Replace the contents of enumerateItems(for:startingAt:) with:

let task = NetworkClient.shared.getMediaItems(atPath: path) { results, error in
    guard let results = results else {
        let error = error ?? FileProviderError.noContentFromServer
        observer.finishEnumeratingWithError(error)
        return
    }

    let items = results.map { mediaItem -> FileProviderItem in
        let ref = MediaItemReference(path: self.path, filename: mediaItem.name)
        return FileProviderItem(reference: ref)
    }

    observer.didEnumerate(items)
    observer.finishEnumerating(upTo: nil)
}

currentTask = task
Copy the code

NetworkClient singleton is implemented to obtain the contents of the specified path. After a successful request, the enumerator’s observer returns new data by calling didEnumerate and finishEnumerating(upTo:). To notify the enumerator observer by finishEnumeratingWithError request to the results if there is a mistake.

Note: A real App might use paging to fetch data, which would use NSFileProviderPage to do this. The App will first use the integer as the page index, and then serialize and store it in the NSFileProviderPage structure.

Finally, you complete the enumerator by adding the following to invalidate() :

currentTask? .cancel() currentTask =nil
Copy the code

If necessary, the current network request will be cancelled because there may be cases where access to the user’s network state or current location may be required, as well as some resource usage.

Once you’re done with this method, you can use the enumerator to access the data from the back-end server, which then brings you to the FileProviderExtension class.

Open the Provider/FileProviderExtension. Swift and place the item (for) replaced code:

guard let reference = MediaItemReference(itemIdentifier: identifier) else {
    throw NSError.fileProviderErrorForNonExistentItem(withIdentifier: identifier)
}
return FileProviderItem(reference: reference)
Copy the code

The identifier argument is provided, and you need to return a FileProviderItem to that Identifier. This guard statement ensures that the created MediaItemReference is valid.

Next, put the urlForItem (withPersistentIdentifier:) and persistentIdentifierForItem (ats) replaced with the following contents:

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

    return NSFileProviderManager.default.documentStorageURL
      .appendingPathComponent(identifier.rawValue, isDirectory: true)
      .appendingPathComponent(item.filename)
}

/ / 2
override func persistentIdentifierForItem(at url: URL) -> NSFileProviderItemIdentifier? {
    let identifier = url.deletingLastPathComponent().lastPathComponent
    return NSFileProviderItemIdentifier(identifier)
}
Copy the code

Here is the code:

  1. Verify that a given identifier can be resolved as an instance of the extended model. It then returns a file URL, which is the location to store the project in the file manager.
  2. byurlForItem(withPersistentIdentifier:)Each URL returned needs to be mapped back to the original settingNSFileProviderItemIdentifier. In this method, you take the<documentStorageURL>/<itemIdentifier>/<filename>Build the URL and adopt the format<itemIdentifier>As an identifier.

There are now two methods that require you to pass in a placeholder URL to a remote file. First you’ll create a helper method to do this, adding the following to providePlaceholder(at:) :

/ / 1
guard let identifier = persistentIdentifierForItem(at: url),
    let reference = MediaItemReference(itemIdentifier: identifier)
else {
    throw FileProviderError.unableToFindMetadataForPlaceholder
}

/ / 2
try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil)

/ / 3
let placeholderURL = NSFileProviderManager.placeholderURL(for: url)
let item = FileProviderItem(reference: reference)

/ / 4
try NSFileProviderManager.writePlaceholder(at: placeholderURL, withMetadata: item)
Copy the code

The above code does the following:

  1. First you create the identifier and reference from the URL provided. An error is thrown if it fails.
  2. When creating placeholders, you must ensure that the directory exists, otherwise you will run into problems usingNSFileManagerTo perform this operation.
  3. thisurlParameters are used to display images, not placeholders. So you have to useplaceholderURL(for:)To create a placeholder URL and get what this placeholder will representNSFileProviderItem.
  4. Writes the placeholder to the file system.

Next replace the contents of providePlaceholder(at:completionHandler:) with:

do {
    try providePlaceholder(at: url)
    completionHandler(nil)}catch {
    completionHandler(error)
}
Copy the code

When the File Provider needs a placeholder URL, it calls providePlaceholder(at:completionHandler:). You will try to create a placeholder using the helper method above and pass it to the completionHandler if an error is thrown. Just like in providePlaceholder(at:), after this step succeeds, nothing needs to be passed, and the File Provider just needs your placeholder URL.

When the user switches between directories, the FileProvider calls enumerator(for:) to request the FileProviderEnumerator for the given identifier. Replace the contents of the act with the following:

if containerItemIdentifier == .rootContainer {
    return FileProviderEnumerator(path: "/")}guard let ref = MediaItemReference(itemIdentifier: containerItemIdentifier), ref.isDirectory
else {
    throw FileProviderError.notAContainer
}

return FileProviderEnumerator(path: ref.path)
Copy the code

This method ensures that a given identifier corresponds to a directory. If it is the root directory, the enumerator is still created because the root directory is also a valid directory.

Compile and run, after the App starts, open the file App, click browse in the lower right corner twice, and you will enter the root directory of the file. Select more locations and providers will appear or expand a list and click to open your App expansion.

Note: if you can’t find more places to expand the project and can’t click, you can click the Edit button in the upper right corner again.

You now have a valid File Provider extension, but there are some important things missing, which you will add next.

Provide thumbnails

Since the App will display the images requested from the back end, it is important to display a thumbnail of the image. You can override a method to generate the thumbnail.

Add to enumerator(for:) :

// MARK: - Thumbnails

override func fetchThumbnails(for itemIdentifiers: [NSFileProviderItemIdentifier], requestedSize size: CGSize,
                              perThumbnailCompletionHandler: @escaping (NSFileProviderItemIdentifier, Data? , Error?) -> Void,
                              completionHandler: @escaping (Error?). ->Void) - >Progress {
    / / 1
    let progress = Progress(totalUnitCount: Int64(itemIdentifiers.count))

    for itemIdentifier in itemIdentifiers {
        / / 2
        let itemCompletion: (Data? .Error?). ->Void = { data, error in
            perThumbnailCompletionHandler(itemIdentifier, data, error)

            if progress.isFinished {
                DispatchQueue.main.async {
                    completionHandler(nil)}}}guard let reference = MediaItemReference(itemIdentifier: itemIdentifier), ! reference.isDirectoryelse {
            progress.completedUnitCount += 1

            let error = NSError.fileProviderErrorForNonExistentItem(withIdentifier: itemIdentifier)
            itemCompletion(nil, error)
            continue
        }

        let name = reference.filename
        let path = reference.containingDirectory

        / / 3
        let task = NetworkClient.shared.downloadMediaItem(named: name, at: path) { url, error in
            guard let url = url, let data = try? Data(contentsOf: url, options: .alwaysMapped) else {
                itemCompletion(nil, error)
                return
            }
            itemCompletion(data, nil)}/ / 4
        progress.addChild(task.progress, withPendingUnitCount: 1)}return progress
}
Copy the code

While this approach is very verbose, the logic is simple:

  1. This method returns oneProgressObject that records the status of each thumbnail request.
  2. It is for eachitemIdentifierDefines a Completion closure that takes care of the closure for each item needed to invoke the method and the last closure that will be invoked.
  3. Use the starter packageNetworkClientDownload the thumbnail file from the server to a temporary file. After the download is complete, the Completion Handler will download thedataPassed to theitemCompletionClosure.
  4. Each download task is added as a dependency to the parent process object.

Note: When dealing with large amounts of data, it may take some time to make separate network requests for each placeholder. So if possible, your back end should provide a way to bulk download images in a single request.

Compile and run. Open the extension in the file to see your thumbnail:

Show full picture

Now when you select an item, the App will display a blank view without the full image:

So far, you’ve only implemented the preview thumbnail display, you still need to add the full image display.

As with thumbnail generation, only one method is required to render the complete image, startProvidingItem(at:completionHandler:). Add the following to the bottom of the FileProviderExtension class:

// MARK: - Providing Items

override func startProvidingItem(at url: URL, completionHandler: @escaping ((_ error: Error?) -> Void)) {
    / / 1
    guard! fileManager.fileExists(atPath: url.path)else {
        completionHandler(nil)
        return
    }

    / / 2
    guard let identifier = persistentIdentifierForItem(at: url), let reference = MediaItemReference(itemIdentifier: identifier) else {
        completionHandler(FileProviderError.unableToFindMetadataForItem)
        return
    }

    / / 3
    let name = reference.filename
    let path = reference.containingDirectory
    NetworkClient.shared.downloadMediaItem(named: name, at: path, isPreview: false) { fileURL, error in
        / / 4
        guard let fileURL = fileURL else {
            completionHandler(error)
            return
        }

        / / 5
        do {
            try self.fileManager.moveItem(at: fileURL, to: url)
            completionHandler(nil)}catch {
            completionHandler(error)
        }
    }
}
Copy the code

The above code functions are:

  1. Checks whether an entry already exists in the specified URL to prevent the same data from being requested again. In real projects, you should check the modification date and file version number to make sure you are getting the latest data. However, this is not necessary in this tutorial because it does not support version control.
  2. Access to relevantURLMediaItemReferenceTo determine which file needs to be requested from the back end.
  3. Extract the file name and path from Reference and make the request.
  4. If an error occurs while downloading the file, the error is passed to the error handling closure.
  5. Moves a file from its temporary download directory to the document store URL specified by the extension.

Compile and run, open the extension, select any image, and you can see the full image.

When you open more files, the extension needs to delete files that have already been downloaded. The File Provider extension has this functionality built in.

You must override stopProvidingItem(at:) to clean up downloaded files and provide new placeholders. Add the following at the bottom of the FileProviderExtension class:

override func stopProvidingItem(at url: URL) {
    try? fileManager.removeItem(at: url)
    try? providePlaceholder(at: url)
}
Copy the code

This deletes the image and calls providePlaceholder(at:) to generate a new placeholder.

This completes the most basic functions of the File Provider. File enumerations, thumbnail previews, and viewing file contents are the basic components of this extension.

By now, your File Provider is fully functional.

What’s next?

You now have an App that contains a valid File Provider, an extension that enumerates and displays backend server stuff.

You can download the full version of the project by clicking on Download Resources.

You can learn more about File providers in Apple’s documentation on File Providers. You can also use other extensions to add custom UIs to File providers, which you can read more about here.

If you’re interested in other ways you can view using files on iOS, check out the document-based App.

Hope you enjoyed this tutorial! If you have any questions or comments, you can join the discussion group at the bottom of this article.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.