I am working on an interactive, animated scene. I want all touches on the scene to be disabled on entry. Then, once the objects (which are subclassed nodes) in the scene finish rotating/moving, I want to re-enable all touches on the screen to allow interaction. I have disabled user interaction using this code:
override func didMove(to view: SKView) {
setupNodes()
view?.isUserInteractionEnabled = false
spinLocations()
}
This is the code, within the scene file, for spinLocations:
func spinLocations() {
var allLocationArrays = [[String : CGPoint]]()
var previousArray = hiddenLocationPositions
for _ in 0...SearchConstant.numSpins {
let freshArray = generateNewLocationArray(previous: previousArray)
allLocationArrays.append(freshArray)
previousArray = freshArray
}
for (item, _) in hiddenLocationPositions {
let node = fgNode.childNode(withName: item) as! LocationNode
node.spin(position: allLocationArrays) // this is function below
}
hiddenLocationPositions = previousArray
}
This is the code for the animations in the node class:
func spin(position: [[String : CGPoint]]) {
var allActions = [SKAction]()
for array in position {
let action = SKAction.move(to: array[self.name!]!, duration: 2.0)
allActions.append(action)
}
let allActionsSeq = SKAction.sequence(allActions)
self.run(SKAction.sequence([SKAction.wait(forDuration: 5.0), allActionsSeq, SKAction.run {
self.position = position[position.count - 1][self.name!]!
},]))
}
This is the code for passing back the touches to the main scene from this class:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let parent = self.parent else { return }
}
As you can see, touch is not disabled here.
I do not want to add a "waitForDuration" SKAction to the runBlock to change the view status after the previous action; I want the program to determine when the animations are finished executing and then re-enable touches.
In order to do this, I theorised using a completion handler might work, but it only re-enables touches immediately (e.g. handling a handler to spin causes the touches to be detected again). Previously, I also tried to disable the view in the runBlock, but of course, that is run instantaneously. How do I ensure that the touches are re-detected following the animation without using "waitForDuration."?
So, this is a simple example that shows how you can:
1) Disable touches completely
2) Spin a node
3) When node is done with spinning, to enable touches
Here is the code (you can copy/paste it to try how it works):
class Object:SKSpriteNode{
func spin(times:Int,completion:#escaping ()->()) {
let duration = 3.0
let angle = CGFloat(M_PI) * 2.0
let oneRevolution = SKAction.rotate(byAngle: angle , duration: duration)
let spin = SKAction.repeat(oneRevolution, count: times)
let sequence = SKAction.sequence([spin,SKAction.run(completion)])
run(sequence, withKey:"spinning")
}
}
class WelcomeScene: SKScene {
override func didMove(to view: SKView) {
view.isUserInteractionEnabled = false
print("Touches Disabled")
let object = Object(texture: nil, color: .purple, size: CGSize(width: 200, height: 200))
addChild(object)
object.spin(times: 3, completion: {[weak self] in
self?.view?.isUserInteractionEnabled = true
print("Touches Enabled")
})
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touch detected")
}
deinit {
print("Welcome scene deinited")
}
}
Here, you disable touches when scene is loaded, start spinning the object, and you pass a completion block to it... That block of code is used here:
let sequence = SKAction.sequence([spin,SKAction.run(completion)])
So after spinning, that block will be executed. Now, there are different ways to do this...Personally, I would use delegation, but I thought this can be less confusing... I can write an example for delegation too if needed, but basically, what you would do, is to set a scene as a delegate of your custom node, and notify it about spinning is done, so the scene can tell the view to re-enable the touches.
Related
I'm working on a keyboard extension for iOS. However, I'm having some weird issues with animations / layers not appearing instantly on the far left of the screen. I use layers / animations to show a "tool tip" when the user presses a key. For all keys except A and Q the tool tips are displayed instantly, but for these two keys there seems to be a slight delay before the layer and animation appears. This only happens on touch down, if I slide into the Q or A hit area the tool tips gets rendered instantly. My debugging shows that the code executes exactly the same for all keys, but for these two keys it has no immediate effect.
Any ideas on if there's anything special with the left edge of the screen that might cause this behaviour? Or am I doing something stupid that might be the cause of this?
This is part of my touch handling code that triggers the tool tip rendering:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if(!shouldIgnoreTouches()) {
for touch in touches {
let location = (touch ).locationInView(self.inputView)
// pass coordinates to offset service to find candidate keys
let keyArray = keyOffsetService.getKeys(_keyboardLayout!, location: location)
let primaryKey = keyArray[0]
if primaryKey.alphaNumericKey != nil {
let layers = findLayers(touch )
if layers.keyLayer != nil {
graphicsService.animateKeyDown(layers.keyLayer as! CATextLayer, shieldLayer: layers.shieldLayer)
_shieldsUp.append((textLayer:layers.keyLayer, shieldLayer:layers.shieldLayer))
}
}
}
}
}
animation code:
func animateKeyDown(layer:CATextLayer, shieldLayer:CALayer?) {
if let sLayer = shieldLayer {
keyDownShields(layer, shieldLayer: sLayer)
CATransaction.begin()
CATransaction.setDisableActions(true)
let fontSizeAnim = CABasicAnimation(keyPath: "fontSize")
fontSizeAnim.removedOnCompletion = true
fontSizeAnim.fromValue = layer.fontSize
fontSizeAnim.toValue = layer.fontSize * 0.9
layer.fontSize = layer.fontSize * 0.9
let animation = CABasicAnimation(keyPath: "opacity")
animation.removedOnCompletion = true
animation.fromValue = layer.opacity
animation.toValue = 0.3
layer.opacity = 0.3
let animGroup = CAAnimationGroup()
animGroup.animations = [fontSizeAnim, animation]
animGroup.duration = 0.01
layer.addAnimation(animGroup, forKey: "down")
CATransaction.commit()
}
}
unhide tooltip layer:
private func keyDownShields(layer:CATextLayer, shieldLayer:CALayer) {
shieldLayer.hidden = false
shieldLayer.setValue(true, forKey: "isUp")
shieldLayer.zPosition = 1
shieldLayer.removeAllAnimations()
layer.setValue(true, forKey: "isUp")
}
This is caused by a feature in iOS 9 which allows the user to switch apps by force pressing the left edge of the screen while swiping right.
You can turn this off by disabling 3D touch but this is hardly a solution.
I am not aware of any API that allows you to override this behavior.
The official solution is overriding preferredScreenEdgesDeferringSystemGestures of your UIInputViewController.
https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys
However, it doesn't seem to work on iOS 13 at least. As far as I understand, that happens due to preferredScreenEdgesDeferringSystemGestures not working properly when overridden inside UIInputViewController, at least on iOS 13.
When you override this property in a regular view controller, it works as expected:
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
return [.left, .bottom, .right]
}
That' not the case for UIInputViewController, though.
UPD: It appears, gesture recognizers will still get .began state update, without the delay. So, instead of following the rather messy solution below, you can add a custom gesture recognizer to handle touch events.
You can quickly test this adding UILongPressGestureRecognizer with minimumPressDuration = 0 to your control view.
Another solution:
My original workaround was calling touch down effects inside hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?, which is called even when the touches are delayed for the view.
You have to ignore the "real" touch down event, when it fires about 0.4s later or simultaneously with touch up inside event. Also, it's probably better to apply this hack only in case the tested point is inside ~20pt lateral margins.
So for example, for a view with equal to screen width, the implementation may look like:
let edgeProtectedZoneWidth: CGFloat = 20
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
guard result == self else {
return result
}
if point.x < edgeProtectedZoneWidth || point.x > bounds.width-edgeProtectedZoneWidth
{
if !alreadyTriggeredFocus {
isHighlighted = true
}
triggerFocus()
}
return result
}
private var alreadyTriggeredFocus: Bool = false
#objc override func triggerFocus() {
guard !alreadyTriggeredFocus else { return }
super.triggerFocus()
alreadyTriggeredFocus = true
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
alreadyTriggeredFocus = false
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
alreadyTriggeredFocus = false
}
...where triggerFocus() is the method you call on touch down event. Alternatively, you may override touchesBegan(_:with:).
I am creating a game ad i am having a hard time creating a jump button. I have created the jump up and fall down SKaction sequence which works perfect here is how it works.
func JumpArrow () {
self.addChild(jumpArrow)
jumpArrow.position = CGPointMake(60, 145)
jumpArrow.xScale = 1
jumpArrow.yScale = 1
}
func heroJumpMovement () {
let heroJumpAction = SKAction.moveToY(hero.position.y + 85,
duration: 0.5)
let heroFallAction = SKAction.moveToY(hero.position.y , duration:
0.5)
let jumpWait:SKAction = SKAction.waitForDuration(CFTimeInterval(1))
let heroMovementSequence:SKAction =
SKAction.sequence([heroJumpAction, heroFallAction ,jumpWait])
hero.runAction(heroMovementSequence)
}
override func touchesBegan(touches: Set<UITouch>, withEvent
event:UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
let node = nodeAtPoint(location)
if node == jumpArrow {
heroJumpMovement()
}
however, I have a problem. if you quickly tap the button the player will fly off the screen. I hope that i can create a UItapGestureRecognizer and create a delay for the tap so you can't tap the button 2-4 times per second you will only be able to tap it once. If this is the wrong way to go about this please tell me
Adding a delay would be the wrong way to go about it.
Instead, in your touchesBegan function, before you call heroJumpMovement() you should check to see if your player is on the ground or not.
Another alternative would be to check if the last jump SKActionSequence has completed or not.
To do the above, you would have something like this (code not checked):
var canJump = true; // Variable will be true if we can jump
func JumpArrow () {
self.addChild(jumpArrow)
jumpArrow.position = CGPointMake(60, 145)
jumpArrow.xScale = 1
jumpArrow.yScale = 1
}
func heroJumpMovement () {
let heroJumpAction = SKAction.moveToY(hero.position.y + 85,
duration: 0.5)
let heroFallAction = SKAction.moveToY(hero.position.y , duration:
0.5)
let jumpWait:SKAction = SKAction.waitForDuration(CFTimeInterval(1))
let heroMovementSequence:SKAction =
SKAction.sequence([heroJumpAction, heroFallAction ,jumpWait])
canJump = false; // We are about to jump so set this to false
hero.runAction(heroMovementSequence, completion: {canJump = true;}) // Set the canJump variable back to true after we have landed
}
override func touchesBegan(touches: Set<UITouch>, withEvent
event:UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
let node = nodeAtPoint(location)
if node == jumpArrow {
if (canJump) { // Make sure we are allowed to jump
heroJumpMovement()
}
}
Notice the canJump variable.
In my SpriteKit Game i'm using:
self.scene!.removeFromParent()
let skView = self.view! as SKView
skView.ignoresSiblingOrder = true
var scene: PlayScene!
scene = PlayScene(size: skView.bounds.size)
scene.scaleMode = .AspectFill
skView.presentScene(scene, transition: SKTransition.fadeWithColor(SKColor(red: 25.0/255.0, green: 55.0/255.0, blue: 12.0/255.0, alpha: 1), duration: 1.0))
to move from one scene to another. But how can I go back to the original scene? Using the same principle of code always led to a major crash..
I made an example where global structure is used to track the info about previousScene. It can be done with a custom property as well, or by using userData which every node has. The logic is the same. Also, I've removed debugging code (debug label code etc.) because it is not important for everything to work.
Example might be better if I added a few buttons where each links to the certain scene, but I left just one button to keep everything short as possible.
What you need to know about this example (you will change this rules according to your game, but the logic is the same - set the previousScene before an actual transition):
there are three scenes, WelcomeScene (default one), MenuScene and a GameScene.
tapping on the black button takes you to the GameScene. There is an exception to this rule when current scene is a GameScene. In that case, transition will take you to the previousScene.
tapping anywhere around the black button will take you to the previous scene. There is an exception to this rule when WelcomeScene is loaded for the first time (previousScene is not set) and a transition will take you to the MenuScene in that case.
-in your GameViewController you should set up a WelcomeScene to be a default one. Otherwise, you should change a code a bit to handle situations what happening when previousScene is not set (like I did in touchesBegan of WelcomeScene).
So those are rules I've made, just in order to make all those transitions a bit more meaningful...
Here is the code (BaseScene.swift):
import SpriteKit
enum SceneType: Int {
case WelcomeScene = 0
case MenuScene //1
case GameScene //2
}
struct GlobalData
{
static var previousScene:SceneType?
//Other global data...
}
class BaseScene:SKScene {
let button = SKSpriteNode(color: SKColor.blackColor(), size: CGSize(width: 50, height: 50))
override func didMoveToView(view: SKView) {
setupButton()
}
private func setupButton(){
if (button.parent == nil){
//Just setup button properties like position, zPosition and name
button.name = "goToGameScene"
button.zPosition = 1
button.position = CGPoint(x: CGRectGetMidX(frame), y: 100)
addChild(button)
}
}
func goToScene(newScene: SceneType){
var sceneToLoad:SKScene?
switch newScene {
case SceneType.GameScene:
sceneToLoad = GameScene(fileNamed: "GameScene")
case SceneType.MenuScene:
sceneToLoad = MenuScene(fileNamed: "MenuScene")
case SceneType.WelcomeScene:
sceneToLoad = WelcomeScene(fileNamed:"WelcomeScene")
}
if let scene = sceneToLoad {
scene.size = size
scene.scaleMode = scaleMode
let transition = SKTransition.fadeWithDuration(3)
self.view?.presentScene(scene, transition: transition)
}
}
}
Every scene (WelcomeScene, MenuScene, GameScene) inherits from a BaseScene class (which is subclass of a SKScene). I guess, there is no need to explain that, but feel free to ask if something confuses you. The important method here (which is used by every subclass) is goToScene(scene:SceneType) and its parameter (of type SceneType) which tells us what type of scene a method should load.
SceneType is just an enum which holds integers...So actually we are not working with objects here, thus there is no fear of strong reference cycles.
Next, there are other scenes (WelcomeScene.swift):
import SpriteKit
class WelcomeScene:BaseScene {
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
self.backgroundColor = SKColor.darkGrayColor()
}
deinit {print ("WelcomeScene deinited")}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
if let location = touch?.locationInNode(self){
//Give a priority to a button - if button is tapped go to GameScene
let node = nodeAtPoint(location)
if node.name == "goToGameScene"{
GlobalData.previousScene = SceneType.MenuScene
goToScene(SceneType.GameScene)
}else{
//Otherwise, do a transition to the previous scene
//Get the previous scene
if let previousScene = GlobalData.previousScene {
GlobalData.previousScene = SceneType.WelcomeScene
goToScene(previousScene)
}else{
// There is no previousScene set yet? Go to MenuScene then...
GlobalData.previousScene = SceneType.WelcomeScene
goToScene(SceneType.MenuScene)
}
}
}
}
}
To keep short as possible, everything is commented. Next code (MenuScene.swift):
import SpriteKit
class MenuScene: BaseScene {
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
backgroundColor = SKColor.purpleColor()
}
deinit {
print ("MenuScene deinited") //If this method isn't called, you might have problems with strong reference cycles.
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first
if let location = touch?.locationInNode(self){
//Give a priority to a button - if button is tapped go to GameScene
let node = nodeAtPoint(location)
if node.name == "goToGameScene"{
GlobalData.previousScene = SceneType.MenuScene
goToScene(SceneType.GameScene)
}else{
//Otherwise, do a transition to the previous scene
//Get the previous scene
if let previousScene = GlobalData.previousScene {
GlobalData.previousScene = SceneType.MenuScene
goToScene(previousScene)
}
}
}
}
}
And for the end (GameScene.swift):
import SpriteKit
class GameScene: BaseScene{
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
self.backgroundColor = SKColor.orangeColor()
}
deinit {print ("GameScene deinited")}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
//Here, we ignore black button because we don't want to transition to the same scene
if let previousScene = GlobalData.previousScene {
GlobalData.previousScene = SceneType.GameScene
goToScene(previousScene)
}
}
}
Preview:
Just read again the rules from the beginning and you will be fine (eg. in GameScene black button doesn't work, or on first launch previousScene is not set , so you will be transitioned to the MenuScene by default).
That would be it. Hope this helps a bit. You can copy and paste the code to test it and improve it to your needs. Still, not sure that you really need this. It looks that you just need to correctly transition between scenes.
HINT: What is important here is that every scene BaseScene, WelcomeScene... has it own .sks file. You create those from File->New->File->Resource and name it appropriately (like BaseClass.sks, WelcomeScene.sks...) Also, it is your job to maintain the state of GlobalData.previousScene variable (eg. set it before the transition is made).
You would need to create a property in your new scene that stores the previous one, something like previousScene. Then you can set it like this: scene.previousScene = self.scene. In you new scene, you can now go back to the previous scene with skView.presentScene(previousScene)
And I'd advise against naming the new scene you are going to present scene because your current scene is also named scene, so if you accidentally forget the self in self.scene then that may cause a lot of confusion. I'd name it something like newScene or sceneToPresent.
Also, your first line, self.scene!.removeFromParent(), isn't necessary. You don't need to remove the current scene before presenting a new one.
I'm looking to create a shop in my game (In SpriteKit) with buttons and images, but I need the items to be scrollable so the player can scroll up and down the shop (Like a UITableView but with multiple SKSpriteNodes and SKLabelNodes in each cell). Any idea how I can do this in SpriteKit?
The second answer as promised, I just figured out the issue.
I recommend to always get the latest version of this code from my gitHub project incase I made changes since this answer, link is at the bottom.
Step 1: Create a new swift file and paste in this code
import SpriteKit
/// Scroll direction
enum ScrollDirection {
case vertical // cases start with small letters as I am following Swift 3 guildlines.
case horizontal
}
class CustomScrollView: UIScrollView {
// MARK: - Static Properties
/// Touches allowed
static var disabledTouches = false
/// Scroll view
private static var scrollView: UIScrollView!
// MARK: - Properties
/// Current scene
private let currentScene: SKScene
/// Moveable node
private let moveableNode: SKNode
/// Scroll direction
private let scrollDirection: ScrollDirection
/// Touched nodes
private var nodesTouched = [AnyObject]()
// MARK: - Init
init(frame: CGRect, scene: SKScene, moveableNode: SKNode) {
self.currentScene = scene
self.moveableNode = moveableNode
self.scrollDirection = scrollDirection
super.init(frame: frame)
CustomScrollView.scrollView = self
self.frame = frame
delegate = self
indicatorStyle = .White
scrollEnabled = true
userInteractionEnabled = true
//canCancelContentTouches = false
//self.minimumZoomScale = 1
//self.maximumZoomScale = 3
if scrollDirection == .horizontal {
let flip = CGAffineTransformMakeScale(-1,-1)
transform = flip
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Touches
extension CustomScrollView {
/// Began
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(currentScene)
guard !CustomScrollView.disabledTouches else { return }
/// Call touches began in current scene
currentScene.touchesBegan(touches, withEvent: event)
/// Call touches began in all touched nodes in the current scene
nodesTouched = currentScene.nodesAtPoint(location)
for node in nodesTouched {
node.touchesBegan(touches, withEvent: event)
}
}
}
/// Moved
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(currentScene)
guard !CustomScrollView.disabledTouches else { return }
/// Call touches moved in current scene
currentScene.touchesMoved(touches, withEvent: event)
/// Call touches moved in all touched nodes in the current scene
nodesTouched = currentScene.nodesAtPoint(location)
for node in nodesTouched {
node.touchesMoved(touches, withEvent: event)
}
}
}
/// Ended
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(currentScene)
guard !CustomScrollView.disabledTouches else { return }
/// Call touches ended in current scene
currentScene.touchesEnded(touches, withEvent: event)
/// Call touches ended in all touched nodes in the current scene
nodesTouched = currentScene.nodesAtPoint(location)
for node in nodesTouched {
node.touchesEnded(touches, withEvent: event)
}
}
}
/// Cancelled
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
for touch in touches! {
let location = touch.locationInNode(currentScene)
guard !CustomScrollView.disabledTouches else { return }
/// Call touches cancelled in current scene
currentScene.touchesCancelled(touches, withEvent: event)
/// Call touches cancelled in all touched nodes in the current scene
nodesTouched = currentScene.nodesAtPoint(location)
for node in nodesTouched {
node.touchesCancelled(touches, withEvent: event)
}
}
}
}
// MARK: - Touch Controls
extension CustomScrollView {
/// Disable
class func disable() {
CustomScrollView.scrollView?.userInteractionEnabled = false
CustomScrollView.disabledTouches = true
}
/// Enable
class func enable() {
CustomScrollView.scrollView?.userInteractionEnabled = true
CustomScrollView.disabledTouches = false
}
}
// MARK: - Delegates
extension CustomScrollView: UIScrollViewDelegate {
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollDirection == .horizontal {
moveableNode.position.x = scrollView.contentOffset.x
} else {
moveableNode.position.y = scrollView.contentOffset.y
}
}
}
This make a subclass of UIScrollView and sets up the basic properties of it. It than has its own touches method which get passed along to the relevant scene.
Step2: In your relevant scene you want to use it you create a scroll view and moveable node property like so
weak var scrollView: CustomScrollView!
let moveableNode = SKNode()
and add them to the scene in didMoveToView
scrollView = CustomScrollView(frame: CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height), scene: self, moveableNode: moveableNode, scrollDirection: .vertical)
scrollView.contentSize = CGSizeMake(self.frame.size.width, self.frame.size.height * 2)
view?.addSubview(scrollView)
addChild(moveableNode)
What you do here in line 1 is you init the scroll view helper with you scene dimensions. You also pass along the scene for reference and the moveableNode you created at step 2.
Line 2 is where you set up the content size of the scrollView, in this case its twice as long as the screen height.
Step3: - Add you labels or nodes etc and position them.
label1.position.y = CGRectGetMidY(self.frame) - self.frame.size.height
moveableNode.addChild(label1)
in this example the label would be on the 2nd page in the scrollView. This is where you have to play around with you labels and positioning.
I recommend that if you have a lot pages in the scroll view and a lot of labels to do the following. Create a SKSpriteNode for each page in the scroll view and make each of them the size of the screen. Call them like page1Node, page2Node etc. You than add all the labels you want for example on the second page to page2Node. The benefit here is that you basically can position all your stuff as usual within page2Node and than just position page2Node in the scrollView.
You are also in luck because using the scrollView vertically (which u said you want) you dont need to do any flipping and reverse positioning.
I made some class func so if you need to disable your scrollView incase you overlay another menu ontop of the scrollView.
CustomScrollView.enable()
CustomScrollView.disable()
And finally do not forget to remove the scroll view from your scene before transitioning to a new one. One of the pains when dealing with UIKit in spritekit.
scrollView?.removeFromSuperView()
For horizontal scrolling simply change the scroll direction on the init method to .horizontal (step 2).
And now the biggest pain is that everything is in reverse when positioning stuff. So the scroll view goes from right to left. So you need to use the scrollView "contentOffset" method to reposition it and basically place all your labels in reverse order from right to left. Using SkNodes again makes this much easier once you understand whats happening.
Hope this helps and sorry for the massive post but as I said it is a bit of a pain in spritekit. Let me know how it goes and if I missed anything.
Project is on gitHub
https://github.com/crashoverride777/SwiftySKScrollView
You have 2 options
1) Use a UIScrollView
Down the road this is the better solution as you get things such as momentum scrolling, paging, bounce effects etc for free. However you have to either use a lot of UIKit stuff or do some sub classing to make it work with SKSpritenodes or labels.
Check my project on gitHub for an example
https://github.com/crashoverride777/SwiftySKScrollView
2) Use SpriteKit
Declare 3 class variables outside of functions(under where it says 'classname': SKScene):
var startY: CGFloat = 0.0
var lastY: CGFloat = 0.0
var moveableArea = SKNode()
Set up your didMoveToView, add the SKNode to the scene and add 2 labels, one for the top and one for the bottom to see it working!
override func didMoveToView(view: SKView) {
// set position & add scrolling/moveable node to screen
moveableArea.position = CGPointMake(0, 0)
self.addChild(moveableArea)
// Create Label node and add it to the scrolling node to see it
let top = SKLabelNode(fontNamed: "Avenir-Black")
top.text = "Top"
top.fontSize = CGRectGetMaxY(self.frame)/15
top.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMaxY(self.frame)*0.9)
moveableArea.addChild(top)
let bottom = SKLabelNode(fontNamed: "Avenir-Black")
bottom.text = "Bottom"
bottom.fontSize = CGRectGetMaxY(self.frame)/20
bottom.position = CGPoint(x:CGRectGetMidX(self.frame), y:0-CGRectGetMaxY(self.frame)*0.5)
moveableArea.addChild(bottom)
}
Then set up your touches began to store position of your first touch:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
// store the starting position of the touch
let touch: AnyObject? = touches.anyObject();
let location = touch?.locationInNode(self)
startY = location!.y
lastY = location!.y
}
Then set up touches moved with the following code to scroll the node by to the limits set, at the speed set:
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
let touch: AnyObject? = touches.anyObject();
let location = touch?.locationInNode(self)
// set the new location of touch
var currentY = location!.y
// Set Top and Bottom scroll distances, measured in screenlengths
var topLimit:CGFloat = 0.0
var bottomLimit:CGFloat = 0.6
// Set scrolling speed - Higher number is faster speed
var scrollSpeed:CGFloat = 1.0
// calculate distance moved since last touch registered and add it to current position
var newY = moveableArea.position.y + ((currentY - lastY)*scrollSpeed)
// perform checks to see if new position will be over the limits, otherwise set as new position
if newY < self.size.height*(-topLimit) {
moveableArea.position = CGPointMake(moveableArea.position.x, self.size.height*(-topLimit))
}
else if newY > self.size.height*bottomLimit {
moveableArea.position = CGPointMake(moveableArea.position.x, self.size.height*bottomLimit)
}
else {
moveableArea.position = CGPointMake(moveableArea.position.x, newY)
}
// Set new last location for next time
lastY = currentY
}
All credit goes to this article
http://greenwolfdevelopment.blogspot.co.uk/2014/11/scrolling-in-sprite-kit-swift.html
Here's the code we used to simulate UIScrollView behavior for SpriteKit menus.
Basically, you need to use a dummy UIView that matches the height of the SKScene then feed UIScrollView scroll and tap events to the SKScene for processing.
It's frustrating Apple doesn't provide this natively, but hopefully no one else has to waste time rebuilding this functionality!
class ScrollViewController: UIViewController, UIScrollViewDelegate {
// IB Outlets
#IBOutlet weak var scrollView: UIScrollView!
// General Vars
var scene = ScrollScene()
// =======================================================================================================
// MARK: Public Functions
// =======================================================================================================
override func viewDidLoad() {
// Call super
super.viewDidLoad()
// Create scene
scene = ScrollScene()
// Allow other overlays to get presented
definesPresentationContext = true
// Create content view for scrolling since SKViews vanish with height > ~2048
let contentHeight = scene.getScrollHeight()
let contentFrame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: contentHeight)
let contentView = UIView(frame: contentFrame)
contentView.backgroundColor = UIColor.clear
// Create SKView with same frame as <scrollView>, must manually compute because <scrollView> frame not ready at this point
let scrollViewPosY = CGFloat(0)
let scrollViewHeight = UIScreen.main.bounds.size.height - scrollViewPosY
let scrollViewFrame = CGRect(x: 0, y: scrollViewPosY, width: UIScreen.main.bounds.size.width, height: scrollViewHeight)
let skView = SKView(frame: scrollViewFrame)
view.insertSubview(skView, at: 0)
// Configure <scrollView>
scrollView.addSubview(contentView)
scrollView.delegate = self
scrollView.contentSize = contentFrame.size
// Present scene
skView.presentScene(scene)
// Handle taps on <scrollView>
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(scrollViewDidTap))
scrollView.addGestureRecognizer(tapGesture)
}
// =======================================================================================================
// MARK: UIScrollViewDelegate Functions
// =======================================================================================================
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scene.scrollBy(contentOffset: scrollView.contentOffset.y)
}
// =======================================================================================================
// MARK: Gesture Functions
// =======================================================================================================
#objc func scrollViewDidTap(_ sender: UITapGestureRecognizer) {
let scrollViewPoint = sender.location(in: sender.view!)
scene.viewDidTapPoint(viewPoint: scrollViewPoint, contentOffset: scrollView.contentOffset.y)
}
}
class ScrollScene : SKScene {
// Layer Vars
let scrollLayer = SKNode()
// General Vars
var originalPosY = CGFloat(0)
// ================================================================================================
// MARK: Initializers
// ================================================================================================
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// ================================================================================================
// MARK: Public Functions
// ================================================================================================
func scrollBy(contentOffset: CGFloat) {
scrollLayer.position.y = originalPosY + contentOffset
}
func viewDidTapPoint(viewPoint: CGPoint, contentOffset: CGFloat) {
let nodes = getNodesTouchedFromView(point: viewPoint, contentOffset: contentOffset)
}
func getScrollHeight() -> CGFloat {
return scrollLayer.calculateAccumulatedFrame().height
}
fileprivate func getNodesTouchedFromView(point: CGPoint, contentOffset: CGFloat) -> [SKNode] {
var scenePoint = convertPoint(fromView: point)
scenePoint.y += contentOffset
return scrollLayer.nodes(at: scenePoint)
}
}
I like the idea of add a SKCameraNode to scroll my menu-scene. I've founded this article really useful. You just have to change the camera position to move your menu. In Swift 4
var boardCamera = SKCameraNode()
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let previousLocation = touch.previousLocation(in: self)
let deltaY = location.y - previousLocation.y
boardCamera.position.y += deltaY
}
}
So I have a pause button in my game that when you press it, the scene gets paused, and everything but one SKNode (the pause menu) gets blurred out. I'm doing this by creating a SKEffectNode that has a filter, and adding everything but the pause menu to it. It works, but it takes a solid 2 seconds for the blur to appear in the background. The scene pauses as soon as you press the button, but the blur and the pause menu only appear a few seconds later. Any ideas?
Here's the code:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
if (self.nodeAtPoint(location).name == "PauseButton"){
if(!scene!.paused) {
blurScene()
scene!.paused = true
self.addChild(pauseMenu!)
}else {
removeBlur()
scene!.paused = false
pauseMenu!.removeFromParent()
}
}
}
}
func blurScene() {
blurNode = SKEffectNode() //Created in the beginning of the class
let blur = CIFilter(name: "CIGaussianBlur", withInputParameters: ["inputRadius": 15.0])
blurNode!.filter = blur
self.shouldEnableEffects = true
for node in self.children {
node.removeFromParent()
blurNode!.addChild(node as! SKNode)
}
self.addChild(blurNode!)
}
func removeBlur() {
var blurredNodes = [SKNode]()
for node in blurNode!.children {
blurredNodes.append(node as! SKNode)
node.removeFromParent()
}
for node in blurredNodes {
self.addChild(node as SKNode)
}
self.shouldEnableEffects = false
blurNode!.removeFromParent()
}
Try adding the SKEffectNode as root view and add the child nodes to it. Then you can set the blur filter already but
self.shouldEnableEffects = false
when you want to blur simply
self.shouldEnableEffects = true