Search code examples
swiftsprite-kitskscene3dtouchquickaction

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 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() {

        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
    }
}

Solution

  • 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 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

    This is swift 3 code.

    import UIKit
    
    /// Shortcut item delegate
    protocol ShortcutItemDelegate: class {
        func shortcutItemDidPress(_ identifier: ShortcutItemIdentifier)   
    }
    
     /// 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 ?? "NoBundleIDFound") + ".\(rawValue)"
          }
      }
    
      /// Shortcut item protocol
      protocol ShortcutItem { } 
      extension ShortcutItem {
    
      // MARK: - Properties
    
      /// Delegate
      private weak var delegate: ShortcutItemDelegate? {
            return self as? ShortcutItemDelegate
      }
    
      // MARK: - Methods
    
     func didPressShortcutItem(withOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        guard let shortcutItem = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem else { return false }
        didPressShortcutItem(shortcutItem)
        return true
    }
    
    
    /// Handle item press
    @discardableResult
    func didPressShortcutItem(_ shortcutItem: UIApplicationShortcutItem) -> Bool {
        guard let _ = ShortcutItemIdentifier(fullType: shortcutItem.type) else { return false }
    
        switch shortcutItem.type {
    
        case ShortcutItemIdentifier.first.type:
            delegate?.shortcutItemDidPress(.first)
    
        case ShortcutItemIdentifier.second.type:
            delegate?.shortcutItemDidPress(.second)
    
        case ShortcutItemIdentifier.third.type:
            delegate?.shortcutItemDidPress(.third)
    
        case ShortcutItemIdentifier.fourth.type:
            delegate?.shortcutItemDidPress(.fourth)
    
        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
       func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
            completionHandler(didPressShortcutItem(shortcutItem))
       }
    

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

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

    And than finally create another extension confirming to the ShortcutItem delegate

    extension AppDelegate: ShortcutItemDelegate {
    
      func shortcutItemDidPress(_ identifier: ShortcutItemIdentifier) {
    
        switch identifier {
        case .first:
            let scene = GameScene(size: CGSize(width: 1024, height: 768))
            loadScene(scene, view: window?.rootViewController?.view)
        case .second:
             //
        case .third:
            //
        case .fourth:
           //
        }
     }
    
     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.