A Day In The Life

とあるプログラマの備忘録

SpriteKitでUIScrollView的なスクロール処理を実装する

SpriteKit でゲームやアプリを開発していると UI 系の部品がなくて困ることがあります。SpriteKit にはラベルしかなくボタンすらないです。ましてやスクロールビューなんて贅沢なものは当然ありません。現在開発しているアプリでどうしても必要になったので自作してみました(プログラムが複雑になるので横スクロールのみ実装しています)。

慣性処理のないスクロールノー

まずはじめに一番単純な実装からいきます。指で動かした分だけラベルが横にスクロールします。慣性の処理がないので touchesBegan: withEvent: と touchesMoved: withEvent: の処理だけで実現できます。

import SpriteKit

class ScrollNode: SKSpriteNode {
    
    // スクロールさせるコンテンツを配置するためのノード
    private var contentNode = SKNode()
    private var startX: CGFloat = 0.0
    private var lastX: CGFloat = 0.0
    
    init(size: CGSize) {
        super.init(texture: nil, color: SKColor.clearColor(), size: size)

        self.userInteractionEnabled = true

        self.contentNode.position = CGPoint(x: 0, y: 0)
        self.addChild(self.contentNode)
        
        // スクロールさせるコンテンツ
        let myLabel = SKLabelNode(fontNamed: "Helvetica")
        myLabel.text = "scroll"
        myLabel.fontSize = 20
        self.contentNode.addChild(myLabel)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        // store the starting position of the touch
        let touch = touches.first
        let location = touch!.locationInNode(self)
        startX = location.x
        lastX = location.x
    }
    
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let touch = touches.first
        let location = touch!.locationInNode(self)
        
        // set the new location of touch
        let currentX = location.x
        
        // スクロールのスピード。好みで変えてください
        let scrollSpeed: CGFloat = 1.0        
        let newX = self.contentNode.position.x + ((currentX - lastX) * scrollSpeed)

        // 左と右端の設定
        let limitFactor: CGFloat = 0.3 // 好みで変えてください
        let leftLimitX = self.size.width * (-limitFactor)
        let rightLimitX = self.size.width * limitFactor

        // 移動処理
        if newX < leftLimitX {
            self.contentNode.position = CGPointMake(leftLimitX, self.contentNode.position.y)
        } else if newX > rightLimitX {
            self.contentNode.position = CGPointMake(rightLimitX, self.contentNode.position.y)
        } else {
            self.contentNode.position = CGPointMake(newX, self.contentNode.position.y)
        }
        
        // Set new last location for next time
        lastX = currentX
    }   
}

使い方

使い方は簡単で適当なサイズを設定して SKScnene に配置してやるだけです。

class GameScene: SKScene {
    override func didMoveToView(view: SKView) {
        let scrollNode = ScrollNode(size: CGSize(width: 200, height: 44))
        self.addChild(scrollNode)
    }
}

参考記事

慣性処理に対応したスクロールノー

慣性処理のないスクロールノードでもとりあえずスクロールの動きはできていますがやっぱり UIScrollView のように慣性スクロールしたほうが自然だし使いごごちも良いです。というわけで慣性処理を追加します。 慣性処理は update メソッドの中で行います。 SKSpriteNode オブジェクトは SKScene オブジェクトのように自動で update メソッドが呼び出されないので自前でタイマーを仕込むか SKScene の update メソッドと連動して動かす必要があります。

import SpriteKit

class ScrollNode: SKSpriteNode {
    
    private var contentNode = SKNode()
    private var startX: CGFloat = 0.0
    private var lastX: CGFloat = 0.0
    // タッチされているかどうか
    private var touching = false
    // 少しずつ移動させる
    private var lastScrollDistX: CGFloat = 0.0
    
