SceneKit - Setting overlaySKScene changing first responder? - swift

I'm very, very new to SceneKit and SpriteKit. I'm making a macOS game that relies on keyboard input, and overriding the keyDown function. Everything works fine, until I set overlaySKScene to a sort-of HUD scene. It seems once I add the overlay that my keyboard input is getting processed by the SKScene instead.
Here's the code I've tried using to keep key input going to my SCNScene:
if let overlayScene = SKScene(fileNamed: "GameOverlay.sks") {
overlayScene.isUserInteractionEnabled = false
sceneView.overlaySKScene = overlayScene
self.becomeFirstResponder()
}
I've tried using the self.becomeFirstResponder() line in conjunction with overriding first responder functions in a custom SKScene class:
override func resignFirstResponder() -> Bool {
return true
}
override func becomeFirstResponder() -> Bool {
return false
}
But that overlay scene is still the one getting all key input.

Try disabling the userInteraction on your overlay:
let yourGameHudSKScene = .. setup your overlay SKScene
scnView.overlaySKScene = yourGameHudSKScene
yourGameHudSKScene.userInteractionEnabled = false

Related

How to prevent videoGravity changes in AVPlayerViewController

My memory is not working for me right now. I think I remember there was a way to prevent the video interface of AVPlayerViewController (or similar) from having the button which allows user to toggle between videoGravity settings, I think those are basically two;
.resizeAspect
.resizeAspectFill
User can also double tap on screen to toggle between these two.
What I'd like to do is force the video of a AVPlayerViewController to only use . .resizeAspect for .videoGravity. I think I remember there should be a way to do this with an easy boolean toggle somewhere, but cannot find it searching for 15 minutes.
Here's a very dirty way I was able to hide the button. Unfortunately, user can still use gestures (double tap, pinch) to zoom the video. The proper thing to do is a full custom view it seems.
e.g.
extension UIView {
var recursiveSubviews: [UIView] {
return subviews + subviews.flatMap { $0.recursiveSubviews }
}
}
class CustomPlayerViewController: AVPlayerViewController {
private func hideZoom() {
let zoomButton = view.recursiveSubviews.first(where: { $0.accessibilityLabel == "Zoom" })
zoomButton?.superview?.removeFromSuperview()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
hideZoom()
}
}

How to make a NSTextView, that is added programmatically, active?

I am making an app where a user can click anywhere on the window and a NSTextView is added at the mouse location. I have got it working with the below code but I am not able to make it active (in focus) after adding it to the view (parent view). I have to click on the NSTextView to make it active but this is not what I want. I want it to automatically become active when its added to the parent view.
Code in my ViewController to add the NSTextView to its view:
private func addText(at point: NSPoint) {
let textView = MyTextView(frame: NSRect(origin: point, size: CGSize(width: 150.0, height: 40.0)))
view.addSubview(textView)
}
MyTextView class looks like below:
class MyTextView: NSTextView {
override var shouldDrawInsertionPoint: Bool {
true
}
override var canBecomeKeyView: Bool {
true
}
override func viewWillDraw() {
isHorizontallyResizable = true
isVerticallyResizable = true
insertionPointColor = .red
drawsBackground = false
isRichText = false
allowsUndo = true
font = NSFont.systemFont(ofSize: 40.0)
}
}
Also, I want it to lose focus (become inactive) when some other elements (view) are clicked. Right now, once a NSTextView becomes active, it stays active no matter what other elements I click except when I click on an empty space to create yet another NSTextView.
I have gone through the Apple docs multiple times but I think I am missing something. Any help would be much appreciated.
Get the NSWindow instance of the NSViewController's view and call makeFirstResponder passing the text view as parameter.
To lose focus call makeFirstResponder passing nil.

How to disable mouse clicks and mouse drags on a NSView?

