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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
On-Demand Resources in iOS Tutorial
20 mins
- Getting Started
- Examining the App
- Game States
- Starting the Game
- Bundles
- Tag Types
- Assigning and Organizing Tags
- Introducing NSBundleResourceRequest
- Using NSBundleResourceRequest
- Downloading Resources
- Looking at the Device Disk
- Best Practices
- Error Handling
- Loading Priorities
- Purging Resources
- Where to Go From Here?
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:
Open the File Inspector using the Utilities menu. Find the section named 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.
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.
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:
- First, there’s the convenience
init()
method. It takes aSet
of tags representing the resources to download. - 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. - Next, the variable
tags
contains the set of tags to be requested by this object. - Next,
bundle
represents the alternate bundle described earlier. Thisbundle
is where the system stores the retrieved resources. - Next,
beginAccessingResources
starts the request of the resources. You invoke this method and pass it a completion handler that takes an Error object. - 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. - 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:
- 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.
-
Next, you create an instance of
NSBundleResourceRequest
to perform your request. -
Next, you verify the creation of the instance was successful, and if so, invoke
beginAccessingResources()
to begin the request. - If there was an error, the app executes the error handler, and you cannot use that resource.
- 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.
-
First, use the shared instance of
ODRManager
and callrequestSceneWith(tag:onSuccess:onFailure:)
. You pass this method the tag of the next level and a success and error handler. - If the request is successful, create the actual game scene it retrieved and present the scene to the user.
-
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:
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.