Nik Nik - 3 months ago 36
Swift Question

3D Touch Quick Actions not working properly with SpriteKit

I'm currently developing a game using Swift 3, SpriteKit, and Xcode 8 beta. I'm trying to implement static 3D Touch Quick Actions from the home screen through the info.plist. Currently, the actions appear fine from the home screen, but don't go to the right SKScene - goes to the initial scene or last opened scene (if app is still open) which means that the scene is not being changed. I've tried various ways of setting the scene inside the switch statement, but none seem to work properly for presenting an SKScene as the line

window!.rootViewController?.present(gameViewController, animated: true, completion: nil)
only works on UIViewController.

Various parts of this code are from various tutorials, but I'm fairly sure through my investigation that the broken part is the scene being presented (unless I'm wrong), because it shouldn't even load any scene if a part is broken.

Are there any ways I can present an SKScene from the AppDelegate or set the opening scene based of the switch statement?

AppDelegate

import UIKit
import SpriteKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

var window: UIWindow?


func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.


}

func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.

}

func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}



func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.


}

func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.


}



func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.



guard !handledShortcutItemPress(forLaunchOptions: launchOptions) else { return false } // ADD THIS LINE

return true
}

}

extension AppDelegate: ShortcutItem {

/// Perform action for shortcut item. This gets called when app is active
func application(_ application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
completionHandler(handledShortcutItemPress(forItem: shortcutItem))

}

}

extension AppDelegate: ShortcutItemDelegate {

func shortcutItem1Pressed() {

GameData.sharedInstance.highScore = 100000
Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(loadShopScene), userInfo: nil, repeats: false)
}

@objc private func loadShopScene() {
let scene = ShopScene(size: CGSize(width: 768, height: 1024))
loadScene(scene: scene, view: window?.rootViewController?.view)
}

func shortcutItem2Pressed() {
Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(loadGameScene), userInfo: nil, repeats: false)
}

@objc private func loadGameScene() {
let scene = GameScene(size: CGSize(width: 768, height: 1024))
loadScene(scene: scene, view: window?.rootViewController?.view)

}

func shortcutItem3Pressed() {
// do something else
}

func shortcutItem4Pressed() {
// do something else
}

func loadScene(scene: SKScene?, view: UIView?, scaleMode: SKSceneScaleMode = .aspectFill) {
guard let scene = scene else { return }
guard let skView = view as? SKView else { return }

skView.ignoresSiblingOrder = true
#if os(iOS)
skView.isMultipleTouchEnabled = true
#endif
scene.scaleMode = scaleMode
skView.presentScene(scene)
}
}


3DTouchQuickActions.swift

import Foundation
import UIKit

/// Shortcut item delegate
protocol ShortcutItemDelegate: class {
func shortcutItem1Pressed()
func shortcutItem2Pressed()
func shortcutItem3Pressed()
func shortcutItem4Pressed()
}

/// Shortcut item identifier
enum ShortcutItemIdentifier: String {
case first // I use swift 3 small letters so you have to change your spelling in the info.plist
case second
case third
case fourth

init?(fullType: String) {
guard let last = fullType.components(separatedBy: ".").last else { return nil }
self.init(rawValue: last)
}

public var type: String {
return Bundle.main.bundleIdentifier! + ".\(self.rawValue)"
}
}

/// Shortcut item protocol
protocol ShortcutItem { }
extension ShortcutItem {

// MARK: - Properties

/// Delegate
private weak var delegate: ShortcutItemDelegate? {
return self as? ShortcutItemDelegate
}

// MARK: - Methods

/// Handled shortcut item press first app launch (needed to avoid double presses on first launch)
/// Call this in app Delegate did launch with options and exit early (return false) in app delegate if this method returns true
///
/// - parameter forLaunchOptions: The [NSObject: AnyObject]? launch options to pass in
/// - returns: Bool
func handledShortcutItemPress(forLaunchOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

guard let launchOptions = launchOptions, let shortcutItem = launchOptions[UIApplicationLaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem else { return false }

handledShortcutItemPress(forItem: shortcutItem)
return true
}

/// Handle shortcut item press
/// Call this in the completion handler in AppDelegate perform action for shortcut item method
///
/// - parameter forItem: The UIApplicationShortcutItem the press was handled for.
/// - returns: Bool
func handledShortcutItemPress(forItem shortcutItem: UIApplicationShortcutItem) -> Bool {
guard let _ = ShortcutItemIdentifier(fullType: shortcutItem.type) else { return false }
guard let shortcutType = shortcutItem.type as String? else { return false }

switch shortcutType {

case ShortcutItemIdentifier.first.type:
delegate?.shortcutItem1Pressed()

case ShortcutItemIdentifier.second.type:
delegate?.shortcutItem2Pressed()

case ShortcutItemIdentifier.third.type:
delegate?.shortcutItem3Pressed()

case ShortcutItemIdentifier.fourth.type:
delegate?.shortcutItem4Pressed()

default:
return false
}

return true
}
}

Answer

Your code is not working because in your app delegate you create a new instance of GameViewController instead of referencing the current one

let gameViewController = GameViewController() // creates new instance

I am doing exactly what you are trying to do with 3d touch quick actions in 2 of my games.

I use an NSTimer with a slight delay in my app Delegate to present the scene. I directly load the scene from the appDelegate, dont try to change the gameViewController scene for this.

I use a reusable helper for this. Assuming you set up everything correctly in your info.plist. (I use small letters in the enum so end your items with .first, .second etc in the info.plist)

Remove all your app delegate code you had previously for the 3d touch quick actions.

Than create a new .swift file in your project and add this code

There is a few bits which are different in swift 2 and swift 3. I included both so delete the bit that you do not need.

import UIKit

/// Shortcut item delegate
protocol ShortcutItemDelegate: class {
       func shortcutItem1Pressed()
       func shortcutItem2Pressed()
       func shortcutItem3Pressed()
       func shortcutItem4Pressed()
}

