0

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
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
Nik
  • 1,664
  • 2
  • 14
  • 27

1 Answers1

1

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.

crashoverride777
  • 10,581
  • 2
  • 32
  • 56
  • Awesome. Sorry for the trouble, it was all a swift 3 syntax thing. Please can you mark my answer. I cant remember why I use a timer. If you dont like the delay try reducing the time or dont use a one at all. – crashoverride777 Aug 20 '16 at 17:45
  • I would redo your other scenes to use AspectFill, even if it means you have to redo a lot of work. Trust me, my 2 games I talked about did use ResizeFill before I changed it. It was a nightmare to make it consistent on all devices. – crashoverride777 Aug 20 '16 at 17:50
  • 1
    You are very welcome. Just to get back to the scene scaling, you will thank my a bunch in the future if you do not use ResizeFill. You can test this very simply. Create a new Xcode Game template. Using the default settings if you run the project the HelloWorld label is perfectly scaled on all iPhones (5, 6, 6Plus). Now go to GameViewController and change the scale mode to ResizeFill and run on all devices. You will notice that it does not look the same anymore on all devices. I used to use a helper to do this manually, remember this also includes things such as physics impulses. It was madness – crashoverride777 Aug 20 '16 at 18:00
  • On iPads it is also perfectly scaled apart from the fact you will have a bit more extra space on the top and bottom (landscape) where you usually just show a bit more background. Im sure you have noticed this in some games you played yourself. Position your stuff from the centre and not the edges. The only thing you might have to do is adjust some hud elements like a pause button. Read this question on how I do that http://stackoverflow.com/questions/34864533/ios-universal-device-app-with-spritekit-how-to-scale-nodes-for-all-views – crashoverride777 Aug 20 '16 at 18:02
  • I dont know what you mean with vector images. It is best to use the Xcode asset catalogue to store your images. There you need 3 universal images, usually png images. e.g 200x200, 400x400 , 600x600 – crashoverride777 Aug 20 '16 at 18:09
  • I dunno about that. I would just create your artwork in highRes, and than export them in the 3 correct resolution as png. Thats the usual way you would do it and how apple does it as well in their sample game DemoBots. – crashoverride777 Aug 20 '16 at 18:11