How to integrate UIDevice rotation and creating a new UIBezierPath after rotation?

100 Views Asked by At

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?

3

There are 3 best solutions below

3
DonMag On BEST ANSWER

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:

enter image description here

enter image description here

We can use this image (named "arrow2"):

enter image description here

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.

0
John Love On

This reply is to @DonMag and is not meant to be an answer.

Boy, have I learned a lot from you ...

At the risk of pushing your buttons, here is a pic of your output when I substitute your "arrow2" with my "train".

Give me a week or whatever to struggle by myself .. but here are a few pics to describe my problem:

    let theSpeed = Double(5*thisSpeed)

    var trainAction = SKAction.follow(
                                  trainPath.cgPath,
                                  asOffset: false,
                                  orientToPath: true,  // <==
                                  speed: theSpeed)
    trainAction = SKAction.repeatForever(trainAction)

which is due to the fact that my Train is not symmetrical, unlike your "arrow2".

enter image description here

Here is what I am looking for:

enter image description here

Good Grief - easy for 0-degrees, 90-degrees + 270-degrees. Just use your favorite graphics editor and start with an image 90-degrees CCW and Swift will magically rotate it 90-degrees CW before it starts moving your SKSpriteNode.

Whoops, not so fast => if we pre-rotate it CCW to counter Swift's CW rotation, then when I display a static Train upon initial start-up of the App, the Train is rotated CCW 90-degrees. I let touch-swiping to stop and re-start movement, so upon start-up the Train is not moving.

The biggee is what I have labelled 180-degrees Somehow I've got to figure out how to keep track of where we are along the ellipse path and adjust the graphic accordingly. For example, it would be nice when at 90-degrees to add a z-rotation of minus 45-degrees and when at 270-degrees to add a z-rotation of plus 45-degrees.

0
John Love On

my Credits SKScene - hopefully you like it

enter image description here