SpriteKit Inertial Camera

,

A custom SKCameraNode designed for prototyping and navigating a SpriteKit scene. It supports panning, pinching, and rotating, with inertia applied to each transformation.

Multi-touch scene navigation. The camera is highly customizable, offering a variety of settings and features.

Demo App

SpriteKit-Inertial-Camera-Icon-Alpha.png

Download SpriteKit Inertial Camera from GitHub, then:

Alternatively, you can preview the demo app without an iOS device:

Minimal Setup

Copy the camera class file in your Xcode project, then follow this minimal setup:

/// Your SpriteKit scene
class MyScene: SKScene, UIGestureRecognizerDelegate {
    
    /// Create camera instance
    let inertialCamera = InertialCamera()
    
    /// Attach to view and scene
    override func didMove(to view: SKView) {
        /// Assign the gesture recognizer delegate before the view
        inertialCamera.gestureRecognizerDelegate = self
        inertialCamera.gesturesView = view
        self.camera = inertialCamera
        addChild(inertialCamera)
    }
    
    /// Update inertia
    override func update(_ currentTime: TimeInterval) {
        inertialCamera.update()
    }
    
    /// Allow simultaneous gestures
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
    
    /// Stop on touch
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        inertialCamera.stop()
    }
    
}

API

Default Transform

The camera is initialized with a default position, scale, and rotation. You can pass different values during initialization:

/// Create the camera with specific transforms
let inertialCamera = InertialCamera(
    position: .zero, /// Optional. Default is CGPoint(x: 0, y: 0)
    xScale: 1,       /// Optional. Default is 1
    yScale: 1,       /// Optional. Default is 1
    rotation: 0,     /// Optional. Default is 0
)

After initialization, you can set new defaults on these properties:

inertialCamera.defaultPosition 
inertialCamera.defaultRotation
inertialCamera.defaultXScale
inertialCamera.defaultYScale

Use reset() to restore all the default transforms:

inertialCamera.reset()
/// Or pass an optional withAnimation parameter. Default is true
inertialCamera.reset(withAnimation: true)

Animating the Camera

Use setTo() to move, scale, or rotate the camera programmatically. Any parameter left as nil keeps its current value.

inertialCamera.setTo(
    position: CGPoint? = nil,   /// Target position (optional).
    xScale: CGFloat? = nil,     /// Target X scale (optional).
    yScale: CGFloat? = nil,     /// Target Y scale (optional).
    rotation: CGFloat? = nil,   /// Target rotation (optional, in radians).
    withAnimation: Bool? = nil  /// Animate transitions (default is true).
)

/// Example: Zoom out without changing position or rotation
inertialCamera.setTo(xScale: 2, yScale: 2)

To stop all camera transformations and animations immediately, use stop():

inertialCamera.stop()

Inertia

The camera has separate inertia controls for pan, scale, and rotation. The velocity values are applied once per frame by update().

/// Toggle position inertia.
inertialCamera.enablePanInertia = true

/// Toggle scale inertia.
inertialCamera.enableScaleInertia = true

/// Toggle rotation inertia.
inertialCamera.enableRotationInertia = true

You can tune how quickly each velocity decays:

/// Position velocity is multiplied by this factor every frame. Default is `0.95`.
inertialCamera.positionInertia = 0.95

/// Scale velocity is multiplied by this factor every frame. Default is `0.75`.
inertialCamera.scaleInertia = 0.75

/// Rotation velocity is multiplied by this factor every frame. Default is `0.85`.
inertialCamera.rotationInertia = 0.85

If inertia is enabled, you can manipulate the velocities directly

inertialCamera.positionVelocity = CGVector(dx: 0, dy: 0)
inertialCamera.scaleVelocity = CGVector(dx: 0, dy: 0)
inertialCamera.rotationVelocity: CGFloat = 0

Zoom

SpriteKit cameras use scale for zoom. A lower scale value means the camera is zoomed in. A higher scale value means the camera is zoomed out.

/// Maximum zoom out. Default is `10`, which is 10% zoom.
inertialCamera.maxScale = 10

/// Maximum zoom in. Default is `1 / 6`, which is 600% zoom.
inertialCamera.minScale = 1 / 6

Lock

You can lock each transform independently, or lock all camera gesture handling.

/// Lock camera pan.
inertialCamera.lockPan = false

/// Lock camera scale.
inertialCamera.lockScale = false

/// Lock camera rotation.
inertialCamera.lockRotation = false

/// Lock all camera gesture handling.
inertialCamera.lock = false

Gesture View

The camera installs its own UIKit gesture recognizers on gesturesView. This can be the SKView presenting the scene, or another UIView in the same view hierarchy. Assign gestureRecognizerDelegate before assigning gesturesView.

inertialCamera.gestureRecognizerDelegate = self
inertialCamera.gesturesView = view

The camera's gesture recognizers are configured so they do not cancel or delay normal SpriteKit touch handling. This makes it easier to combine camera navigation with scene interactions such as selecting, dragging, or tapping nodes.

Clamping

Use area to clamp the camera position to a rectangular area centered on the camera parent’s coordinate system. If area is nil, no position clamping is applied.

/// Clamp camera movement to this scene-sized area.
inertialCamera.area = CGSize(width: 3000, height: 2000)

/// Remove camera position clamping.
inertialCamera.area = nil

Delegate

The InertialCameraDelegate protocol provides methods for tracking camera changes. A common use case is updating the UI when the camera's state changes. For example, in the demo scene, the zoom UI label is updated using the cameraDidScale callback.

In the object where you want to listen to camera changes, such as the scene, conform to the InertialCameraDelegate protocol and implement its methods:

class MyScene: SKScene, InertialCameraDelegate {
    
   override func didMove(to view: SKView) {
       //..
       /// Assign the scene as the delegate
       inertialCamera.delegate = self
       //..
    }
    
    func cameraDidMove(to position: CGPoint) {
        /// Handle camera position change
    }
    
    func cameraDidRotate(to angle: CGFloat) {
        /// Handle camera rotation change
    }
    
    func cameraDidScale(to scale: (x: CGFloat, y: CGFloat)) {
        /// Handle camera scale change
    }
    
    /// Track action change
    override func didEvaluateActions() {
        inertialCamera.didEvaluateActions()
    }
    
}

The didEvaluateActions method is necessary because some of the camera's transformations are performed using SKAction. However, SKAction does not automatically notify the camera of changes it makes to the camera's properties (e.g., position, scale, or rotation). By invoking didEvaluateActions() after actions are evaluated, the camera can update its state and ensure that the delegate methods are called with the latest values.

License

This project is licensed under the Apache License 2.0.

If this project helps your work, attribution or a link back is appreciated: https://www.achrafkassioui.com/spritekit-inertial-camera

Credits

This project started as a fork of SKCamera-Demo. Thank you @HumboldtCodeClub for sharing and commenting your code.