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?
-
Dan, did you figure out how to get the buttons working with Crashoverride777's Scrolling View? i'm having trouble – Josh Schlabach Jun 15 '16 at 17:57
-
@JoshSchlabach yeah I did, I seem to remember it was to do with making sure that the scroll view finishes its scroll and becomes static, because it wouldn't recognise touches whilst it thought it was still scrolling, although I found it was a bit buggy sometimes so I ended up not using it! – Jun 16 '16 at 19:29
-
what did you use then? I need help with my scroll view for my app! – Josh Schlabach Jun 17 '16 at 14:58
-
@JoshSchlabach I did find a way to scroll, but it didn't have any of the inertia of the normal scrolling (menu items didn't bounce when they get to the bottom, menu stops scrolling as soon as you lift your touch etc) and found it didn't feel good, so I actually abandoned it and changed my menu design a little bit. I guess you're making a vertical scrolling menu of sorts? – Jun 17 '16 at 22:08
-
Yes and I'm making a shop as well. – Josh Schlabach Jun 17 '16 at 22:38
-
1@JoshSchlabach use Crash's method, but add a: `print("description of what code has just done")` to the end of each section, then watch the console as you try and scroll, it should tell you where it's messing up and hopefully will allow you to realise why the buttons aren't working (Or at least that's what I did). Sorry I can't be of more help, I can't quite remember what I did to solve the issue at the time. – Jun 18 '16 at 10:36
4 Answers
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

- 10,581
- 2
- 32
- 56
-
That's absolutely amazing, I'll have a go at implementing it tomorrow and I'll update you on how it goes. Thankyou so much you are an absolute lifesaver – Dec 11 '15 at 23:41
-
You are welcome. This should defo work, i just tried it in xCode with the basic template to refresh my memory. Please let me know how it goes just incase I missed something. – crashoverride777 Dec 11 '15 at 23:46
-
Had a go at implementing it this morning, it's worked like a dream. A really clever method as well actually, using the moveable node and adding the nodes to the moveable node really works. Many thanks again - you've been a massive help and done brilliant work! – Dec 12 '15 at 12:10
-
My pleasure, happy coding. Just remember that the scroll view has priority, so make sure you check interactions between your buttons and scroll view so things don't interfere with each other. – crashoverride777 Dec 12 '15 at 12:47
-
Thanks so much for posting this - I have a question - when I do this for horizontal, I can only scroll to the left, but I actually want to scroll to the right instead (so let the user see what's over on the right side of the screen). I saw you wrote to consider the contentOffset, so I did this code here. It works, but would you say this is good? scrollView.contentOffset = CGPoint(x: self.frame.size.width * 2, y: 0) – NullHypothesis Feb 23 '16 at 03:59
-
http://stackoverflow.com/users/4945232/crashoverride777 How do I get the buttons to work on the scroll view? – Josh Schlabach Jun 15 '16 at 17:58
-
Hey, you use the touches began method in your corresponding SKScene. Have a look at my GitHub project for some sample code (GameScene.swift). – crashoverride777 Jun 15 '16 at 18:07
-
https://github.com/crashoverride777/Swift-SpriteKit-UIScrollView-Helper/blob/master/CustomScrollView/GameScene.swift – crashoverride777 Jun 15 '16 at 18:09
-
-
1Thanks very much. Just incase you are copying the code from this answer, don't. Go to gitHub for the latest version, I have made some changes and fixes since this answer. – crashoverride777 Jun 22 '16 at 19:02
-
1Question, I don't want the ScrollView to be the full size of the screen (which it appears to always be). Say I wanted to list each friend a Player has. I get the nodes on fine, click them etc. But I don't want it to scroll off the top and bottom of the screen, Just say a 200x300 box in the middle of the screen. – Ryann786 Mar 21 '19 at 02:26
-
This is helpful to organize and frame the idea for a native SpriteKit scrollView. However, this isn't actually a SpriteKit scrollView, but a UIScrollView that supports SKNodes as children. It won't work in watchOS. Instead of UIScrollView, the scrollView needs to be an empty SKNode that positions its children based on contentSize and scroll direction. – Michael N Aug 13 '19 at 16:52
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

- 10,581
- 2
- 32
- 56
-
Thankyou that's really useful! Would this method allow the inertia that comes with UIScrollView? Or would I have to try and add that myself? – Dec 11 '15 at 21:26
-
You won't get any off the cool effects unfortunately. With the scroll view it is such a pain to use in skscenes. You have to do a load of trickery to register touches, to add SKNodes instead of UIKit objects, when you overlay menus on top of the scroll view u have to deactivate it, things scroll in the opposite direction unless you flip the scroll view, you have to position stuff differently because SpriteKit kit has different coordinates than UKit. It's quite a mess but it is doable. Depends how important the menu is to you – crashoverride777 Dec 11 '15 at 21:33
-
Looks like your previous recommended method is the answer then! I wondered why there was so little documentation on ScrollView in SpriteKit... – Dec 11 '15 at 21:36
-
I mean if you are comfortable with UIKit and storyboards you could make another view controller and do it that way. However if you just want to add it to a SKScene than its the pain. Especially the positioning which is basically in reverse. It's up to you I can show you if you want? – crashoverride777 Dec 11 '15 at 21:40
-
If I created a menu using a storyboard and UIKit, could I use that to replace the SKScene the menu is currently in? All my other scenes are SKScenes. – Dec 11 '15 at 21:45
-
I'm not sure because I myself are not very comfortable with storyboards, I mainly focus on SpriteKit. It's cool I'll add another answer for you with the scroll view. Give me like 10 min or something – crashoverride777 Dec 11 '15 at 21:47
-
No problem I am stil playing around with it, I haven't used my helper in a while and I want to make sure I post everything correct. I'll defo post soon – crashoverride777 Dec 11 '15 at 22:19
-
-
I am gonna post now. Its quite long so dont get scared. Its not that bad. – crashoverride777 Dec 11 '15 at 23:29
-
@crashoverride777 I've used this and it helped me a lot. I'm creating a MKMapView and I can't add it using addChild so I use .addSubview but moveableArea can't add it. What should I do? – Niall Kehoe Dec 31 '16 at 22:33
-
Hey, sorry this is a really old answer and I am not using it anymore. I am now using a UIScrollView, check out my GitHub project called SwiftySKScrollView. I also haven't used map view before but it should work. – crashoverride777 Jan 01 '17 at 14:21
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)
}
}

- 33,605
- 61
- 269
- 439
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
}
}

- 1,327
- 17
- 22