How to create a vertical scrolling menu in spritekit? - swift

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

Related

Increasing the size of a spriteNode using spriteKit in update function (Swift)

I'm having troubles with animations, physics and stuff like these.
Anyway, what I'm trying to do is creating a pulsing effect for a sphere where the pulse itself is able to collide with elements on the screen, so I created another spriteNode (child of the sphere) that I want to scale continuously, without depending on touches.
I read that in these cases is better not to use SKActions.
My question is: how do I scale or increase the size of a spriteNode via update function?
The code represents how I worked so far for movement for example.
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var entities = [GKEntity]()
var graphs = [String: GKGraph]()
var movingSquareLv1 = SKSpriteNode()
var pulsingSphereLv2 = SKSpriteNode()
var lv2SpherePulse = SKSpriteNode()
var xVeloicty: CGFloat = 100
override func sceneDidLoad() {
movingSquareLv1 = self.childNode(withName: "movingSquare") as!SKSpriteNode
movingSquareLv1.physicsBody = SKPhysicsBody(rectangleOf: movingSquareLv1.size)
movingSquareLv1.physicsBody ? .affectedByGravity = false
pulsingSphereLv2 = self.childNode(withName: "pulsingSphere") as!SKSpriteNode
pulsingSphereLv2.physicsBody ? .affectedByGravity = false
lv2SpherePulse = pulsingSphereLv2.childNode(withName: "lv2SpherePulse") as!SKSpriteNode
lv2SpherePulse.physicsBody ? .affectedByGravity = false
}
func touchDown(atPoint pos: CGPoint) {
}
func touchMoved(toPoint pos: CGPoint) {
}
func touchUp(atPoint pos: CGPoint) {
}
override func touchesBegan(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchDown(atPoint: t.location( in: self))
}
}
override func touchesMoved(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchMoved(toPoint: t.location( in: self))
}
}
override func touchesEnded(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchUp(atPoint: t.location( in: self))
}
}
override func touchesCancelled(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchUp(atPoint: t.location( in: self))
}
}
override func update(_ currentTime: TimeInterval) {
rectMovement()
}
func rectMovement() {
if movingSquareLv1.frame.maxX >= self.size.width / 2 {
xVeloicty = -100
} else if movingSquareLv1.frame.minX <= -self.size.width / 2 {
xVeloicty = 100
}
let rate: CGFloat = 0.5
let relativeVelocity: CGVector = CGVector(dx: xVeloicty - movingSquareLv1.physicsBody!.velocity.dx, dy: 0)
movingSquareLv1.physicsBody!.velocity = CGVector(dx: movingSquareLv1.physicsBody!.velocity.dx + relativeVelocity.dx * rate, dy: 0)
}
}
This is the object composed of two sprites, the one I want to work on is the largest one.
Whoever told you to not use SKActions is wrong. This is exactly when you want to use an SKAction, and if you have multiple circles pulsing, you use the same action on all circles.
You really want to keep your update function as small as possible. Trust in appleā€™s optimizations because they have better access to the API than you do.
I don't know why whatever you read said that actions aren't appropriate, but if you want to change a node's size from update explicitly, then you can set its scaling.
myNode.setScale(2.5) // 2.5x as big as normal
myNode.setScale(0.5) // half size
myNode.xScale = 1.5
myNode.yScale = myNode.xScale * 2 // Set x and y separately for non-uniform scaling
See the documentation in the "Scaling and Rotation" section of SKNode:
https://developer.apple.com/documentation/spritekit/sknode

(Swift) pan & zoom constrained to a certain size image