I am making a mac app using Swift and this app has a custom view (a class extending NSView and overriding its draw method). Now, I want to disable all mouse clicks and mouse drags on this view and pass them on to the other applications running beneath my application.
I have tried the following ways (gleaned from Apple documentation and other SO questions) to disable clicks on my view and nothing worked for me so far:
1. Overriding hitTest inside my custom View class
override func hitTest(_ point: NSPoint) -> NSView? {
let view = super.hitTest(point)
return view == self ? nil : view
}
2. Overriding acceptsFirstMouse inside my custom View class
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
return false
}
3. Overriding mouseDown in ViewController as well as in my custom View class
override func mouseDown(with event: NSEvent) {
// do nothing
}
4. Overriding mouseDragged in ViewController as well as in my custom View class
override func mouseDragged(with event: NSEvent) {
// do nothing
}
Am I missing something?
This isn't handled at the view level, it's handled at the window level. You can set the ignoresMouseEvents property of the window to true.
The issue is that the Window Server will only dispatch an event to a single process. So, once it has arrived in your app, it's not going to another. And there's no feasible way for your app to forward it along, either.

Disable swipe to close on AVPlayerController

iOS 11 has introduced a swipe to close AVPlayerController. I have app that is aimed at toddlers so the screen is easily swiped causing the video to close. Is there anyway to remove the gesture to close the player?
I have tried adding a gesture override to the AVPlayerController's view but it doesn't work. There is a possible solution on How can I add Swipe Gesture to AVPlayer in swift 3 but there must be a cleaner way
If AVPlayerController is embedded (not presenting) the Close button is not presented in the controls view.
My solution is to find the subview with gesture recognizers and remove pan gesture recognizer
for v in playerViewController.view.subviews {
if v.gestureRecognizers != nil {
for gr in v.gestureRecognizers! {
if gr is UIPanGestureRecognizer {
// remove pan gesture to prevent closing on pan
v.removeGestureRecognizer(gr)
}
}
}
}
I managed to solve issue. As #Vakas commented, the AVPlayerController shouldn't be subclassed. I had originally subclassed it and presented using a modal segue. This was causing the problem.
To solve it, I created another view controller that embeds the AVPlayerController in it.
import UIKit
import AVKit
class PlayerViewController: UIViewController, AVPlayerViewControllerDelegate {
var videoRecord: Video!
var presentingController = ""
var videos = [Video]()
var presentingPlaylist: Playlist?
let playerViewController = TFLPlayerController()
override func viewDidLoad() {
super.viewDidLoad()
playerViewController.delegate = self
playerViewController.videoRecord = videoRecord
playerViewController.videos = self.videos
playerViewController.allowsPictureInPicturePlayback = false
// Add the original AVPlayerController in here
self.addChildViewController(playerViewController)
let playerView = playerViewController.view
playerView?.frame = self.view.bounds
self.view.addSubview(playerView!)
playerViewController.didMove(toParentViewController: self)
}
}
I basically use this View Controller to pass through the properties such as videos, etc, to the originally subclassed AVPlayerController.
None of the comments above solved the issue (iOS 13+). Solution:
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
avPlayerViewController.view.addGestureRecognizer(panGestureRecognizer)
where handlePanGesture(_:) is the method that will be called if a pan happen on the screen (and the video won't move - that was the problem in the question that it was dragged), and avPlayerViewController is the AVPlayerViewController instance.
Note: if you want to prevent the pinch / rotation and any other gesture, you can add for every gesture a new UI...GestureRecognizer. Just make sure, that all UI...GestureRecognizers' delegate is set, and this function is implemented:
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}

TVOS 10 SpriteKit Focus Navigation default focused item

