9

So, Currently I have a game that i’ve made in sprite kit and have used this way of fitting everything to the screen size:

buyButton = SKSpriteNode(texture: SKTexture(imageNamed: "BuyButton"), color: .clear, size: CGSize(width: frame.maxX / 2.9, height: frame.maxY / 10))
buyButton.position = CGPoint(x:-frame.maxX + frame.midX*2, y: -frame.maxY + frame.midY*1.655)
addChild(buyButton)

as you can see it uses the frame to calculate the width and height along with the position of the node on the scene, this works with all screen sizes that I have been using from the 6s to the 8 Plus. but when it comes to the iPhone X there is a problem with it. it seems to stretch everything because of the screen size being bigger and oddly shaped as you can see below compared to the iPhone 8 Plus

I’ve been looking for solutions to this problem but none have really helped or just even make it worse and I couldn’t understand how to programmatically use the safe area layout in my sprite kit project like in this solution here

My Question is

How do I go about getting everything to fit in the iPhone X’s screen so that it fits and isn’t cutting off the score labels and stuff I have in the top right corners?

EDIT 2:

itemDescriptionLabel = UILabel(frame: CGRect(x: frame.maxX - 150, y: frame.maxY - 130 , width: frame.maxX / 0.7, height: frame.maxY / 3.5))
itemDescriptionLabel.font = UIFont(name: "SFCompactRounded-Light", size: 17)
itemDescriptionLabel.text = "This purchase stops all the popup ads that happen after a certain amount of time playing the game from showing up."
itemDescriptionLabel.numberOfLines = 4
itemDescriptionLabel.adjustsFontSizeToFitWidth = true
self.scene?.view?.addSubview(itemDescriptionLabel)

EDIT 3

enter image description here

EDIT 4

let safeAreaInsets = yourSpriteKitView.safeAreaInsets;
buyButton.position = CGPoint(x:safeAreaInsets.left - frame.maxX + frame.midX*2, y: safeAreaInsets.top - frame.maxY + frame.midY*1.655)

EDIT 5

screenWidth = self.view!.bounds.width
screenHeight = self.view!.bounds.height

coinScoreLabel = Score(num: 0,color: UIColor(red:1.0, green: 1.0, blue: 0.0, alpha: 1), size: 50, useFont: "SFCompactRounded-Heavy" )  // UIColor(red:1.00, green:0.81, blue:0.07, alpha:1.0)
coinScoreLabel.name = "coinScoreLabel"
coinScoreLabel.horizontalAlignmentMode = .right
//coinScoreLabel.verticalAlignmentMode = .top
coinScoreLabel.position = CGPoint(x: screenWidth / 2.02, y: inset.top - screenHeight / 2.02)
coinScoreLabel.zPosition = 750
addChild(coinScoreLabel)

I tried it on another SpriteNode that I have such as this one below, which it worked for I have no idea why the yellow label is doing this.

 playButton = SKSpriteNode(texture: SKTexture(imageNamed: "GameOverMenuPlayButton"), color: .clear, size: CGSize(width: 120, height: 65))
 playButton.position = CGPoint(x: inset.right - frame.midX, y: inset.bottom + frame.minY + playButton.size.height / 1.7)
 playButton.zPosition = 1000
 deathMenuNode.addChild(playButton) 

It came out like this which is perfect:

enter image description here

Astrum
  • 375
  • 6
  • 21
  • I think you should follow the official guide lines about safearea instead of try to add, multiply, subtract or divide values accordling with your actual device. Suppose a new device coming out the next months like for example iPhone 11 with a new safearea dimensions: shall you re-calculate all objects positions? – Alessandro Ornano Jan 23 '18 at 07:23

2 Answers2

9

Interesting question. The problem borns when we try to "transform" our view to SKView to create our game scene. Essentially, we could intercept that moment (using viewWillLayoutSubviews instead of viewDidLoad) and transform the view frame accordling with the safeAreaLayoutGuide layout frame property.

Some code as example:

GameViewController:

class GameViewController: UIViewController {
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        if #available(iOS 11.0, *), let view = self.view {
           view.frame = self.view.safeAreaLayoutGuide.layoutFrame
        }  
        guard let view = self.view as! SKView? else { return }
        view.ignoresSiblingOrder = true
        view.showsFPS = true
        view.showsNodeCount = true
        view.showsPhysics = true
        view.showsDrawCount = true
        let scene = GameScene(size:view.bounds.size)
        scene.scaleMode = .aspectFill
        view.presentScene(scene)
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        print("---")
        print("∙ \(type(of: self))")
        print("---")
    }
}

