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

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.

(Please accept my apologies for this Post’s length .. but I can’t seem to avoid it)

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 viewWillLayoutSubviews() {

    // please see code below

}

My success was much better when I implemented viewWillLayoutSubviews(), versus rotated() .. so let me provide detailed code just for viewWillLayoutSubviews().

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()
    // and later reset when stopping + resuming moving myTrain
    // via stopFollowTrainPath()
    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 viewWillLayoutSubviews() {
    
    super.viewWillLayoutSubviews()
    
    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")
            
}   // viewWillLayoutSubviews


    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() is 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?

Elsewhere, someone properly suggested to change viewWillLayoutSubviews to viewDidLayoutSubviews.

This makes total sense because I wish to generate a new UIBezierPath after rotation when all the SKSpriteNodes have been resized and repositioned. Regrettably this did not work. So, I'm still on the prowl for the correct solution.

I have even tried to delete the current UIBezierPath by calling trainPath.cgPath.getPoints().removeAll and then re-create it when the UIDevice is rotated. Once again, visually it shows that the SKSpriteNode follows the old UIBezierPath

After rotation, a new one has not been created. This is for certain because the SKSpriteNode follows the old one if it's moving before rotation. However, if I stop the movement before the rotation within viewDidLayoutSubviews() and resume the movement after the rotation, a new UIBezierPath is created. But what I want is to replace the UIBezierPath while moving when rotation happens. If you scroll up my OP, you will see that my viewDidLayoutSubviews() calls createTrainPath() which needs ? to be called during this rotation.

As already noted, I have tried to effectively delete the old UIBezierPath by removing all the .cgPath [CGPoint] before calling setTrackPaths() (instead of stopping the movement and then resuming). Please note that my setTrackPaths() calls createTrainPath(). Unfortunately, the old Path stuck around with its old [CGPoint] and was never replaced by createTrainPath() when the latter called trainPath = UIBezierPath(ovalIn: trackRect).

It just dawned on me that the problem could rest with my call to trackRect = CGRect(x:, y:, width:tracksWidth, height:tracksHeight). I say that because with rotation, the width and height parts change. Never mind ... I just deleted that guess because my call to setGamePieceParms() occurs before setTrackPaths() .. and the former changes tracksWidth and tracksHeight. Back to the drawing board ...

I really appreciate the comprehensive code you shared! While diving into your code to create a new UIBezierPath, I couldn't find the part where you rotate the view. It would be really helpful if you could provide a complete focused sample that we can run so that we can better understand the issue.

In your code, it seems that you are currently hardcoding the width and height based on the rotation angle. However, this approach may not work well on different devices as the dimensions will vary.

Therefore, destroying the object and recreating it after rotation will give you a new UIBezierPath that is accurately sized for the rotated view. Here's the link to the documentation that might be useful:

https://developer.apple.com/documentation/spritekit/skscene/scaling_a_scene_s_content_to_fit_the_view

Reference my comment above:

I have even tried to delete the current UIBezierPath by calling trainPath.cgPath.getPoints().removeAll and then re-create it when the UIDevice is rotated. Once again, visually it shows that the SKSpriteNode follows the old UIBezierPath

Next:

    if let ourScene = GameScene(fileNamed: thisSceneName) {
                    
        // Force SKScene to fill up its parent SKView:
        if (thisSceneName == "GameScene")  { ourScene.scaleMode = .aspectFill }
        else                               { ourScene.scaleMode = .resizeFill }
    }

My rotation code is within:

override func viewDidLayoutSubviews() {
    
    super.viewDidLayoutSubviews()
    
    if (thisSceneName == "GameScene") {
        
        setGamePieceParms()   // for GamePieces, e.g., trainWidth + dogWidth
        setTrackPaths()       // for trainPath + dogPath, etc.
        reSizeAndPositionNodes()

    }   // if (thisSceneName == "GameScene")
            
}   // viewDidLayoutSubviews

I chose it over:

#if os(iOS) && !targetEnvironment(macCatalyst)
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(rotated),
                                               name: UIDevice.orientationDidChangeNotification,
                                               object: nil)
#endif

@objc func rotated() {
                
        if (thisSceneName == "GameScene") {
            
            setGamePieceParms()   // for GamePieces, e.g., trainWidth + dogWidth
            setTrackPaths()       // for trainPath + dogPath, etc.
            reSizeAndPositionNodes()

        }   // if (thisSceneName == "GameScene")
                
    }   // rotated

Anyway, I've got a complete focused Project as .zip, so how do I get it to you since you've chosen to reply within this Forum?

Whoops ... you have to use a Game Controller to run the App

FWIW - this eventual App actually will never be finished. I will always be expanding it, for example:

(1) a track winding thru all sorts of hills (2) an animated waterfall (3) a hero galloping on a horse to rescue a damsel in distress

There'll be a version 1.1, 1.2, 1.3 etc.

I'm retired military and with the short term assignments (2 years between moving to somewhere else), I couldn't afford the time to set up an HO Train Set in my basement. So, this is the next best thing.

TO: Albert from DTS .. You have acknowledged receipt of my complete bare bones Project.

You keep asking for a simple Project, but I can honestly say that the Project you've received is bare bones and simple.

I have led you step-by-step through (1) loading of my SKSpriteNodes, (2) running the Train with a Game Controller and (3) while the Train is moving, using Xcode to rotate the SKScene. Following this 3 step process, I see how, for example, If I am looking at Portrait mode, I see the moving Train following the old Landscape path.

Like, while staring at Portrait mode, the Train disappears off the left edge and right edge of the UIDevice.

= the OLD path

In conclusion, I must very, very regretfully conclude that I must seek help elsewhere.

Bye.

In view of the fact that "something is better than nothing", I have decided (with rotation and the Train is moving) to:

(1) stop the movement

(2) delete the trackPath via

func deletePath(_ thePath:UIBezierPath) {
    
    var thePoints = thePath.cgPath.getPoints()
    thePoints.removeAll()
    
}   // deletePath

FWIW, getPoints() is an extension CGPath that I found elsewhere.

(3) call createTrainPath() which starts the new UIBezierPath from the original starting point

(4) finally call resumeGame() which calls startFollowTrainPath()

It is 100% absolutely certain that this is much less than I desire which is to replace (3) with a new UIBezierPath which starts where the Train was before I rotated the UIDevice

So, that challenge is still on the table ...

In the meantime, I need to attack the very, very last challenge which is summarized here:

https://stackoverflow.com/questions/77903234/why-does-orient-path-true-cause-the-sprite-node-to-rotate-cw-when-the-path-is

I reference stack overflow.com because they allow presenting .pngs in an effort to show what the problem is all about.

John Love

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