    init(size: CGSize) {
        super.init(texture: nil, color: SKColor.clearColor(), size: size)

        self.userInteractionEnabled = true

        self.contentNode.position = CGPoint(x: 0, y: 0)
        self.addChild(self.contentNode)
        
        // スクロールさせるコンテンツ
        let myLabel = SKLabelNode(fontNamed: "Helvetica")
        myLabel.text = "scroll"
        myLabel.fontSize = 20
        self.contentNode.addChild(myLabel)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        self.touching = true
        // store the starting position of the touch
        let touch = touches.first
        let location = touch!.locationInNode(self)
        startX = location.x
        lastX = location.x
    }
    
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let touch = touches.first
        let location = touch!.locationInNode(self)
        
        // set the new location of touch
        let currentX = location.x
        
        lastScrollDistX =  lastX - currentX
        
        self.contentNode.position.x -= lastScrollDistX
        
        // Set new last location for next time
        lastX = currentX
    }
    
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        self.touching = false
    }
    
    override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
        self.touching = false
    }
    
    func update(currentTime: CFTimeInterval) {
        // タッチされてたら
        guard !touching else {
            return
        }
       
        // 左と右端の設定
        let limitFactor: CGFloat = 0.3
        let leftLimitX: CGFloat = self.size.width * (-limitFactor)
        let rightLimitX: CGFloat = self.size.width * limitFactor
        if self.contentNode.position.x < leftLimitX {
            // 行き過ぎたから戻す
            self.contentNode.position.x = leftLimitX
            lastScrollDistX = 0.0
            return
        }
        if self.contentNode.position.x > rightLimitX {
            // 行き過ぎたから戻す
            self.contentNode.position.x = rightLimitX
            lastScrollDistX = 0.0
            return
        }
            
        // 慣性処理
        var slowDown: CGFloat = 0.98
        if fabs(lastScrollDistX) < 0.5 {
            slowDown = 0.0
        }
        lastScrollDistX *= slowDown
        self.contentNode.position.x -= lastScrollDistX
    }
}

使い方

はじめに説明しましたが SKScnene に配置してやるだけでは updata メソッドが実行されないので SKScene の update メソッドと連動させてやる必要があります。

class GameScene: SKScene {

    var scrollNode: ScrollNode!
    
    override func didMoveToView(view: SKView) {
        self.scrollNode = ScrollNode(size: CGSize(width: 200, height: 44))
        self.addChild(scrollNode)
    }

    override func update(currentTime: CFTimeInterval) {
        // scrollNodeのupdateメソッドを呼ぶ
        self.scrollNode.update(currentTime)
    }
}

参考記事

慣性処理をなめらかにしたい

慣性スクロールには対応できましたが少し動きが硬いです。そこでアニメーションを入れてなめらかに動くように修正してみます。

class ScrollNode: SKSpriteNode {
    :
    : 省略
    :
    func update(currentTime: CFTimeInterval) {
        // タッチされてたら
        guard !touching else {
            return
        }
        
        let limitFactor: CGFloat = 0.3
        let leftLimitX: CGFloat = self.size.width * (-limitFactor)
        let rightLimitX: CGFloat = self.size.width * limitFactor
        if self.contentNode.position.x < leftLimitX {
            // アニメーションさせる
            if !self.contentNode.hasActions() {
                let move = SKAction.moveToX(leftLimitX, duration: 0.2)
                move.timingMode = .EaseInEaseOut
                self.contentNode.runAction(move)
            }
            lastScrollDistX = 0.0
            return
        }
        if self.contentNode.position.x > rightLimitX {
            // アニメーションさせる
            if !self.contentNode.hasActions() {
                let move = SKAction.moveToX(rightLimitX, duration: 0.2)
                move.timingMode = .EaseInEaseOut
                self.contentNode.runAction(move)
            }
            lastScrollDistX = 0.0
            return
        }
            
        var slowDown: CGFloat = 0.98
        if fabs(lastScrollDistX) < 0.5 {
            slowDown = 0.0
        }
        lastScrollDistX *= slowDown
        self.contentNode.position.x -= lastScrollDistX
    }
}