GameScene:

class GameScene: SKScene {
    override func didMove(to view: SKView) {
        print("---")
        print("∙ \(type(of: self))")
        print("---")
        let labLeftTop = SKLabelNode.init(text: "LEFTTOP")
        labLeftTop.horizontalAlignmentMode = .left
        labLeftTop.verticalAlignmentMode = .top
        let labRightTop = SKLabelNode.init(text: "RIGHTTOP")
        labRightTop.horizontalAlignmentMode = .right
        labRightTop.verticalAlignmentMode = .top
        let labLeftBottom = SKLabelNode.init(text: "LEFTBTM")
        labLeftBottom.horizontalAlignmentMode = .left
        labLeftBottom.verticalAlignmentMode = .bottom
        let labRightBottom = SKLabelNode.init(text: "RIGHTBTM")
        labRightBottom.horizontalAlignmentMode = .right
        labRightBottom.verticalAlignmentMode = .bottom
        self.addChild(labLeftTop)
        self.addChild(labRightTop)
        self.addChild(labLeftBottom)
        self.addChild(labRightBottom)
        labLeftTop.position = CGPoint(x:0,y:self.frame.height)
        labRightTop.position = CGPoint(x:self.frame.width,y:self.frame.height)
        labLeftBottom.position = CGPoint(x:0,y:0)
        labRightBottom.position = CGPoint(x:self.frame.width,y:0)
    }
}

Other way to do:

Another way to obtain just only the differences between the safe area layout frame and the current view frame without passing through viewWillLayoutSubviews for launching our scene, could be using protocols.

P.S.: Down below, I've report a reference image where you can see the different sizes between:

  • the main screen height (our view height)
  • safe area
  • status bar height (on the top, 44 px for the iPhone X)

GameViewController:

protocol LayoutSubviewDelegate: class {
    func safeAreaUpdated()
}
class GameViewController: UIViewController {
    weak var layoutSubviewDelegate:LayoutSubviewDelegate?
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        if let _ = self.view {
           layoutSubviewDelegate?.safeAreaUpdated()
        }
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        print("---")
        print("∙ \(type(of: self))")
        print("---")
        guard let view = self.view as! SKView? else { return }
        view.ignoresSiblingOrder = true
        view.showsFPS = true
        view.showsNodeCount = true
        view.showsPhysics = true
        view.showsDrawCount = true
        let scene = GameScene(size:view.bounds.size)
        scene.scaleMode = .aspectFill
        view.presentScene(scene)
    }
}

GameScene:

class GameScene: SKScene,LayoutSubviewDelegate {
    var labLeftTop:SKLabelNode!
    var labRightTop:SKLabelNode!
    var labLeftBottom:SKLabelNode!
    var labRightBottom:SKLabelNode!
    func safeAreaUpdated() {
        if let view = self.view {
            let top = view.bounds.height-(view.bounds.height-view.safeAreaLayoutGuide.layoutFrame.height)+UIApplication.shared.statusBarFrame.height
            let bottom = view.bounds.height - view.safeAreaLayoutGuide.layoutFrame.height-UIApplication.shared.statusBarFrame.height
            refreshPositions(top: top, bottom: bottom)
        }
    }
    override func didMove(to view: SKView) {
        print("---")
        print("∙ \(type(of: self))")
        print("---")
        if let view = self.view, let controller = view.next, controller is GameViewController {
            (controller as! GameViewController).layoutSubviewDelegate = self
        }
        labLeftTop = SKLabelNode.init(text: "LEFTTOP")
        labLeftTop.horizontalAlignmentMode = .left
        labLeftTop.verticalAlignmentMode = .top
        labRightTop = SKLabelNode.init(text: "RIGHTTOP")
        labRightTop.horizontalAlignmentMode = .right
        labRightTop.verticalAlignmentMode = .top
        labLeftBottom = SKLabelNode.init(text: "LEFTBTM")
        labLeftBottom.horizontalAlignmentMode = .left
        labLeftBottom.verticalAlignmentMode = .bottom
        labRightBottom = SKLabelNode.init(text: "RIGHTBTM")
        labRightBottom.horizontalAlignmentMode = .right
        labRightBottom.verticalAlignmentMode = .bottom
        self.addChild(labLeftTop)
        self.addChild(labRightTop)
        self.addChild(labLeftBottom)
        self.addChild(labRightBottom)
        labLeftTop.position = CGPoint(x:0,y:self.frame.height)
        labRightTop.position = CGPoint(x:self.frame.width,y:self.frame.height)
        labLeftBottom.position = CGPoint(x:0,y:0)
        labRightBottom.position = CGPoint(x:self.frame.width,y:0)
    }
    func refreshPositions(top:CGFloat,bottom:CGFloat){
        labLeftTop.position = CGPoint(x:0,y:top)
        labRightTop.position = CGPoint(x:self.frame.width,y:top)
        labLeftBottom.position = CGPoint(x:0,y:bottom)
        labRightBottom.position = CGPoint(x:self.frame.width,y:bottom)
    }
}

