On-Demand Resources in tvOS Tutorial

Learn how to download resources in your tvOS apps upon demand – especially useful if you are trying to make your app fit within the 200MB limit! By Jawwad Ahmad.

Leave a rating/review
Save for later
Share

Have you ever run into the situation in which you’ve run out of space on your iOS device? I’m pretty sure that all of us have at some point or another. So you go into your iPhone storage settings, determine which apps are taking up the most space and hit delete for a few of the largest apps that you haven’t used in a while.

As an app developer, you’d rather your app not stand out for taking up the most disk space!

Apple introduced On-Demand Resources (ODR) as a way to manage the size of your apps. The idea behind ODR is that your app only retrieves the resources it needs when they’re required. If the app needs additional resources, it downloads them on-demand.

There are two main reasons to take advantage of ODR in your apps:

  1. Faster initial download: The faster the user can download the app, the faster they can start falling in love with it.
  2. Smaller app size: When storage is limited, the biggest apps are usually the first ones that users will delete. You want to keep your app’s storage footprint well under the radar!

Time to size things up — and dive right into adding ODR to an existing app.

Getting Started

Download the starter project for this tutorial here.

This tutorial will be using the project named RWHomeTheater, which lets you pick from a collection of remarkable, entertaining, enthralling videos that range from a cow eating grass to water boiling. The app has 14 videos, which take up nearly 200 MB.

Download the videos for the tutorial here: http://bit.ly/tvos-videos.

Now that you have these files on your computer, you need to copy them into the directory alongside starter, starter-tags, and final.

Your directory should now look like this:

Open RWHomeTheater and run the app in tvOS Simulator; you’ll see the lack of a Play All option in the app. All classes and collection view cells for Play All have been removed from the project.

In Xcode, open Main.storyboard and zoom in on the top-right of the RW Home Theater Scene. There’s a Progress View that’s hidden by default; later on, you’ll use this to show the user the status of their downloads.

NSBundleResourceRequest and Tags

To work with ODR, you’ll need a basic understanding of Bundles and how they handle files.

A Bundle represents a group of files on the device. In the case of ODR, these files will include resources such as videos, images, sounds, 3D models and shaders; the main exception is that you can’t include Swift, Objective-C or C++ source code in ODRs.

Most of the time, you’ll use Bundle.main as your main bundle. By default, all the resources downloaded in your app are included in the main bundle.

Think of the main bundle as a big box that holds all of your app’s “stuff”.

However, when you use ODR, the resources you download aren’t in the main bundle; instead, they’re in a separate bundle that contains all the resources of a given tag. The OS only downloads a tag’s resources when needed.

When you request a tag, the OS downloads all resources for that tag and stores it in a Bundle. Then, instead of using the main bundle to find the resource, you simply look inside this alternative bundle.

You request a tag using an instance of NSBundleResourceRequest. This resource request takes strings that represent the tags as parameters and lets you know when your resources are available. Once the resource has been loaded, you can use the resource request’s bundle property to access the files in the bundle.

Once you’re done with an NSBundleResourceRequest, you can call endAccessingResources() on the request and let it deallocate. This lets the system know you’re done with the resources and that it can delete them if necessary.

Now that you understand the foundations of ODR, you can get started on coding!

Adding Tags

In the project navigator, expand the videos group and the Animals subgroup under the Resources folder and select cows_eating_grass.mp4:

Show the File Inspector using the Utilities menu. Notice the section named On Demand Resource Tags:

To add a tag to a file, you simply type the name of the tag in this text box and press Enter.

In order to make things easier to discern from code, you’ll add the video’s name as its tag.

Be careful — if you don’t name the tag exactly the same as the video, then ODR won’t work properly.

For cows_eating_grass.mp4, add “cows_eating_grass” to the On Demand Resource Tags text field and press Enter:

Select the project in the project navigator, select the target, and go into the Resource Tags tab; you’ll see the Resource Tags menu.

The app has a tag named “cows_eating_grass” with a single associated file:

Next up — adding the tags for every single video file. Oh, come on, it’ll be fun! :]

If you don’t want to go through the laborious task of adding these tags — good news! You can simply open the project from this tutorial’s folder named starter – tags.

Once you’ve added all of the tags, whichever way you chose to do it, the Resource Tags menu should look like the following (with Prefetched selected):

Build and run your app; when the app loads, open the debug navigator in Xcode and select Disk. This will show you the status of all tags in the app:

The app won’t work at the moment since you haven’t downloaded the files to the Apple TV yet: every tag says “Not Downloaded”. Time to fix that problem!

Resource Utility Class

In order to keep all your ODR code organized, you’ll create a class to manage your on-demand resources. Select File\New\File…, then choose tvOS\Source\Swift File, and select Next. Name your file ResourceManager.swift and make sure that the RWHomeTheater group is selected in the Group dropdown, then click Create.

Add the following class declaration to the file:

class ResourceManager {
  static let shared = ResourceManager()
}

This code creates a new class named ResourceManager and creates a class variable for a shared instance.

Now, add the following code to the class:

// 1
func requestVideoWith(tag: String,
  onSuccess: @escaping () -> Void,
  onFailure: @escaping (Error) -> Void)
  -> NSBundleResourceRequest {

  // 2
  let videoRequest = NSBundleResourceRequest(tags: [tag])
  videoRequest.loadingPriority
    = NSBundleResourceRequestLoadingPriorityUrgent

  // 3
  videoRequest.beginAccessingResources { error in
    OperationQueue.main.addOperation {
      if let error = error {
        onFailure(error)
      } else {
        onSuccess()
      }
    }
  }      
  // 4
  return videoRequest
}

There’s a lot of new stuff going on here, so taking it piece by piece:

  1. Create a method to easily request a new video in your app. This method takes the tag name as a parameter, as well as two closures: one to call if the download succeeds, and one to call if the download fails.
  1. Instantiate a new NSBundleResourceRequest with the given tag. By setting loadingPriority of the request to NSBundleResourceRequestLoadingPriorityUrgent, the system knows that the user is waiting for the content to load and that it should download it as soon as possible.
  2. To start loading the resources, call beginAccessingResources. The completion handler is called on a background thread, so you add an operation to the main queue to respond to the download’s result. If there is an error, the onFailure closure is called; otherwise, onSuccess is called.
  3. Return the bundle request so that the requester can access the bundle’s contents after the download finishes.

Before you can add this to the project, you need to make a small change to the video struct. Open Video.swift and find the current declaration of videoURL:

var videoURL: URL {
  return Bundle.main.url(forResource: videoName,
    withExtension: "mp4")!
}

Replace that declaration with the following:

var videoURL: URL!

Also, change the implementation of videoFrom(dictionary:) to match the following:

static func videoFrom(dictionary: [String: String]) -> Video {
  let previewImageName = dictionary["previewImageName"]!
  let title = dictionary["videoTitle"]!
  let videoName = dictionary["videoName"]!
  return Video(previewImageName: previewImageName,
    title: title, videoName: videoName, videoURL: nil)
}

In the original code, videoURL pointed to a location in the main bundle for the file. However, since you’re now using ODR, the resource is now in a different bundle. You’ll set the video’s URL once the video is loaded — and once you know where the video is located.