uibezierpath

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.

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?


Solution

  • 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.