Output:

enter image description here

Subclassing methods:

Another way to get the correct safe area dimensions without touch your current classes code, could be made using some subclass, so for our GameViewController we set it as GameController and for GameScene we can set it as GenericScene like this example:

GameViewController:

class GameViewController: GameController {
    override func viewDidLoad() {
        super.viewDidLoad()
        guard let view = self.view as! SKView? else { return }
        view.ignoresSiblingOrder = true
        let scene = GameScene(size:view.bounds.size)
        scene.scaleMode = .aspectFill
        view.presentScene(scene)
    }
}
protocol LayoutSubviewDelegate: class {
    func safeAreaUpdated()
}
class GameController:UIViewController {
    weak var layoutSubviewDelegate:LayoutSubviewDelegate?
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        if let _ = self.view {
            layoutSubviewDelegate?.safeAreaUpdated()
        }
    }
}

GameScene:

class GameScene: GenericScene {
    override func safeAreaUpdated() {
        super.safeAreaUpdated()
        if let view = self.view {
            let insets = view.safeAreaInsets
            // launch your code to update positions here
        }
    }
    override func didMove(to view: SKView) {
        super.didMove(to: view)
        // your stuff
    }
}
class GenericScene: SKScene, LayoutSubviewDelegate {
    func safeAreaUpdated(){}
    override func didMove(to view: SKView) {
        if let view = self.view, let controller = view.next, controller is GameViewController {
            (controller as! GameViewController).layoutSubviewDelegate = self
        }
    }
}

References:

enter image description here

