On-Demand Resources in iOS Tutorial

On-demand resources are a technique to reduce the initial download size of applications which rely on large assets for use. This is especially useful to games developers with large assets only required during certain stages of the game. By James Goodwill.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Assigning and Organizing Tags

The first thing to consider is which resources you want to package with the application. For this game app, it makes sense to at least give the user the first level of the game. You don’t want them starting without any game levels.

In the project navigator, select GameScene2.sks from the Bamboo Breakout group:

Game Levels

Open the File Inspector using the Utilities menu. Find the section named On Demand Resource Tags:

On Demand Resource Tags

When tagging a resource, try to use a meaningful name. This will help you keep all your on-demand resources organized. For GameScene2.sks, which represents Level 2 of the game, you are going to use the tag level2.

Type level2 in the Tags input and press Enter.

Tagging Level 2

Once you’ve finished tagging GameScene2.sks, tag the rest of the scenes using the same pattern. When finished, select the Bamboo Breakout Target, Resource Tags, and then All. You should see all the tags you added.

Allthetags.

Introducing NSBundleResourceRequest

Okay, you’ve tagged all your on-demand resources. It’s time to add the code to download them. Before doing this, take a closer look at the NSBundleResourceRequest object:

// 1
public convenience init(tags: Set<String>) 
// 2
open var loadingPriority: Double
// 3
open var tags: Set<String> { get }
// 4 
open var bundle: Bundle { get }
// 5
open func beginAccessingResources(completionHandler: @escaping (Error?) -> Swift.Void)
// 6
open var progress: Progress { get }
// 7 
open func endAccessingResources()

Taking it step-by-step:

  1. First, there’s the convenience init() method. It takes a Set of tags representing the resources to download.
  2. Next is the variable loadingPriority. It provides a hint to the resource loading system and represents the loading priority of this request. The range of this priority is from 0 to 1, with 1 being the highest priority. The default value is 0.5.
  3. Next, the variable tags contains the set of tags to be requested by this object.
  4. Next, bundle represents the alternate bundle described earlier. This bundle is where the system stores the retrieved resources.
  5. Next, beginAccessingResources starts the request of the resources. You invoke this method and pass it a completion handler that takes an Error object.
  6. Next, there’s a Progress object. You can watch this object to see the status of the download. This application won’t use this object, because the assets are so small and download really quickly. It’s good to be aware of it though.
  7. Finally, endAccessingResources tells the system you no longer need these resources. The system will now know it can purge these resource from the device.

Using NSBundleResourceRequest

Now that you know the internals of NSBundleResourceRequest, you can create a utility class to manage the downloading of resources.

Create a new Swift file and name it ODRManager. Replace the contents of the file with the following:

import Foundation

class ODRManager {

  // MARK: - Properties
  static let shared = ODRManager()
  var currentRequest: NSBundleResourceRequest?
}

Currently the class contains a reference to itself (implementing the singleton approach) and a variable of type NSBundleResourceRequest.

Next, you’ll need a method to start the ODR request. Add the following method below the currentRequest property:

// 1
func requestSceneWith(tag: String,
                onSuccess: @escaping () -> Void,
                onFailure: @escaping (NSError) -> Void) {
    
  // 2
  currentRequest = NSBundleResourceRequest(tags: [tag])
    
  // 3
  guard let request = currentRequest else { return }

  request.beginAccessingResources { (error: Error?) in
        
    // 4
    if let error = error {
      onFailure(error as NSError)
      return
    }

    // 5
    onSuccess()
  }
}

Taking each commented section in turn:

  1. The first thing to look at is the method definition. This method takes three parameters. The first is the tag of the on-demand resource you want to retrieve. The second is a success handler and the third is an error handler.
  2. Next, you create an instance of NSBundleResourceRequest to perform your request.
  3. Next, you verify the creation of the instance was successful, and if so, invoke beginAccessingResources() to begin the request.
  4. If there was an error, the app executes the error handler, and you cannot use that resource.
  5. If there wasn’t any error, the app can execute the success handler. At this point, the app can assume the resource is available for use.

Downloading Resources

Now it’s time to put this class to use. Open GameScene.swift, find touchesBegan(_:with:) and change the LevelOver case to the following:

case is LevelOver:
      
  // 1
  ODRManager.shared.requestSceneWith(tag: "level\(nextLevel)", onSuccess: {
       
    // 2 
    guard let newScene = GameScene(fileNamed:"GameScene\(self.nextLevel)") else { return }
          
    newScene.scaleMode = .aspectFit
    newScene.nextLevel = self.nextLevel + 1
    let reveal = SKTransition.flipHorizontal(withDuration: 0.5)
    self.view?.presentScene(newScene, transition: reveal)
  },
    // 3
    onFailure: { (error) in

      let controller = UIAlertController(
        title: "Error",
        message: "There was a problem.",
        preferredStyle: .alert)

      controller.addAction(UIAlertAction(title: "Dismiss", style: .default, handler: nil))
      guard let rootViewController = self.view?.window?.rootViewController else { return }

      rootViewController.present(controller, animated: true)
    })

At first glance, this may look like a complex body of code, but it’s pretty straightforward.

  1. First, use the shared instance of ODRManager and call requestSceneWith(tag:onSuccess:onFailure:). You pass this method the tag of the next level and a success and error handler.
  2. If the request is successful, create the actual game scene it retrieved and present the scene to the user.
  3. If there was an error, create an instance of UIAlertController and let the user know a problem occurred.

Looking at the Device Disk

Once you’ve made all these changes, build and run the app. See if you can get through the first level and then stop. You should see the following:

You may need to plug in your device and play the game there, since it can be difficult to play in the simulator. Be sure to leave your device plugged in and Xcode attached.

After beating the first level, tap the screen once more and stop. You will now see a screen like the following:

Open Xcode, open the Debug navigator then select Disk. Here, you’ll see the status of all on-demand resources in the app:

Device Disk

At this point, the app has only downloaded Level 2 and it’s In Use. Go ahead and play some more levels and keep an eye on the Disk Usage. You can watch as the app downloads each resource when it’s required.

James Goodwill

Contributors

James Goodwill

Author

Darren Ferguson

Tech Editor

Chris Belanger

Editor

Andy Obusek

Final Pass Editor and Team Lead

Over 300 content creators. Join our team.