I am trying to pan and zoom across an image background in spritekit, I have managed to get the zoom working ok and manually entered some restrictions on how far you can pan the image, however the problem is when you pan the screen right to the edge of the image and then zoom out the background shows.
I want the camera to restrict only to the image on screen and not any blank background. Any ideas on how I should do this or any better solutions?
Here is what I got so far
class GameScene:SKScene{
var cam: SKCameraNode!
var scaleNum:CGFloat=1
override func didMove(to view: SKView){
cam=SKCameraNode()
cam.setScale(CGFloat(scaleNum))
self.camera=cam
self.addChild(cam)
let gesture=UIPinchGestureRecognizer(target: self, action: #selector(zoomIn(recognizer:)))
self.view!.addGestureRecognizer(gesture)
}
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed{
cam.setScale(recognizer.scale)
scaleNum=recognizer.scale
if cam.xScale<1 || cam.yScale<1{
cam.setScale(1)
}
if cam.xScale>3 || cam.yScale > 3{
cam.setScale(3)
}
// recognizer.scale=1
test()
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let firstTouch=touches.first
let location=(firstTouch?.location(in: self))!
let previousLocation=firstTouch?.previousLocation(in: self)
cam?.position.x -= location.x - (previousLocation?.x)!
cam?.position.y -= location.y - (previousLocation?.y)!
test()
}
func test(){
if cam.position.x < 1000*scaleNum{
cam.position.x=1000*scaleNum
}
if cam.position.x > 9200*scaleNum{
cam.position.x=9200*scaleNum
}
if cam.position.y<617*scaleNum{
cam.position.y=617*scaleNum
}
if cam.position.y>4476*scaleNum{
cam.position.y=4476*scaleNum
}
}
}
First of all, I would change your zoomIn function to this:
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed {
scaleNum = recognizer.scale
if scaleNum < 1 { scaleNum = 1 }
if scaleNum > 3 { scaleNum = 3 }
cam.setScale(scaleNum)
test()
}
}
It is easier to understand, you're not setting the camera scale twice, and most importantly, when you clamp the camera scale, scaleNum reflects that clamped value. This was not the case before, and in fact, that small change might be your entire problem.
Now I don't have much experience with UIPinchGestureRecognizer but I think the reason your zoom gesture works "ok" is because you are assigning directly from recognizer.scale to cam scale. Correct me if I'm wrong, but I think UIGestureRecognizer always starts with a scale of 1 for each new gesture, but your camera scale maintains its last value.
As an example, imagine your camera is at a scale of 1. A user zooms in to a scale of 2, the scene zooms in perfectly. The user lifts their fingers ending the gesture. Then the user tries to zoom in more, so they begin a new gesture, starting with a scale of 1, but your scene is still at a scale of 2. You can't assign the gesture scale directly or the image scale will 'jump' back to 1 for each new gesture. You have to convert from the gesture scale space to the camera scale space.
How exactly you do this is a design and feel choice. With no experience, my advice would be to change the line in my zoomIn function from
`scaleNum = recognizer.scale'
to
`scaleNum *= recognizer.scale`
Try both versions, and let me know how they work. If there is still a problem, then it most likely resides in your test() function. If so, I will try and help out with that as needed.
Thanks for the answer above, I managed to get it working, code below. Still needs a bit of tweaking but you can pan and zoom anywhere on the background image but the view should be constrained within the background image and not move into empty space beyond the image
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var cam: SKCameraNode!
var scaleNum: CGFloat=1
var background: SKSpriteNode!
var playableRect: CGRect!
override func didMove(to view: SKView) {
background=self.childNode(withName: "clouds") as! SKSpriteNode
cam=SKCameraNode()
cam.setScale(CGFloat(scaleNum))
self.camera=cam
self.addChild(cam)
self.isUserInteractionEnabled=true
let gesture=UIPinchGestureRecognizer(target: self, action: #selector(zoomIn(recognizer:)))
self.view!.addGestureRecognizer(gesture)
let maxAspectRatio:CGFloat=16.0/9.0
let playableHeight=size.width/maxAspectRatio
let playableMargin=(size.height-playableHeight)/2.0
playableRect=CGRect(x:0, y: playableMargin, width: size.width, height: playableHeight)
}
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed{
let savedScale=scaleNum
scaleNum=recognizer.scale
if scaleNum<1{
scaleNum=1
}
else if scaleNum>3{
scaleNum=3
}
if testcamera(posX: cam.position.x, posY: cam.position.y){
cam.setScale(scaleNum)
}
else{
scaleNum=savedScale
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let firstTouch=touches.first
let location=(firstTouch?.location(in: self))!
var posX=cam.position.x
var posY=cam.position.y
let previousLocation=firstTouch?.previousLocation(in: self)
posX -= location.x - (previousLocation?.x)!
posY -= location.y - (previousLocation?.y)!
if testcamera(posX: posX, posY: posY){
cam.position.x=posX
cam.position.y=posY
}
}
func testcamera(posX: CGFloat, posY: CGFloat)->Bool{
var cameraRect : CGRect {
let xx = posX - size.width/2*scaleNum
let yy = posY - playableRect.height/2*scaleNum
return CGRect(x: xx, y: yy, width: size.width*scaleNum, height: playableRect.height*scaleNum)
}
let backGroundRect=CGRect(x: background.position.x-background.frame.width/2, y: background.position.y-background.frame.height/2, width: background.frame.width, height: background.frame.height)
return backGroundRect.contains(cameraRect)
}
}

Cannot disable, then reenable touch, after an SKAction animation

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.

Swift SKSpritenodes in a UIScrollView

Okay I'm trying to add a SKSpriteNode to an UIScrollView, how can I do that?
What have you tried so far?.
As a general stack overflow rule you should always post code if you expect help.
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()
How to create a vertical scrolling menu in spritekit?
https://github.com/crashoverride777/SwiftySKScrollView
You are running into a problem I struggled with for a while. The fundamental problem is that you cannot add SKNodes to a UIKit view, and you cannot add UIKit views to SKNodes. Views are not nodes, and nodes are not views, sadly this will not work. While you can add UIKit elements to the SKScene's view property, that is as far as you can take it. I struggled with this for quite a while before I finally got it.
I still mix UIKit and SpriteKit, btw, but I do so in a limited fashion and it never relies on adding nodes to views or vice versa.
Your best bet is to create an SKNode of some sort that is as big as you need it to be to hold all of your SKSpriteNodes and then adjust its position on screen during the touchesMoved: function. (Essentially create your own SpriteKit based scroll view).
You can use an SKCameraNode and a scene that's bigger than your viewport. That's basically the SpriteKit version of a scroll view.

Sprite Kit: create node only once for all scenes

Normally in a sprite kit game, when a new scene presented, all the nodes in the old scene and their content removed automatically. Now what is, if a node like "HUD" should be not removed? Is there any way in sprite kit to create a node only once and use it in all scenes without removing and creating it again and again every time in every new scene? There must be a technique that makes it possible. that's a serious sprite kit design problem, if it is not possible. But I don't think so. The singleton technique is working great with an audio player, that created only once and used in all scenes. There is surley a way to create a node only once and use it in all scenes. Thanks for any idea.
You can't create a node that persists between scenes. Once you present a new scene, you would need to add the nodes to this new scene.
For this reason, I do not use SKScenes the way Apple describes in the documentation because of this issue. Not only is it cumbersome to have to add the nodes to the new scene each time but also extremely inefficient for nodes like background nodes that should always be present.
So what I did is create 2 scenes, one for the game scene and one for the menu (GUI).
For the menu scene I subclass SKNodes for my interface and then use SKActions on these nodes to present and dismiss them on the screen so it feels like the user is transitioning between scenes. This gives you total customization because you can present multiple nodes, you can keep nodes on the screen permanently etc.
By subclassing the SKNodes you can organize your code just as you did for the scenes. Each node will represent a "scene" in your App. Then you just need to write a method to present and dismiss these nodes.
I've added some sample code below to show one implementation of using SKNodes as "Scenes." The sample code has a base class called SceneNode which we subclass (just as you would subclass an SKScene). In this implementation, I use the GameScene to handle all transitions between scene nodes*. I also keep track of the current scene node so that I can update its layout in case the scene changes size (such as rotation or window resize on OS X**). Your game might not need this, but it's a great way to dynamically layout your nodes. Anything that you want to add to the background or keep around, simply add it to the GameScene. Anything that you want to add to a scene, simply subclass a SceneNode, transition to it and your good to go.
*You could easily present scene nodes directly from other scene nodes instead of going through the GameScene. However I have found that using the GameScene to handle transitions between nodes works very well, especially when you have many scenes with complex transitions.
**There is a bug on OS X, resizing the window does not call the scene's didChangeSize. You need to manually call it.
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = GameScene(size:self.view.bounds.size)
scene.scaleMode = .ResizeFill
(self.view as! SKView).presentScene(scene)
}
}
class GameScene: SKScene {
var currentSceneNode: SceneNode!
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor.whiteColor()
transitionToScene(.Menu)
}
override func didChangeSize(oldSize: CGSize) {
currentSceneNode?.layout()
}
func transitionToScene(sceneType: SceneTransition) {
switch sceneType {
case .Menu:
currentSceneNode?.dismissWithAnimation(.Right)
currentSceneNode = MenuSceneNode(gameScene: self)
currentSceneNode.presentWithAnimation(.Right)
case .Scores:
currentSceneNode?.dismissWithAnimation(.Left)
currentSceneNode = ScoresSceneNode(gameScene: self)
currentSceneNode.presentWithAnimation(.Left)
default: fatalError("Unknown scene transition.")
}
}
}
class SceneNode: SKNode {
weak var gameScene: GameScene!
init(gameScene: GameScene) {
self.gameScene = gameScene
super.init()
}
func layout() {}
func presentWithAnimation(animation:Animation) {
layout()
let invert: CGFloat = animation == .Left ? 1 : -1
self.position = CGPoint(x: invert*gameScene.size.width, y: 0)
gameScene.addChild(self)
let action = SKAction.moveTo(CGPoint(x: 0, y: 0), duration: 0.3)
action.timingMode = SKActionTimingMode.EaseInEaseOut
self.runAction(action)
}
func dismissWithAnimation(animation:Animation) {
let invert: CGFloat = animation == .Left ? 1 : -1
self.position = CGPoint(x: 0, y: 0)
let action = SKAction.moveTo(CGPoint(x: invert*(-gameScene.size.width), y: 0), duration: 0.3)
action.timingMode = SKActionTimingMode.EaseInEaseOut
self.runAction(action, completion: {self.removeFromParent()})
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MenuSceneNode: SceneNode {
var label: SKLabelNode
var container: SKSpriteNode
override func layout() {
container.position = CGPoint(x: gameScene.size.width/2.0, y: gameScene.size.height/2.0)
}
override init(gameScene: GameScene) {
label = SKLabelNode(text: "Menu Scene")
label.horizontalAlignmentMode = .Center
label.verticalAlignmentMode = .Center
container = SKSpriteNode(color: UIColor.blackColor(), size: CGSize(width: 200, height: 200))
container.addChild(label)
super.init(gameScene: gameScene)
self.addChild(container)
self.userInteractionEnabled = true
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
self.gameScene.transitionToScene(.Scores)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ScoresSceneNode: SceneNode {
var label: SKLabelNode
var container: SKSpriteNode
override func layout() {
container.position = CGPoint(x: gameScene.size.width/2.0, y: gameScene.size.height/2.0)
}
override init(gameScene: GameScene) {
label = SKLabelNode(text: "Scores Scene")
label.horizontalAlignmentMode = .Center
label.verticalAlignmentMode = .Center
container = SKSpriteNode(color: UIColor.blackColor(), size: CGSize(width: 200, height: 200))
container.addChild(label)
super.init(gameScene: gameScene)
self.addChild(container)
self.userInteractionEnabled = true
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
self.gameScene.transitionToScene(.Menu)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
enum SceneTransition{
case Menu, Scores
}
enum Animation {
case Left, Right, None
}