DemoBots/ProgressScene.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
A scene used to indicate the progress of loading additional content between scenes. |
*/ |
import SpriteKit |
/** |
The KVO context for `ProgressScene` instances. This provides a stable |
address to use as the `context` parameter for the KVO observation methods. |
*/ |
private var progressSceneKVOContext = 0 |
class ProgressScene: BaseScene { |
// MARK: Properties |
/// Returns the background node from the scene. |
override var backgroundNode: SKSpriteNode? { |
return childNode(withName: "backgroundNode") as? SKSpriteNode |
} |
var labelNode: SKLabelNode { |
return backgroundNode!.childNode(withName: "label") as! SKLabelNode |
} |
var progressBarNode: SKSpriteNode { |
return backgroundNode!.childNode(withName: "progressBar") as! SKSpriteNode |
} |
/* |
Because we're using a factory method for initialization (we want to load |
the scene from a file, but `init(fileNamed:)` is not a designated init), |
we need to make most of the properties `var` and implicitly unwrapped |
optional so we can set the properties after creating the scene with |
`progressScene(withSceneLoader sceneLoader:)`. |
*/ |
/// The scene loader currently handling the requested scene. |
var sceneLoader: SceneLoader! |
/// Keeps track of the progress bar's initial width. |
var progressBarInitialWidth: CGFloat! |
/// Add child progress objects to track downloading and loading states. |
var progress: Progress? { |
didSet { |
// Unregister as an observer on the old value for the "fractionCompleted" property. |
oldValue?.removeObserver(self, forKeyPath: "fractionCompleted", context: &progressSceneKVOContext) |
// Register as an observer on the initial and for changes to the "fractionCompleted" property. |
progress?.addObserver(self, forKeyPath: "fractionCompleted", options: [.new, .initial], context: &progressSceneKVOContext) |
} |
} |
/// A registered observer object for `SceneLoaderDownloadFailedNotification`s. |
private var downloadFailedObserver: AnyObject? |
// MARK: Initializers |
/** |
Constructs a `ProgressScene` that will monitor the download |
progress of on demand resources and the loading progress of bringing |
assets into memory. |
*/ |
static func progressScene(withSceneLoader loader: SceneLoader) -> ProgressScene { |
// Load the progress scene from its sks file. |
let progressScene = ProgressScene(fileNamed: "ProgressScene")! |
progressScene.createCamera() |
progressScene.setup(withSceneLoader: loader) |
// Return the setup progress scene. |
return progressScene |
} |
func setup(withSceneLoader loader: SceneLoader) { |
// Set the sceneLoader. This may be in the downloading or preparing state. |
self.sceneLoader = loader |
// Grab the `sceneLoader`'s progress if it is already loading. |
if let progress = sceneLoader.progress { |
self.progress = progress |
} |
else { |
// Else start loading the scene and hold onto the progress. |
progress = sceneLoader.asynchronouslyLoadSceneForPresentation() |
} |
// Register for notifications posted when the `SceneDownloader` fails. |
let defaultCenter = NotificationCenter.default |
downloadFailedObserver = defaultCenter.addObserver(forName: NSNotification.Name.SceneLoaderDidFailNotification, object: sceneLoader, queue: OperationQueue.main) { [unowned self] notification in |
guard let loader = notification.object as? SceneLoader, let error = loader.error else { fatalError("The scene loader has no error to show.") } |
self.showError(error as NSError) |
} |
} |
deinit { |
// Unregister as an observer of 'SceneLoaderDownloadFailedNotification' notifications. |
if let downloadFailedObserver = downloadFailedObserver { |
NotificationCenter.default.removeObserver(downloadFailedObserver, name: NSNotification.Name.SceneLoaderDidFailNotification, object: sceneLoader) |
} |
// Set the progress property to nil which will remove this object as an observer. |
progress = nil |
} |
// MARK: Scene Life Cycle |
override func didMove(to view: SKView) { |
super.didMove(to: view) |
centerCameraOnPoint(point: backgroundNode!.position) |
// Remember the progress bar's initial width. It will change to indicate progress. |
progressBarInitialWidth = progressBarNode.frame.width |
if let error = sceneLoader.error { |
// Show the scene loader's error. |
showError(error as NSError) |
} |
else { |
showDefaultState() |
} |
} |
// MARK: Key Value Observing (KVO) for NSProgress |
@nonobjc override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
// Check if this is the KVO notification we need. |
guard context == &progressSceneKVOContext else { |
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) |
return |
} |
if let changedProgress = object as? Progress, changedProgress == progress, keyPath == "fractionCompleted" { |
// Update the progress UI on the main queue. |
DispatchQueue.main.async { |
guard let progress = self.progress else { return } |
// Update the progress bar to match the amount of progress completed. |
self.progressBarNode.size.width = self.progressBarInitialWidth * CGFloat(progress.fractionCompleted) |
// Display a contextually specific progress description. |
self.labelNode.text = progress.localizedDescription |
} |
} |
} |
// MARK: ButtonNodeResponderType |
override func buttonTriggered(button: ButtonNode) { |
switch button.buttonIdentifier! { |
case .retry: |
// Set up the progress for a new preparation attempt. |
progress = sceneLoader.asynchronouslyLoadSceneForPresentation() |
sceneLoader.requestedForPresentation = true |
showDefaultState() |
case .cancel: |
/* |
Canceling the parent progress propagates the cancellation to the child |
progress objects. |
In `SceneLoaderDownloadingResourcesState` this will cause the completionHandler to |
be invoked on `beginAccessingResources(withCompletionHandler completionHandler:)` |
with an NSUserCancelledError. See the NSBundleResourceRequest documentation |
for more information. |
In `SceneLoaderPreparingResourcesState` this will cancel all operations. |
*/ |
progress!.cancel() |
default: |
// Allow `BaseScene` to handle the event in `BaseScene+Buttons`. |
super.buttonTriggered(button: button) |
} |
} |
// MARK: Convenience |
func button(withIdentifier identifier: ButtonIdentifier) -> ButtonNode? { |
return backgroundNode?.childNode(withName: identifier.rawValue) as? ButtonNode |
} |
func showDefaultState() { |
progressBarNode.isHidden = false |
// Only display the "Cancel" button. |
button(withIdentifier: .home)?.isHidden = true |
button(withIdentifier: .retry)?.isHidden = true |
button(withIdentifier: .cancel)?.isHidden = false |
// Reset the button focus. |
resetFocus() |
} |
func showError(_ error: NSError) { |
// A new progress object will have to be created for any subsequent loading attempts. |
progress = nil |
// Display "Quit" and "Retry" buttons. |
button(withIdentifier: .home)?.isHidden = false |
button(withIdentifier: .retry)?.isHidden = false |
button(withIdentifier: .cancel)?.isHidden = true |
// Hide normal state. |
progressBarNode.isHidden = true |
progressBarNode.size.width = 0.0 |
// Reset the button focus. |
resetFocus() |
// Check if the error was due to the user cancelling the operation. |
if error.domain == NSCocoaErrorDomain && error.code == NSUserCancelledError { |
labelNode.text = NSLocalizedString("Cancelled", comment: "Displayed when the user cancels loading.") |
} |
else { |
showAlert(for: error) |
} |
} |
func showAlert(for error: NSError) { |
labelNode.text = NSLocalizedString("Failed", comment: "Displayed when the scene loader fails to load a scene.") |
// Display the error description in a native alert. |
#if os(OSX) |
guard let window = view?.window else { fatalError("Attempting to present an error when the scene is not in a window.") } |
let alert = NSAlert(error: error) |
alert.beginSheetModal(for: window, completionHandler: nil) |
#else |
guard let rootViewController = view?.window?.rootViewController else { fatalError("Attempting to present an error when the scene is not in a view controller.") } |
let alert = UIAlertController(title: error.localizedDescription, message: error.localizedRecoverySuggestion, preferredStyle: .alert) |
let alertAction = UIAlertAction(title: "OK", style: .`default`, handler: nil) |
alert.addAction(alertAction) |
rootViewController.present(alert, animated: true, completion: nil) |
#endif |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13