Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4400

On-Demand Resources in tvOS Tutorial

$
0
0
Note: This is an abbreviated chapter from the tvOS Apprentice, to give you a sneak peek of what’s inside the book, released as part of the tvOS Apprentice Week. We hope you enjoy!

on-demand-feature

When I got my first iPod Touch several years ago, I excitedly opened the App Store and started downloading the first ten games from the Top Charts. It wasn’t long before I came to the sad realization that my iPod storage wasn’t infinite: the alert telling me I didn’t have enough storage to download all the apps quashed my initial enthusiasm.

iOS 9 and tvOS 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’s needed up to that point. When 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!
Note: tvOS apps have a maximum initial download size of 200 MB, and any additional resources must be available through ODR. Not only is ODR recommended, it is a requirement for many apps!

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

Getting Started

First download these two files:

Put both files in a folder somewhere (I called mine ODR) and unzip both files. The resulting directory structure should look like this:

DirStructure

Open RWHomeTheater in Xcode and build and run. You’ll see the following:

RWHomeTheater

In this app, you can pick from a collection of remarkable, entertaining videos that range from a cow eating grass to water boiling. Currently the app has 14 videos, which take up nearly 200 MB by themselves — looks like a case for some ODR!

After you play around with the app a bit, back in Xcode open Main.storyboard and zoom in on the top-right of the RW Home Theater Scene. Notice there’s a UIProgressView that’s hidden by default; later on, you’ll use this to show the user the status of their downloads.

Note: The starter project for this tutorial comes from our book, the tvOS Apprentice. If you’d like to learn how to make this project and learn about video playback in tvOS along the way, check out the book!

NSBundleResourceRequest and Tags

To work with ODR, you’ll need a basic understanding of NSBundle and how it handles files.

NSBundle 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 ODR.

Most of the time, you’ll use NSBundle.mainBundle() 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”.

diagram_1

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 an NSBundle. Then, instead of using the main bundle to find the resource, you simply look inside this alternative bundle.

diagram_2

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 become deallocated. 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 the 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:

CowsEatingGrassLocation

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

ShowingResourceTagField

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:

ResourceTagFieldNameAdded

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:

ResourceTagTable

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 download an alternative version of the starter project that has the tags pre-filled for you.

Once you’ve added all of the tags – whichever way you chose to do it – the Resource Tags menu should look like the following:

ResourceMenuAfterFilesAdded

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:

DiskDebug

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\Swift File, and name it ResourceManager.

Add the following class declaration to the file:

class ResourceManager {
  static let sharedManager = ResourceManager()
}

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

Now, add the following code to ResourceManager:

