Working solution:
@objc func handlePinch(_ sender: UIPinchGestureRecognizer) {
guard let camera = self.camera else {
return
}
if sender.state == .began {
sender.scale = camera.xScale
}
if sender.state == .changed {
camera.setScale(sender.scale)
}
}
For clarity, self.camera
is the weak var property defined on SKScene.
When the pinch gesture is initiated, the sender's scale always starts from 1.0. So if you want your camera to start from its previous scale, you need to set the sender's scale to your previous camera scale.
For example, if your camera's scale was 1.5 when the gesture ended, the next time a pinch gesture begins, the sender's scale is set to 1.5. Then when the fingers are moved (i.e. sender.state == .changed
), the sender's scale increases/decreases starting from 1.5.
Otherwise, the camera's scale gets set back to 1.0, causing a clipping effect. You will not be able to zoom in/out more than a certain amount, since the scale always starts back from 1.0 instead of picking up where it left off.
According to Apple's documentation,
Because your action method may be called many times, you can’t simply apply the current scale factor to your content. (...) Instead, cache the original value of your content, apply the scale factor to that original value, and apply the new value back to your content.
This means that the action method attached to the gesture recognizer is called continuously (multiple times a second) whenever the fingers move (i.e. sender.state == .changed)
. If you apply any kind of scaling directly to the camera, be it addition, multiplication, division e.g. camera.setScale(sender.scale + 0.1)
, the camera's scale will change exponentially.
You can read more here - https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/handling_uikit_gestures/handling_pinch_gestures
EDIT:
My original answer does not account for the fact that SKCameraNode
applies its inverse scaling on to the scene's elements. That is to say, for a pinch gesture expecting to zoom the camera out, the camera is actually zooming in. If you want the camera to zoom in and out as expected, you can use the following code:
In GameScene.swift
, add a variable to store the previous camera scale:
class GameScene: SKScene {
var previousCameraScale: CGFloat = CGFloat.zero
}
Change the handlePinch
function as follows:
@objc func handlePinch(_ sender: UIPinchGestureRecognizer) {
guard let camera = self.camera else {
return
}
if sender.state == .began {
previousCameraScale = camera.xScale
sender.scale = previousCameraScale
}
if sender.state == .changed {
let change = sender.scale - previousCameraScale
let newScale = max(min((previousCameraScale - change), maxCameraScale), minCameraScale)
camera.setScale(newScale)
}
}
For sender.state == .began
, there is no change. The sender's scale is still set to the previous camera scale so that there is no clipping effect, only now we're storing the variable in the previousCameraScale
property so that we can use it in repeated calls of the handlePinch
action method.
For sender.state == .changed
, we first calculate how much the sender's scale has changed. Then we subtract this change from the previous camera scale, and lastly set the camera's scale to this new scale.
For example, if previousCameraScale
= 1.5 and the user zooms in (fingers spread apart, causing sender.scale
to increase, to, let's say, 1.7), the change
will be 0.2. The camera's scale needs to decrease as the sender's scale increases. So if sender.scale
is increased to 1.7, camera.scale
needs to decrease to 1.3. This is essentially what previousCameraScale - change
means.
I also clamped newScale
between a lower and upper bound so that (1) the scale doesn't go negative, which would flip everything, and (2) the camera's scale doesn't get too large, in which case everything would become super small.