NEW! Pre-order my latest book, Testing Swift! >>

< Previous: Building the environment: SKTexture and filling a path   Next: Unleash the bananas: SpriteKit texture atlases >

Mixing UIKit and SpriteKit: UISlider and SKView

We've been mixing UIKit and SpriteKit ever since our first SpriteKit project, way back in project 11. Don't believe me? Look inside GameViewController.swift and you'll see a plain old UIViewController do all the work of loading and showing our GameScene code. There's a Main.storyboard file containing that view controller, and if you go to the identity inspector (Alt+Cmd+3) you'll see it has SKView set for its custom class – that's the view holding our scene.

This UIKit setup existed all along, but so far we've been ignoring it. No more: we're going to add some controls to that view so that players can fire bananas. The way the game works, each player gets to enter an angle and a velocity for their throw. We'll be recreating this with a UISlider for both of these numbers, along with a UILabel so players can see exactly what numbers they chose. We'll also add a "Launch" button that makes the magic happen.

Now, think about this for a moment: our game view controller needs to house and manage the user interface, and the game scene needs to manage everything inside the game. But they also need to talk to each other: the view controller needs to tell the game scene to fire a banana when the launch button is clicked, and the game scene needs to tell the view controller when a player's turn has finished so that another banana can be launched again.

This two-way communication could be done using NotificationCenter, but it's not very pleasant: we know the sender and receiver, and we know exactly what kind of data they will send and receive, so the easiest solution here is to give the view controller a property that holds the game scene, and give the game scene a property that holds the view controller.

When we discussed closures in project 5, I explained that you needed to make self either unowned or weak so that you avoided strong reference cycles – where a view controller owns a closure and the closure owns the view controller so that neither of them ever get destroyed. Well, with our game scene and game view controller have the same problem: if they both own each other using a property, we have a problem.

The solution is to make one of them have a weak reference to the other: either the game controller owns the game scene strongly, or the game scene owns the game controller strongly, but not both. As it so happens, the game controller already strongly owns the game scene, albeit indirectly: it owns the SKView inside itself, and the view owns the game scene. So, it's owned, we just don't have a reference to it.

So, our solution is straightforward: add a strong reference to the game scene inside the view controller, and add a weak reference to the view controller from the game scene. Add this property to the game scene:

weak var viewController: GameViewController!

Now add this property to the game view controller:

var currentGame: GameScene!

Like I said, the game controller already owns the game scene, but it's a pain to get to. Adding that property means we have direct access to the game scene whenever we need it. To set the property, put this into the viewDidLoad() method of the game view controller, just after the call to presentScene():

currentGame = scene as? GameScene
currentGame.viewController = self

The first line sets the property to the initial game scene so that we can start using it. The second line makes sure that the reverse is true so that the scene knows about the view controller too.

Now to design the user interface: this needs two sliders, each with two labels, plus a launch button and one more label that will show whose turn it is. When you open Main.storyboard you'll probably see that it's shaped like an iPhone, which isn’t helpful when designing this user interface. Instead, I’d like you to click the View As button at the bottom of Interface Builder, and select a 9.7-inch iPad in landscape orientation so that we have more space for drawing.

Drop two sliders into your layout, both 300 points wide. The first should be at X:20, the second should be at X:480, and both should be at Y:20. Now place two labels in there, both 120 points wide. The first should be at X:325, the second should be at X:785, and both should be at Y:24 – this is slightly lower than the sliders so that everything is centered neatly.

For the launch button, place a button at X:910 Y:13, with width 100 and height 44; for the "which player is it?" label, place a label at X:370 Y:64 with width 285 and height 35.

That's the basic layout, but to make it all perfect we need a few tweaks. Using the attributes inspector, change the left-hand slider so that it has a maximum value of 90 and a current value of 45, then change the right-hand slider so that it has a maximum value of 250 and a current value of 125.

Make sure all three of your labels have their text color set to white, then give the bottom one the text “<<< PLAYER ONE" and center alignment. Select the button then give it a system bold font of size 22, a title of "LAUNCH" and a red text color.

That's the layout all done, but we also need lots of outlets: using the assistant editor, create these outlets:

  • For the left slider: angleSlider
  • For the left label: angleLabel
  • For the right slider: velocitySlider
  • For the right label: velocityLabel
  • For the launch button: launchButton
  • For the player number: playerNumber

You'll also need to create actions from the left slider, the right slider and the button: angleChanged(), velocityChanged() and launch() respectively.

That's all the layout done, so we're finished with Interface Builder and you can open up GameViewController.swift.

We need to fill in three methods (angleChanged(), velocityChanged() and launch()), write one new method, then make two small changes to viewDidLoad().

The action methods for our two sliders are both simple: they update the correct label with the slider's current value. A UISlider always stores its values as a Float, but we only care about the integer value of that float so we're going to convert the values to Ints then use string interpolation to update the labels. Here's the code for both these methods:

@IBAction func angleChanged(_ sender: Any) {
    angleLabel.text = "Angle: \(Int(angleSlider.value))°"
}

@IBAction func velocityChanged(_ sender: Any) {
    velocityLabel.text = "Velocity: \(Int(velocitySlider.value))"
}

The only hard thing there is typing the ° symbol that represents degrees – to do that, press Shift+Alt+8. With those methods written, we need to call both of them inside viewDidLoad() in order to have them load up with their default values. Add this to viewDidLoad() just after the call to super:

angleChanged(angleSlider)
velocityChanged(velocitySlider)

You could easily have typed default values into Interface Builder, and sometimes it's helpful to do so in order to measure your layout correctly, but setting it in code means you have only one place that can set those values so it's easier to change later if needed.

When a player taps the launch button, we need to hide the user interface so they can't try to fire again until we're ready, then tell the game scene to launch a banana using the current angle and velocity. Our game will then proceed with physics calculations until the banana is destroyed or lost (i.e., off screen), at which point the game will tell the game controller to change players and continue.

The code for the launch() method is trivial, largely because the work of actually launching the banana is hidden behind a call to a launch() method that we'll add to the game scene shortly:

@IBAction func launch(_ sender: Any) {
    angleSlider.isHidden = true
    angleLabel.isHidden = true

    velocitySlider.isHidden = true
    velocityLabel.isHidden = true

    launchButton.isHidden = true

    currentGame.launch(angle: Int(angleSlider.value), velocity: Int(velocitySlider.value))
}

Finally, we're going to create a activatePlayer() method that will be called from the game scene when control should pass to the other player. This will just update the player label to say who is in control, then show all our controls again:

func activatePlayer(number: Int) {
    if number == 1 {
        playerNumber.text = "<<< PLAYER ONE"
    } else {
        playerNumber.text = "PLAYER TWO >>>"
    }

    angleSlider.isHidden = false
    angleLabel.isHidden = false

    velocitySlider.isHidden = false
    velocityLabel.isHidden = false

    launchButton.isHidden = false
}

To make your code compile, you need to add a launch() method to GameScene.swift. It doesn't need to be the real thing, but it does need to accept parameters for angle and velocity. Give it this code for now:

func launch(angle: Int, velocity: Int) {
}

Being able to mix UIKit and SpriteKit means game menus and other controls are a cinch.

Love Hacking with Swift?

Get all 40 projects in PDF and HTML: buy the Hacking with Swift book! It contains over 1300 pages of hands-on Swift coding, and will really help boost your iOS career

< Previous: Building the environment: SKTexture and filling a path   Next: Unleash the bananas: SpriteKit texture atlases >
MASTER SWIFT NOW
Buy Testing Swift Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let me know!

Average rating: 5.0/5

Click here to visit the Hacking with Swift store >>