 /// Shortcut item identifier
enum ShortcutItemIdentifier: String {
     case first // I use swift 3 small letters so you have to change your spelling in the info.plist 
     case second
     case third
     case fourth

     private init?(fullType: String) {
          guard let last = fullType.componentsSeparatedByString(".").last else { return nil }
          self.init(rawValue: last)
      }

      public var type: String {
           return Bundle.main.bundleIdentifier! + ".\(self.rawValue)"
      }
  }

  /// Shortcut item protocol
  protocol ShortcutItem { } 
  extension ShortcutItem {

  // MARK: - Properties

  /// Delegate
  private weak var delegate: ShortcutItemDelegate? {
        return self as? ShortcutItemDelegate
  }

  // MARK: - Methods

 // Swift 2
func handledShortcutItemPress(forLaunchOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    guard let launchOptions = launchOptions, shortcutItem = launchOptions[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem else { return false }
    handledShortcutItemPress(forItem: shortcutItem)
    return true
}

 /// Swift 3
 func handledShortcutItemPress(forLaunchOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    guard let launchOptions = launchOptions, shortcutItem = launchOptions[UIApplicationLaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem else { return false }
    handledShortcutItemPress(forItem: shortcutItem)
    return true
}


/// Handle item press
func handledShortcutItemPress(forItem shortcutItem: UIApplicationShortcutItem) -> Bool {
    guard let _ = ShortcutItemIdentifier(fullType: shortcutItem.type) else { return false }
    guard let shortcutType = shortcutItem.type as String? else { return false }

    switch shortcutType {

    case ShortcutItemIdentifier.first.type:
        delegate?.shortcutItem1Pressed()

    case ShortcutItemIdentifier.second.type:
        delegate?.shortcutItem2Pressed()

    case ShortcutItemIdentifier.third.type:
        delegate?.shortcutItem3Pressed()

    case ShortcutItemIdentifier.fourth.type:
        delegate?.shortcutItem4Pressed()

    default:
        return false
    }

    return true
   }
}

Than in your app delegate create an extension with this method (you missed this in your code)

extension AppDelegate: ShortcutItem {

   /// Perform action for shortcut item. This gets called when app is active

   // swift 2
   func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
        completionHandler(handledShortcutItemPress(forItem: shortcutItem))
   }

   // swift 3
   func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
        completionHandler(handledShortcutItemPress(forItem: shortcutItem))
   }

Than you need to adjust the didFinish method in your AppDelegate to look like this

  /// Swift 2
  func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.

    ...

    guard !handledShortcutItemPress(forLaunchOptions: launchOptions) else { return false } // ADD THIS LINE

    return true
}

  /// Swift 3
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.

    ...

    guard !handledShortcutItemPress(forLaunchOptions: launchOptions) else { return false } // ADD THIS LINE

    return true
}

And than finally create another extension confirming to the ShortcutItem delegate

    extension AppDelegate: ShortcutItemDelegate {

func shortcutItem1Pressed() {
    NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(loadGameScene), userInfo: nil, repeats: false)
}
@objc private func loadGameScene() {
    let scene = GameScene(size: CGSize(width: 1024, height: 768))
    loadScene(scene, view: window?.rootViewController?.view)
}

func shortcutItem2Pressed() {
    // do something else
}

func shortcutItem3Pressed() {
     // do something else
}

func shortcutItem4Pressed() {
    // do something else
}

 func loadScene(scene: SKScene?, view: UIView?, scaleMode: SKSceneScaleMode = .aspectFill) {
    guard let scene = scene else { return }
    guard let skView = view as? SKView else { return }

    skView.ignoresSiblingOrder = true
    #if os(iOS)
        skView.isMultipleTouchEnabled = true
    #endif
    scene.scaleMode = scaleMode
    skView.presentScene(scene)
    }
}

The load scene method I normally have in another helper which is why I pass the view into the func.

Hope this helps.

Comments