How to integrate UIDevice rotation and creating a new UIBezierPath after rotation?
My challenge here is to successfully integrate UIDevice
rotation and creating a new UIBezierPath
every time the UIDevice
is rotated.
EDIT: changed to viewDidLayoutSubviews
per DonMag's recommendation. This makes sense because I wish to generate a new UIBezierPath after rotation when all the SKSpriteNodes have been resized and repositioned.
Oh, how I wish that worked .. but it did not
As a preamble, I have bounced back and forth between
NotificationCenter.default.addObserver(self,
selector: #selector(rotated),
name: UIDevice.orientationDidChangeNotification,
object: nil)
called within my viewDidLoad()
together with
@objc func rotated() {
}
and
override func viewDidLayoutSubviews() {
// please see code below
}
My success was much better when I implemented viewDidLayoutSubviews()
, versus rotated()
.. so let me provide detailed code just for viewDidLayoutSubviews()
.
I have concluded that every time I rotate the UIDevice
, a new UIBezierPath
needs to be generated because positions
and sizes
of my various SKSprieNodes
change.
I am definitely not saying that I have to create a new UIBezierPath
with every rotation .. just saying I think I have to.
Start of Code
// declared at the top of my `GameViewController`:
var myTrain: SKSpriteNode!
var savedTrainPosition: CGPoint?
var trackOffset = 60.0
var trackRect: CGRect!
var trainPath: UIBezierPath!
My UIBezierPath
creation and SKAction.follow
code is as follows:
// called with my setTrackPaths() – see way below
func createTrainPath() {
// savedTrainPosition initially set within setTrackPaths().
// We no longer keep tabs on this Position because
// UIBezierPath's built-in .currentPoint does that for us.
trackRect = CGRect(x: savedTrainPosition!.x,
y: savedTrainPosition!.y,
width: tracksWidth,
height: tracksHeight)
trainPath = UIBezierPath(ovalIn: trackRect)
trainPath = trainPath.reversing() // makes myTrain move CW
} // createTrainPath
func startFollowTrainPath() {
let theSpeed = Double(5*thisSpeed)
var trainAction = SKAction.follow(
trainPath.cgPath,
asOffset: false,
orientToPath: true,
speed: theSpeed)
trainAction = SKAction.repeatForever(trainAction)
createPivotNodeFor(myTrain)
myTrain.run(trainAction, withKey: runTrainKey)
} // startFollowTrainPath
func stopFollowTrainPath() {
guard myTrain == nil else {
myTrain.removeAction(forKey: runTrainKey)
savedTrainPosition = myTrain.position
return
}
} // stopFollowTrainPath
Here is the detailed viewWillLayoutSubviews
I promised earlier:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if (thisSceneName == "GameScene") {
// code to pause moving game pieces
setGamePieceParms() // for GamePieces, e.g., trainWidth
setTrackPaths() // for trainPath
reSizeAndPositionNodes()
// code to resume moving game pieces
} // if (thisSceneName == "GameScene")
} // viewDidLayoutSubviews
func setGamePieceParms() {
if (thisSceneName == "GameScene") {
roomScale = 1.0
let roomRect = UIScreen.main.bounds
roomWidth = roomRect.width
roomHeight = roomRect.height
roomPosX = 0.0
roomPosY = 0.0
tracksScale = 1.0
tracksWidth = roomWidth - 4*trackOffset // inset from screen edge
#if os(iOS)
if UIDevice.current.orientation.isLandscape {
tracksHeight = 0.30*roomHeight
}
else {
tracksHeight = 0.38*roomHeight
}
#endif
// center horizontally
tracksPosX = roomPosX
// flush with bottom of UIScreen
let temp = roomPosY - roomHeight/2
tracksPosY = temp + trackOffset + tracksHeight/2
trainScale = 2.8
trainWidth = 96.0*trainScale // original size = 96 x 110
trainHeight = 110.0*trainScale
trainPosX = roomPosX
#if os(iOS)
if UIDevice.current.orientation.isLandscape {
trainPosY = temp + trackOffset + tracksHeight + 0.30*trainHeight
}
else {
trainPosY = temp + trackOffset + tracksHeight + 0.20*trainHeight
}
#endif
} // setGamePieceParms
// a work in progress
func setTrackPaths() {
if (thisSceneName == "GameScene") {
if (savedTrainPosition == nil) {
savedTrainPosition = CGPoint(x: tracksPosX - tracksWidth/2, y: tracksPosY)
}
else {
savedTrainPosition = CGPoint(x: tracksPosX - tracksWidth/2, y: tracksPosY)
}
createTrainPath()
} // if (thisSceneName == "GameScene")
} // setTrackPaths
func reSizeAndPositionNodes() {
myTracks.size = CGSize(width: tracksWidth, height: tracksHeight)
myTracks.position = CGPoint(x: tracksPosX, y: tracksPosY)
// more Nodes here ..
}
End of Code
My theory says when I call setTrackPaths()
with every UIDevice
rotation, createTrainPath()
should be called.
Nothing happens of significance visually as far as the UIBezierPath
is concerned .. until I call startFollowTrainPath()
.
Bottom Line
It is then that I see for sure that a new UIBezierPath
has not been created as it should have been when I called createTrainPath()
when I rotated the UIDevice
.
The new UIBezierPath
is not new, but the old one.
If you’ve made it this far through my long code, the question is what do I need to do to make a new UIBezierPath
that fits the resized and repositioned SKSpriteNode
?
Trying to boil this down to the basics to make it understandable...
When using scene.scaleMode = .resizeFill
, we can implement override func didChangeSize(_ oldSize: CGSize)
in the SKScene
class. This will be called when the scene size changes, such as on device rotation.
So, for a very simple example that will look like this:
We can use this image (named "arrow2"):
and this example code...
GameViewController class
import UIKit
import SpriteKit
import GameplayKit
class GameViewController: UIViewController {
var scene: GameScene!
override func viewDidLoad() {
super.viewDidLoad()
scene = GameScene(size: view.frame.size)
scene.scaleMode = .resizeFill
if let skView = view as? SKView {
skView.presentScene(scene)
}
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .all
}
override var prefersStatusBarHidden: Bool {
return true
}
}
GameScene class
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var spOval: SKShapeNode!
var myTrain: SKSpriteNode!
var trainPath: UIBezierPath!
var currentSize: CGSize = .zero
override func didMove(to view: SKView) {
// ellipse frame will be set in updateFraming
spOval = SKShapeNode(ellipseIn: .zero)
spOval.lineWidth = 5
spOval.strokeColor = .lightGray
addChild(spOval)
myTrain = SKSpriteNode(imageNamed: "arrow2")
addChild(myTrain)
updateFraming()
startAnim()
}
override func didChangeSize(_ oldSize: CGSize) {
if let v = self.view {
// this can be called multiple times on device rotation,
// so we only want to update the framing and animation
// if the size has changed
if currentSize != v.frame.size {
currentSize = v.frame.size
updateFraming()
startAnim()
}
}
}
func updateFraming() {
// self.view is optional, so safely unwrap
guard let thisSKView = self.view else { return }
let sz = thisSKView.frame.size
// make the ellipse width equal to view width minus 120-points on each side
let w: CGFloat = sz.width - 240.0
// if view is wider than tall (landscape)
// set ellipse height to 30% of view height
// else (portrait)
// set ellipse height to 38% of view height
let h: CGFloat = sz.width > sz.height ? sz.height * 0.3 : sz.height * 0.38
// center horizontally
let x: CGFloat = (sz.width - w) * 0.5
// put bottom of ellipse 40-points from bottom of view
let y: CGFloat = 40.0
let r: CGRect = .init(x: x, y: y, width: w, height: h)
// create the "path to follow"
trainPath = UIBezierPath(ovalIn: r).reversing()
// update the visible oval
spOval.path = trainPath.cgPath
}
func startAnim() {
var trainAction = SKAction.follow(
trainPath.cgPath,
asOffset: false,
orientToPath: true,
speed: 200.0)
trainAction = SKAction.repeatForever(trainAction)
myTrain.run(trainAction, withKey: "myKey")
}
}
Here's a link to a full project: https://github.com/DonMag/SpriteKitRotation
Not entirely sure this will give you what you're going for, since you have a lot of code that is not clear... but hopefully it can at least get you headed in the right direction.