I am trying to update my SpriteKit games to use the new SKNode focus navigation feature, but I am having trouble to change the default focused item.
Essentially I have this code in my button class to support focus navigation
class Button: SKSpriteNode {
var isFocusable = true // easy way to disable focus incase menus are shown etc
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
userInteractionEnabled = true
}
// MARK: - Focus navigation
#if os(tvOS)
extension Button {
/// Can become focused
override var canBecomeFocused: Bool {
return isFocusable
}
/// Did update focus
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if context.previouslyFocusedItem === self {
// some SKAction to reset the button to default settings
}
else if context.nextFocusedItem === self {
// some SKAction to scale the button up
}
}
}
#endif
Everything is working great, however by default the first button on the left side of the screen is focused.
I am trying to change this to another button but I cannot do it. I now you are supposed to use
override var preferredFocusEnvironments: [UIFocusEnvironment]...
// preferredFocusedView is depreciated
but I dont understand how and where to use this.
I tried adding this code to my menu scene to change the focused button from the default (shop button) to the play button thats on the right side of the screen.
class MenuScene: SKScene {
// left side of screen
lazy var shopButton: Button = self.childNode(withName: "shopButton")
// right side of screen
lazy var playButton: Button = self.childNode(withName: "playButton")
// Set preferred focus
open override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [self.playButton]
}
}
and calling
setNeedsFocusUpdate()
updateFocusIfNeeded()
in didMoveToView but it doesn't work.
How can I change my default focused button in SpriteKit?
So I finally got this working thanks to this great article.
https://medium.com/folded-plane/tvos-10-getting-started-with-spritekit-and-focus-engine-53d8ef3b34f3#.pfwcw4u70
The main step I completely missed is that you have tell your GameViewController that your scenes are the preferred focus environment.
This essentially means that your SKScenes will handle the preferred focus instead of GameViewController.
In a SpriteKit game the SKScenes should handle the UI such as buttons using SpriteKit APIs such as SKLabelNodes, SKSpriteNodes etc. Therefore you need to pass the preferred focus to the SKScene.
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// default code to present your 1st SKScene.
}
}
#if os(tvOS)
extension GameViewController {
/// Tell GameViewController that the currently presented SKScene should always be the preferred focus environment
override var preferredFocusEnvironments: [UIFocusEnvironment] {
if let scene = (view as? SKView)?.scene {
return [scene]
}
return []
}
}
#endif
Your buttons should be a subclass of SKSpriteNode that you will use for all your buttons in your game. Use enums and give them different names/ identifiers to distinguish between them when they are pressed (checkout Apples sample game DemoBots).
class Button: SKSpriteNode {
var isFocusable = true // easy way to later turn off focus for your buttons e.g. when overlaying menus etc.
/// Can become focused
override var canBecomeFocused: Bool {
return isFocusable
}
/// Did update focus
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if context.previouslyFocusedItem === self {
// SKAction to reset focus animation for unfocused button
}
if context.nextFocusedItem === self {
// SKAction to run focus animation for focused button
}
}
}
Than in your Start Scene you can set the focus environment to your playButton or any other button.
class StartScene: SKScene {
....
}
#if os(tvOS)
extension StartScene {
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [playButton]
}
}
#endif
If you overlay a menu or other UI in a scene you can do something like this e.g GameScene (move focus to gameMenuNode if needed)
class GameScene: SKScene {
....
}
#if os(tvOS)
extension GameScene {
override var preferredFocusEnvironments: [UIFocusEnvironment] {
if isGameMenuShowing { // add some check like this
return [gameMenuNode]
}
return [] // empty means scene itself
}
}
#endif
You will also have to tell your GameViewController to update its focus environment when you transition between SKScenes (e.g StartScene -> GameScene). This is especially important if you use SKTransitions, it took me a while to figure this out. If you use SKTransitions than the old and new scene are active during the transition, therefore the GameViewController will use the old scenes preferred focus environments instead of the new one which means the new scene will not focus correctly.
I do it like this every time I transition between scenes. You will have to use a slight delay or it will not work correctly.
...
view?.presentScene(newScene, transition: ...)
#if os(tvOS)
newScene.run(SKAction.wait(forDuration: 0.02)) { // wont work without delay
view?.window?.rootViewController?.setNeedsFocusUpdate()
view?.window?.rootViewController?.updateFocusIfNeeded()
}
#endif