Several treasures like not so well-known frameworks and libraries can be found in the iOS SDK. Most of them can be proved to be real gems and truly useful tools, saving you from hours of extra work. One of them is the Quick Look Framework, and even if you’ve never used it before, it’s easy to understand what’s all about just from its name; it provides previewing capabilities for documents that an app handles. Like most people, I wasn’t aware of it until I had to use it for first time; then I appreciated its existence.
The Quick Look Framework can be used quite easily, and it can open specific types of documents for previewing. Those types are:
- iWork documents (Pages, Numbers and Keynote)
- Microsoft Office documents (as long as they’ve been created with Office 97 or any other newer version)
- PDF files
- Images
- Text files
- Rich-Text Format documents
- Comma-Separated Value files (csv)
As you might be thinking right now, the Quick Look framework can become really handy if your app is dealing with files of any of the above file types and you want your users to be able to preview their content. Except for that, the Quick Look framework provides sharing options as well. An activity controller (UIActivityViewController
) can be presented for sending or sharing the previewed document.
Becoming a bit more specific and technical now, the developer’s main task when using the Quick Look framework is to provide a datasource
that the preview controller will use in order to eventually open the proper document(s) for previewing. This datasource is actually a list of NSURL
objects specifying the path to each document, and apparently it can be either a locally stored, or a remote document. Locally stored files include those that reside either in the documents directory, or in the bundle of the app.
The Quick Look framework provides a view controller named QLPreviewController
(Quick Look Preview Controller) for quick looking a document. This view controller can be presented to the app modally, or to be pushed to the navigation stack if the app is navigation-based. It’s the main component of the framework, and once it’s presented it provides the sharing options, as well as the possibility to switch to another document without having to dismiss it by displaying a list with the whole set of available documents. Additionally, the framework requires the implementation of two datasource methods that belong to the QLPreviewControllerDataSource
protocol in order for the preview to properly work. Besides that, there’s also the QLPreviewControllerDelegate
protocol as well, which you can optionally
implement and gain that way more control over the functionalities of the Quick Look framework.
We’ll have the chance to discuss all the above in details, and as we usually do, we’ll use a demo app for this purpose. However, before we go to the insights of the Quick Look framework, let’s have a quick tour to what our demo app is all about.
About the Demo App
For the demonstration needs of this tutorial we’ll use a navigation-based app with one view controller only. This view controller (named FileListViewController
) will present a list of files (in a tableview) that are stored in the application’s bundle. We won’t use all the possible supported document types; instead we’ll only use a subset of them, but still, we’ll be able to properly see how the Quick Look framework works.
When a row matching to a file is tapped to the tableview, the Quick Look Preview Controller will open the respective document for preview. The preview controller will be pushed to the navigation stack, but I’ll show you how to present it modally as well (actually, it’s the same way you present any other view controller modally). Furthermore, we’ll have the chance to see the extra functionalities provided by the preview controller (like sharing and switching document) once we push it for first time, and we’ll discuss about some of the optional delegate methods.
The following screenshot present the initial view controller (the FileListViewController
) with the sample files presented on it. As you see, the actual file name and the type of the document are actually presented to each row (we’ll go through the respective implementation).
When a document gets opened for previewing will look like the next image:
You can download a starter project to work with while reading the rest of this text. Once you get it, just take a look to the existing code, and then we’ll complete it step by step as we’ll be moving through the next parts.
Files and File URLs
In the starter project you’ll find a collection of sample files that we’re going to preview in this demo app. Those files have been appended to the application’s bundle, but that’s not enough in order for our app to use them for previewing. It’s our responsibility (developer’s responsibility) to tell the app which files to use with the Quick Look framework.
Being more specific now, we’ll create an array to provide the file names of the sample files existing to the bundle. Those file names will be used for two purposes:
- In order to display the file name and description in our tableview properly.
- Most importantly, to create the list of
NSURL
objects that the Quick Look framework needs as a datasource, so its capable of retrieving and previewing each file.
It’s clear now how the name of each file will be used, so having said that it’s time for us to get our hands dirty. The first thing we need to to is to create an array with the list of file names (including their extensions of course), so make sure that you have the FileListViewController.swift
file in Xcode open. In the beginning of the respective class, add the next line:
let fileNames = ["AppCoda-PDF.pdf", "AppCoda-Pages.pages", "AppCoda-Word.docx", "AppCoda-Keynote.key", "AppCoda-Text.txt", "AppCoda-Image.jpeg"]
In our example we already know the names of the files we’ll use for previewing, so it’s easy to declare the fileNames
array and give values to it at the same time. However there might be cases in real apps that it’s impossible for you to have this information in advance (for example, if you’re downloading files). In such a case you’ll have to populate the above array with values dynamically and when you’re able to add one or more files to that collection.
Now let’s make the following declaration:
var fileURLs = [NSURL]()
This array is going to be the datasource for the Quick Look framework, but not only. We’ll also use it as the datasource for the tableview as well.
Let’s create now a new method, and let’s fill the above array with values. In that new method, we’ll get each file name specified in the fileNames
array and we’ll create a new NSURL object. Once we make sure that the real respective file is there, we’ll append the new NSURL object to the fileURLs
array, and our work in this part has been done.
func prepareFileURLs() { for file in fileNames { let fileParts = file.componentsSeparatedByString(".") if let fileURL = NSBundle.mainBundle().URLForResource(fileParts[0], withExtension: fileParts[1]) { if NSFileManager.defaultManager().fileExistsAtPath(fileURL.path!) { fileURLs.append(fileURL) } } } }
Notice that we’re using the componentsSeparatedByString(...)
method of the String
class to break each single file name into its parts, meaning the name and the extension. The rest is easy and straightforward: We create a NSURL
object by using the URLForResource(...)
method of the NSBundle
class, and once we ensure that the file described by the newly created NSURL object really exists, we append it to the fileURLs
array.
Jump now to the viewDidLoad()
and call the above method:
override func viewDidLoad() { ... prepareFileURLs() }
Displaying the Files
By having the datasource for the tableview prepared, we can continue to the display of the files that will be previewed, along with a short description about their types. Note that this part regards our demo app only and not the Quick Look framework in general, however I intentionally added this part to the post in case it gives you some ideas for your own projects.
What we really want to achieve here is the following:
Our goal is to start from the NSURL
objects describing the URL for each file, and by properly handling them to end up having the single file name and a description of the document type for every file. In order to manage that, we obviously need to get the NSURL
as a string object and break it
into its components. The last component will always be the full file name which we need so we break as well in order to get the actual name and the extension for each file.
What I described right above will come in life in a new method that we are about to write for that purpose exactly. Even though there’s nothing particularly difficult in the next code snippet, I’ve added comments before each line so it makes it easier for you to understand it. Notice that the method does not return a single value, but a tuple
. The first value of the tuple is the actual file name, and the second is the extension.
func extractAndBreakFilenameInComponents(fileURL: NSURL) -> (fileName: String, fileExtension: String) { // Break the NSURL path into its components and create a new array with those components. let fileURLParts = fileURL.path!.componentsSeparatedByString("/") // Get the file name from the last position of the array above. let fileName = fileURLParts.last // Break the file name into its components based on the period symbol ("."). let filenameParts = fileName?.componentsSeparatedByString(".") // Return a tuple. return (filenameParts![0], filenameParts![1]) }
The above method will become a really handy tool for us, because we’ll use it for two purposes:
- We’ll display the single file name (the first value in the tuple) to the tableview.
- We’ll use the file extension for creating a short description about the document type.
Let’s head towards the first one, and let’s pay our first visit to the tableView(tableView:cellForRowAtIndexPath)
tableview method. Modify it so it looks like the next one (add the two lines between the cell dequeueing and the return line), so for each file you get the name parts and display the name to the main text label of the cell:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("idCellFile", forIndexPath: indexPath) let currentFileParts = extractAndBreakFilenameInComponents(fileURLs[indexPath.row]) cell.textLabel?.text = currentFileParts.fileName return cell }
Let’s create another custom method now. This one is going to return a simple description regarding the document type related to each file extension. There’s nothing difficult as you’ll see, there’s just one switch
statement that actually “decides” about the proper description. Note that I’ve added cases only for the file types used in this demo app, however you can modify it and add or remove extensions depending on your needs.
func getFileTypeFromFileExtension(fileExtension: String) -> String { var fileType = "" switch fileExtension { case "docx": fileType = "Microsoft Word document" case "pages": fileType = "Pages document" case "jpeg": fileType = "Image document" case "key": fileType = "Keynote document" case "pdf": fileType = "PDF document" default: fileType = "Text document" } return fileType }
Back to the tableView(tableView:cellForRowAtIndexPath)
, let’s add one line that calls the above method, so we display each file’s type description to the secondary label of the cell:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("idCellFile", forIndexPath: indexPath) ... cell.detailTextLabel?.text = getFileTypeFromFileExtension(currentFileParts.fileExtension) return cell }
There’s one thing left to do, and that is to set the proper number of rows for the tableview. If you noticed, zero rows are returned by default to the starter project, and that’s something we have to change if we want to see any rows to the tableview. Go ahead, and replace the following tableview method with the content you see right next:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return fileURLs.count }
If you feel so, go and run the app for first time. If you went step by step through the text above, then what you will get is going to be similar to the screenshot in the beginning of this part.
The Quick Look Preview Controller Datasource
The first thing you should always do when working with the Quick Look framework is to import it in the class (or classes) that you’re planning to use it. So, this is where we’re going to start from here as well. In the FileListViewController.swift
file, move to the top and before the class opening, and add the next line:
import QuickLook
Now we are ready to declare and initialise at the same time a vital object that will allow us to use the Quick Look framework features. To the top of the FileListViewController
class add the following:
let quickLookController = QLPreviewController()
As you see, I preferred here to declare a class-wide QLPreviewController
object, however this isn’t mandatory to do if you don’t want so. There’s an alternative way, and that is to use local object(s) in the method that you trigger the presentation of the Quick Look Preview Controller.
Before we are able to use the quickLookController
object, we must adopt the QLPreviewControllerDataSource
protocol. This is mandatory to do every time you use the Quick Look framework, because the couple of methods that you’ll see in a few moments consist of your only way to set the datasource of the Quick Look Preview Controller. However, before we see those methods, let’s go to the header line of the class and let’s append the QLPreviewControllerDataSource
protocol:
class FileListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, QLPreviewControllerDataSource
Let’s focus now on the two required datasource methods that must be implemented mandatorily. As I just mentioned above, those two (methods) will actually enable the previewing feature on the Quick Look Preview Controller, as they will “say” to it how many documents exist for previewing, and which one should be previewed at any given time. Apparently, the fileURLs
array we created earlier is going to be used again now.
The first method specifies how many items exist for previewing in the Quick Look Preview Controller. This number in our demo app is equal to the total items contained in the fileURLs
array, therefore all we need to do is to return that exact value:
func numberOfPreviewItemsInPreviewController(controller: QLPreviewController) -> Int { return fileURLs.count }
The second method specifies the item that should be previewed among all the items existing in the fileURLs
array:
func previewController(controller: QLPreviewController, previewItemAtIndex index: Int) -> QLPreviewItem { return fileURLs[index] }
According to the official docs, the QLPreviewItem
type returned from the above method (it’s a protocol actually) declares its methods as a category on the NSURL class, therefore we’re allowed to return NSURL objects from it (for simplicity, consider the NSURL and the QLPreviewItem to be pretty much the same). You might find the supported methods of the QLPreviewItem
protocol useful, so take a look here in case you want to create a custom QLPreviewItem
object.
The above two methods are necessary every time you need to use the QLPreviewControllerDataSource
. Always make sure that the number of items in the Quick Look Preview Controller (the number returned in the first method) matches to the length of the collection you’re using as a datasource (second method), otherwise you’ll have problems with the index value being out of the array bounds.
There’s one last step we shouldn’t forget about. We must set our class as the datasource of the quickLookController
object. Go to the viewDidLoad()
method and add the next line:
override func viewDidLoad() { ... quickLookController.dataSource = self }
Previewing Documents
Having set our class as the datasource of the Quick Look Preview Controller object and having implemented the two respected methods, it’s time to make our demo app respond to our taps and open the selected document for preview. For that purpose, we’re going to implement the next tableview method:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { }
One basic and simple rule that you should always have in mind, is to make sure that the document you’re trying to preview can be actually opened by the Quick Look framework. That’s easy to do, as the QLPreviewController
class provides a class method named canPreviewItem(:)
designed specifically for that reason. That method returns a Bool
value, and when it’s true
the document type is supported and can be previewed. In any other case the document cannot be opened. Needless to say that you should proceed only when that method returns true
and the the document is really supported by the Quick Look framework.
Once you check what I just described and being able to proceed, you must specify the index
of the item that matches to the tapped row and eventually to the NSURL
(path) of the document that should be previewed. The quickLookController
object has a property named currentPreviewItemIndex
that you can use for that purpose. Even though it’s logically difficult to make a mistake here, make sure that the index of the item that is about to be previewed is within the bounds of the datasource array (in this case the fileURLs
array), otherwise you’ll probably see your app crashing. I would say that this part is the most important one here, as this is the place that you actually “tell” the Quick Look Preview Controller which document to open.
Lastly, the Quick Look Preview Controller must be presented. This can be done in two different ways: It can be either presented as a modal view controller, or to be pushed to the navigation stack if you’re making a navigation-based app and you have a navigation controller. I’ll show you both.
Now that all the above have been said, it’s time to see them written in code. It takes three lines of code only which are shown to the following snippet:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { if QLPreviewController.canPreviewItem(fileURLs[indexPath.row]) { quickLookController.currentPreviewItemIndex = indexPath.row navigationController?.pushViewController(quickLookController, animated: true) } }
As you see, first of all we make sure that the tapped document can be previewed (in this demo app we know in advance that all documents can be opened, but this is a check you should always do, especially if you grab your documents from a server and you can’t be sure about their types). Next, we specify the exact index of the document we want to open, and lastly we push the quickLookController
to the navigation stack. Alternatively, here’s what you need to present the preview controller modally:
presentViewController(quickLookController, animated: true, completion: nil)
If you run the app now and you tap on any row, you’ll see the respective document being opened for preview by the Quick Look Preview Controller.
Features Provided by the Quick Look Preview Controller
As you can notice, the Quick Look Preview Controller has a toolbar at the bottom with two bar button items. The first one on the left allows you to send and share the currently previewed document through an activity controller (UIActivityViewController
).
The number and kind of sharing options being available in the activity controller varies, and depends on the device that the app is running (of course, the provided options are lesser in the Simulator than in a device), the social networks you’re currently logged in, etc. But, no matter what, by using the left bar button item you can instantly send or share the previewed document in no time at all.
The second bar button item in the right side presents a modal view controller with a list of all the available documents specified in the datasource.
That button can be used as a shortcut actually, so the users can switch between documents without being necessary to dismiss (or pop) the Quick Look Preview Controller. It’s a fast and immediate access to the whole document collection that sometimes can be simpler to use instead of returning back to the originating view controller.
The Quick Look Preview Controller Delegate
Besides than mandatorily adopting the QLPreviewControllerDataSource
protocol and implementing the two required datasource methods, you can also conform to the QLPreviewControllerDelegate
protocol and implement some delegate methods that will give you some greater control over the preview controller and some actions related to it. Note that adopting the second protocol and implementing the delegate methods is optional, and that your app can work even without it. However, we’ll see some highlights just to extend a bit the goals of this tutorial.
The first step is (what else?) to adopt the QLPreviewControllerDelegate
protocol, so you have to modify the header of the class in the FileListViewController.swift
file as follows:
class FileListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, QLPreviewControllerDataSource, QLPreviewControllerDelegate
Which methods you’ll choose to implement depends on you and your app’s requirements. A full list of all the provided delegate methods can be found in the official docs. Here we’ll see just a part of them.
After having adopted the protocol, it’s important to make our class the delegate of the quickLookController
object, so go to the viewDidLoad()
method and add the next line:
override func viewDidLoad() { ... quickLookController.delegate = self }
We’ll begin by presenting a couple of two delegate methods that are invoked by the Quick Look framework when the preview controller is about to be dismissed (or popped in navigation-based apps like this one). Actually, there’s one delegate method called right before the preview controller gets dismissed, and one that is called once the preview controller has been dismissed. For the sake of the demo, we’ll implement both of them, and we’ll print a single message on the console so as to know when each one gets invoked. Let’s start by the first one:
func previewControllerWillDismiss(controller: QLPreviewController) { print("The Preview Controller will be dismissed.") }
In the second delegate method we’ll deselect the previously tapped row, in addition to the message that will be printed to the console. That way we set an action to be triggered on our view controller when the preview controller is popped.
func previewControllerDidDismiss(controller: QLPreviewController) { tblFileList.deselectRowAtIndexPath(tblFileList.indexPathForSelectedRow!, animated: true) print("The Preview Controller has been dismissed.") }
Running the app now, you’ll verify that when the preview controller gets popped from the navigation stack the tapped row is deselected automatically.
It’s quite common for links to be existing to the documents previewed by the Quick Look Preview Controller. When that happens, there might be cases where you would like the links to be working, but there are also cases where the links should trigger no action at all. The following delegate method exists for that reason exactly, as it’s the perfect place to apply your logic and allow or prevent links from opening the target URL(s):
func previewController(controller: QLPreviewController, shouldOpenURL url: NSURL, forPreviewItem item: QLPreviewItem) -> Bool { if item as! NSURL == fileURLs[0] { return true } else { print("Will not open URL \(url.absoluteString)") } return false }
In the above method, we don’t allow any URL to open (return false
), with one exception only. If the link comes from the PDF document (the first one), then we allow it and true is returned in that case. Run the app again, and try to tap on the link in the PDF document first, and then on the link existing in the Word document. See what happens and keep watching your console as well. You’ll notice that the link in the Word document is being prevented from opening, while the Safari loads the URL specified in the PDF document.
Summary
Reaching to the end of the post, I think that we would all agree that the Quick Look framework is one of the most easy ones residing in the iOS SDK. Implementing it and allowing document previewing includes no hassle at all, and if you’re on the mood for it, there are some optional delegate methods to play with. In my opinion the Quick Look framework and the Quick Look Preview Controller specifically consist of a great tool when the app has to deal with documents, as important functionalities are provided almost for free. This post will hopefully set you on the right track if you’re about to put the Quick Look framework in motion. And if you’re not, it really worths it considering doing so!
For reference, you can download the full project on GitHub.