Alessandro Ornano
  • 34,887
  • 11
  • 106
  • 133
  • Hi, Thanks for you reply, This has problems, firstly when I click the ads button in the top right of the screen Which brings up a menu that contains a UIlabel that makes a subview it doesn't show the rest of the menu and instead decreases the size of the screen as seen in edit 2: I have provided the code of the UILabel too. – Astrum Jan 20 '18 at 10:48
  • This problem also persists through the other menu screens that i try to access in the game which doesn't work well or doesn't even show them. – Astrum Jan 20 '18 at 10:54
  • Your issue could happened if your ads SDK is not up to date or don’t respect the safe area, are you sure this one compliant with iOS 11 and iPhoneX ? – Alessandro Ornano Jan 20 '18 at 10:54
  • Yeah the ad button doesn't actually show an ad it just shows a menu where people can purchase the no ads part of the game. Also my ads SDKs are updated to the latest version as of yesterday – Astrum Jan 20 '18 at 10:56
  • Could you say to me what is this SDK? Have you a web link to reach it? – Alessandro Ornano Jan 20 '18 at 10:59
  • Its not an SDK or even an Advert thats the problem, I think its the UILabel making a subview thats the problem. like when you in the solution above override the viewWillLayoutSubviews function is what causes a conflict. please see EDIT 2 in the original post – Astrum Jan 20 '18 at 11:03
  • The `viewWillLayoutSubviews` method is called **after** `viewDidLoad`, you can test it also using breakpoints on both methods. So, if `viewDidLoad` is already started, all subviews could be present and showed on screen. I think you just simple intercept this/these views and change it's/their position according with the safe area. I can help you if you show more code.. – Alessandro Ornano Jan 20 '18 at 11:12
  • Is there anyway i can not have black bars on the screen and just adjust my nodes such as the labels for the safe area? without overriding the function in the view controller (could you take a look at Clafou's solution to this and tell me what you think.) Because i don't really want black bars on the iPhone screen if i can help it. – Astrum Jan 20 '18 at 11:19
  • Have you follow also the instructions about the black bars showed for example in [this](https://stackoverflow.com/a/46184998/1894067) answer? Take a look also to [this](https://github.com/mesmotronic/air-ios-launch-images) gitHub link with all latest iOS launch images... Let me know your results. – Alessandro Ornano Jan 20 '18 at 11:36
  • Hi, Thanks for your help with this, I took the exact code for the other solution and copied it straight into a new swift project that is fully updated and ready for ios 11 and it didn't come out like in your screenshot but it came out like in edit 3 in my original post.(i think it has something to do with the navigation part) Also is it possible to do edit 4 and just get the spritekit's view safe area insets instead of writing out all that code in the view controller and the game scene, if so what would I put as "yourSpritekitView" part? (as seen in edit 4) – Astrum Jan 21 '18 at 00:57
  • Hi Astrum, [here](https://github.com/aornano/iPhoneXSpriteKit) you can find the **gitHub** page of my sources. About your "edit 4" you must always use protocols because you've launched your scene from `viewDidLoad`, so the scene was built in a phase where we don't know yet the correct safe area dimension. With my protocol you can do, inside the `safeAreaUpdated` doing also: `if let view = self.view { let safeAreaInsets = view.safeAreaInsets` } – Alessandro Ornano Jan 21 '18 at 08:18
  • I've added also a third way to made it: through subclassing. Choose the more confortable method for you, probably this last one can permit to you to leave intact your code just adding other code down below your classes... – Alessandro Ornano Jan 21 '18 at 09:16
3

You can get margins through your view's safeAreaInsets. The insets will be zero on most devices, but non-zero on the iPhone X.

For example replace your line with this:

let safeAreaInsets = yourSpriteKitView.safeAreaInsets;
buyButton.position = CGPoint(x:safeAreaInsets.left - frame.maxX + frame.midX*2, y: safeAreaInsets.top - frame.maxY + frame.midY*1.655)
Clafou
  • 15,250
  • 7
  • 58
  • 89
  • I tried this but it gives me an error, I don't know what you mean by the yourSpriteKitView part (what do I put in that spot?) – Astrum Jan 20 '18 at 10:21
  • I don't know where your code is, but if it's in a view controller then you can use `self.view` in place of that `yourSpriteKitView`. Basically any UIView that is presented should do. – Clafou Jan 21 '18 at 10:37
  • That seems to work well except every time I want to do it I have to define the if #available(iOS 11.0, *) { thing then i have to have that line in the function and to tell you the truth i don't really want to do that for all my sprite nodes that i have on the screen. anyway around this? – Astrum Jan 21 '18 at 12:56
  • The iPhone X can only run iOS 11 or later so you are safe to use insets of zero for iOS < 11. You can even use a computed var like this: private var inset: UIEdgeInsets { if #available(iOS 11.0, *) { return self.view.safeAreaInsets } return .zero } – Clafou Jan 21 '18 at 14:26
  • ok so i'm using this the variable described in my GameScene will have a go at using it and get back to you soon – Astrum Jan 21 '18 at 14:50
  • Hi, I tried it out and it seemed to have work for one of my play button sprite nodes but not for my scoring label. could you take a look at EDIT 5 please thanks. – Astrum Jan 22 '18 at 14:02
  • It only partially worked though and isn’t working for my scoring label for some reason could take a look at edit 5 thanks I have upped your answer – Astrum Jan 22 '18 at 14:10
  • It all sounds a bit confused, I'd advise you try and work things out on paper and write your code accordingly. Dividing the screen height by 2.02 is weird. maybe you should add the height, then subtract the half thee height of the sprite plus some margin or something. Good luck anyway. – Clafou Jan 22 '18 at 14:53
  • Hi, thanks for your help this is working for the bottom part of the screen as i tested it in a new project. the problem now is that the top part of the screen doesn't seem to want to do the same thing. could you help me with this please i have posted another question about it here: https://stackoverflow.com/questions/48483786/spritekit-safearealayoutguide-not-finding-top – Astrum Jan 28 '18 at 06:30