// 1
func requestVideoWithTag(tag: String,
  onSuccess: () -> Void,
  onFailure: (NSError) -> Void) -> NSBundleResourceRequest {
 
    // 2
    let videoRequest = NSBundleResourceRequest(tags: [tag])
    videoRequest.loadingPriority
      = NSBundleResourceRequestLoadingPriorityUrgent
 
    // 3
    videoRequest.beginAccessingResourcesWithCompletionHandler {
      error in
      NSOperationQueue.mainQueue().addOperationWithBlock {
        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.
  2. 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.
  3. To start loading the resources, call beginAccessingResourcesWithCompletionHandler. This 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.
  4. Return the bundle request so that the requestor 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: NSURL {
  return NSBundle.mainBundle().URLForResource(videoName,
    withExtension: "mp4")!
}

Replace that declaration with the following:

var videoURL: NSURL!

Also, change the implementation of videoFromDictionary to match the following:

static func videoFromDictionary(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.

Requesting tags on selection

Open VideoListViewController.swift and add the following property to VideoListViewController:

var currentVideoResourceRequest: NSBundleResourceRequest?

This property will store the NSBundleResourceRequest for the most recently requested video.

Next, you’ll need to request this video when the user selects one of the videos in the collection view.

Find didSelectVideoAtIndexPath(_:) and replace its implementation with the following:

func didSelectVideoAtIndexPath(indexPath: NSIndexPath) {
  // 1
  currentVideoResourceRequest?.progress.cancel()
  // 2
  guard var video = collectionViewDataSource
    .videoForIndexPath(indexPath),
    let videoCategory = collectionViewDataSource
      .videoCategoryForIndexPath(indexPath) else {
        return
  }
  // 3
  currentVideoResourceRequest = ResourceManager.sharedManager
    .requestVideoWithTag(video.videoName,
      onSuccess: { [weak self] in
 
      },
      onFailure: { [weak self] error in
 
      }
  )
}

The code breaks down as follows:

  1. If there’s a video request in progress, cancel that resource request and let the new video request take priority.
  2. This is the same code from didSelectVideoAtIndexPath(_:); it’s how you find the video and category of the selected cell.
  3. Set currentVideoResourceRequest equal to a new NSBundleResourceRequest created by ResourceManager. The resource tag passed as a parameter is the name of the video – this is why you followed that strict naming scheme earlier.

The next step is to handle the failure case. Add the following method to the VideoListViewController extension:

func handleDownloadingError(error: NSError) {
  switch error.code{
  case NSBundleOnDemandResourceOutOfSpaceError:
    let message = "You don't have enough storage left to download this resource."
    let alert = UIAlertController(title: "Not Enough Space",
      message: message,
      preferredStyle: .Alert)
    alert.addAction(UIAlertAction(title: "OK",
      style: .Cancel, handler: nil))
    presentViewController(alert, animated: true,
      completion: nil)
  case NSBundleOnDemandResourceExceededMaximumSizeError:
    assert(false, "The bundle resource was too large.")
  case NSBundleOnDemandResourceInvalidTagError:
    assert(false, "The requested tag(s) (\(currentVideoResourceRequest?.tags ?? [""])) does not exist.")
  default:
    assert(false, error.description)
  }
}

handleDownloadingError(_:) handles all the possible errors from a resource request.

The main three error cases occur when either the device is out of storage, the resource is too large, or you’ve requested an invalid tag. If the device is out of storage, you alert the user of the problem. You’ll need to catch the other two issues before you deploy your app; at that point, it’s too late to make changes. So that they don’t go unnoticed in your testing phase (you are testing, right?), you crash the app with an error message.

The default assertion catches any other errors that could occur, such as network loss.

Now, call your new method in the onFailure(_:) closure:

self?.handleDownloadingError(error)

In the onSuccess closure, add the following code to handle the successful download:

guard let currentVideoResourceRequest =
  self?.currentVideoResourceRequest else { return }
video.videoURL = currentVideoResourceRequest.bundle
  .URLForResource(video.videoName, withExtension: "mp4")
let viewController = PlayVideoViewController
  .instanceWithVideo(video, videoCategory: videoCategory)
self?.navigationController?.pushViewController(viewController,
  animated: true)

Here you set the URL of the selected video to the downloaded resource within the requested bundle. Then, you present a new instance of PlayVideoViewController with the video as a parameter.

Run your app; select video to play and you’ll notice there’s a bit of a delay before the video starts. This is because the video is being downloaded from the server – or in this case, from your computer.

To check that your app is using the on-demand bundle, open the debug navigator and select Disk; the Status column of your chosen video will now show “In Use”.

If you press Menu on your remote, you’ll notice the tag still shows as “In Use”. That’s not quite right, is it? The resource should change to “Downloaded” since you’re no longer using the resource. You’ll fix this in the next section.

Purging Content

Responsible management of your resources includes releasing them when you’re done. This takes two steps:

  1. Call endAccessingResources() on the NSBundleResourceRequest.
  2. Let the resource request deallocate.

The best time to let ODR know you no longer need the video is once the user’s finished watching the video. This happens when you dismiss PlayVideoViewController and VideoListViewController reappears on-screen.

Add the following method to VideoListViewController.swift:

override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)
 
  currentVideoResourceRequest?.endAccessingResources()
  currentVideoResourceRequest = nil
}

This code first checks that VideoListViewController reappears once you’ve dismissed a different view controller from the navigation stack. It then calls endAccessingResources() on the resource request and sets the property to nil, which lets the resource request deallocate.

Build and run your app; watch the Resource Tags menu as you play a video, then press Menu to go back. The Resource Tags menu now shows the requested resource as “Downloaded”. Perfect! You won’t delete the resource until the system needs the space. As long as the device has room, the resources will remain in storage.

Progress Reporting

At present, the user has no way to see the progress of the download. Is the video almost completely downloaded? Or is it not downloading at all?

In order to indicate the progress of the download, you’ll use a progress bar to observe the progress of the NSBundleResourceRequest.

Back in VideoListViewController.swift, add the following line to the beginning of didSelectVideoAtIndexPath(_:):

progressView.hidden = false

At the beginning of both the onSuccess and onFailure closures, add the following line – which does the exact opposite as the previous line:

self?.progressView.hidden = true

This code shows the progress bar when the download begins, and hides it when the download ends.

Key-Value Observing

To connect the progress bar with the NSBundleResourceRequest, you need to use Key-Value Observing.

Open ResourceManager.swift and add the following to the top of the file, above the class declaration:

let progressObservingContext = UnsafeMutablePointer<Void>()

Next, you need to change requestVideoWithTag to accept the progress observer as a parameter.

Replace the declaration of requestVideoWithTag with the following:

func requestVideoWithTag(tag: String,
  progressObserver: NSObject?,
  onSuccess: () -> Void,
  onFailure: (NSError) -> Void) -> NSBundleResourceRequest {

This method now has a new progressObserver parameter that will make it easier to use KVO with your custom view controller and progress bar.

Within this method, add the following code before the return statement:

if let progressObserver = progressObserver {
  videoRequest.progress.addObserver(progressObserver,
    forKeyPath: "fractionCompleted",
    options: [.New, .Initial],
    context: progressObservingContext)
}

Here, you add the argument as an observer to the request’s progress.

Just like you added the observer before the resource was loaded, you’ll need to remove the observer once it’s loaded. Add the code below to the beginning of the NSOperationQueue.mainQueue().addOperationWithBlock block:

if let progressObserver = progressObserver {
  videoRequest.progress.removeObserver(progressObserver,
    forKeyPath: "fractionCompleted")
}

Xcode will respond with an error in VideoListViewController; this is because you changed the method signature of requestVideoWithTag.

Open VideoListViewController.swift and change the line with the error to the following:

currentVideoResourceRequest = ResourceManager.sharedManager
  .requestVideoWithTag(video.videoName, progressObserver: self,
  onSuccess: { [weak self] in
    ...
  },
  onFailure: { [weak self] error in
    ...
  }
)

The only change here is that you added the progressObserver parameter and passed self as the observer.

In order to respond to changes as the download progresses, you’ll need to implement observeValueForKeyPath in the view controller – your observer.

Add the following method to VideoListViewController:

override func observeValueForKeyPath(keyPath: String?,
  ofObject object: AnyObject?, change: [String : AnyObject]?,
  context: UnsafeMutablePointer<Void>) {
 
    if context == progressObservingContext
      && keyPath == "fractionCompleted"{
 
        NSOperationQueue.mainQueue().addOperationWithBlock {
          self.progressView.progress
            = Float((object as! NSProgress).fractionCompleted)
        }
    }
}

When the value of the download’s progress changes, you reflect this change in the progress bar on the main thread.

Build and run your app; select a video to watch and you’ll see the progress bar at the top-right corner of the screen. Once the progress bar has filled, it will disappear and the video will play:

ProgressBarScreenshot

w00t – videos on demand!

Where To Go From Here?

Here is the example code from this On-Demand Resources in tvOS tutorial.

TVTThumb

You now have all the details you need to bring On-Demand Resources into your tvOS apps. ODR lets you create large, resource-intensive apps that still fit within the 200MB initial download requirement.

If you want to learn more, you should check out our book the tvOS Apprentice. The chapter in the book goes into further detail about anticipating the users actions and the different types of resource tags that are available. You also might enjoy the other 27 chapters and 500+ pages in the book! :]

In the meantime, if you have any questions or comments about using TVML templates, please join the forum discussion below!

The post On-Demand Resources in tvOS Tutorial appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4400

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>