Using SpriteKit with SwiftUI

sgchipman

S.G. Chipman

Posted on January 1, 2020

Using SpriteKit with SwiftUI

Background

About a month ago I decided to write a native iOS version of a website I'd launched in the Spring of 2019 that provides sports field closure status for the county I live in. The website is mobile friendly, of course, but I also wanted to provide additional functionality that my website doesn't, such as push notifications when the status of a favorite field changes and a "Today Widget" that gives users a quick glance at the status of their favorites.

Most importantly, it would satisfy my need for an excuse to learn SwiftUI.

The app will be free, but I thought giving users the ability to send me a few bucks if they've found the app useful couldn't hurt. If someone decided to do that, I wanted to be sure to show them a little love, too.

I've written a couple of games with SpriteKit, so I thought displaying a particle effect after a successful transaction would be fun, as well as a nice way to say "thanks!".

But how can we use SpriteKit with SwiftUI?

Enter UIViewRepresentable

SwiftUI provides a way to wrap UIKit and other framework components so they can be used in SwiftUI views via the UIViewRepresentable protocol. A common use case for this is wrapping UIActivityIndicatorView since SwiftUI doesn't provide it yet.

So, let's use UIViewRepresentable to make an SKView available to our SwiftUI view.

import SwiftUI
import SpriteKit

// EmitterView will be the name of our SwiftUI view.
struct EmitterView: UIViewRepresentable {
    /**
    makeUIView and updateUIView are required methods of the UIViewRepresentable
    protocol. makeUIView does just what you'd think - makes the view we want.

    updateUIView allows you to update the view with new data, but we don't need
    it for our purposes. 
    */
    func makeUIView(context: UIViewRepresentableContext<EmitterView>) -> SKView {
        // Create our SKView
        let view = SKView()
        // We want the view to animate the particle effect over our SwiftUI view
        // and let its components show through so we'll set allowsTransparenty to true.
        view.allowsTransparency = true
        // Load our custom SKScene
        if let scene = LoveScene(fileNamed: "LoveScene") {
            // We need to set the background to clear.
            scene.backgroundColor =  .clear
            view.presentScene(scene)
        }
        return view
    }

    func updateUIView(_ uiView: SKView, context: UIViewRepresentableContext<EmitterView>) {

    }

}

/**
This is our SKScene subclass that will present the emitter node.
*/
class LoveScene: SKScene {
    override func didMove(to view: SKView) {
       super.didMove(to: view)

       // Create our SKEmitterNode with a particle effect named "love"
       // More info about creating particle effects in Xcode is 
       // linked at the end of the article.
       if let emitter: SKEmitterNode = SKEmitterNode(fileNamed: "love") {
            // Set the initial alpha of the node to 0, as we're going to fade it in.
            emitter.alpha = 0
            // Add the emitter node to the scene
            addChild(emitter)
            // Fade the node in with a duration of half a second.
            emitter.run(SKAction.fadeIn(withDuration: 0.5)) {
                // Fade the node out with a duration of 5 seconds.
                emitter.run(SKAction.fadeOut(withDuration: 5.0)) {
                    // Clean up our emitter node.
                    emitter.removeFromParent()
                    // Tell our SwiftUI view that the animation has finished.
                    NotificationCenter.default.post(name: Utils.resetCoffeeNotification, object: nil)
                }
            }
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Using EmitterView in a SwiftUI View

In the MenuView of the app, there is an option to "Buy us a Cup of Coffee" that kicks off a $2.99 in-app-purchase request via StoreKit. When StoreKit informs us that the transaction was successful, we post a notification that the MenuView is listening for that updates an @State var named coffee to true.

When coffee is true, our EmmitterView is added to the view.

 @State private var coffee: Bool = false

 var body: some View {
        // We want the EmitterView to appear above the NavigationView
        // so we'll wrap them both in a ZStack
        ZStack {
            // coffee is a @State var, so the emitter view will only be rendered
            // to the view if its true.
            if coffee {
                EmitterView()
                    // We want the EmitterView to span the entire view, so we'll
                    // ignore safe areas.
                    .edgesIgnoringSafeArea(.all)
                    // The EmitterView needs to have a transparent background as well.
                    .background(Color.clear)
                    // Make sure we have a higher zIndex than the NavigationView
                    .zIndex(2)
                    // Disable the view so it doesn't eat taps while its visible.
                    .disabled(true)
            }
            NavigationView {
                VStack {
                    Form {
                        #if DEBUG
                        Section {
                            debug
                        }
                        #endif
                        Section {
                            mapPreference
                            notificationPreference
                            faq
                        }
                        ...
Enter fullscreen mode Exit fullscreen mode

As pointed out in a comment in the LoveScene class, we also post a notification when the SKEmitterNode has completed its five second fade out. MenuView is listening for that as well and will set coffee back to false when it's received.

To set that up, we'll add an onAppear handler on our NavigationView in the MenuView that adds the observers.

.onAppear {
   NotificationCenter.default.addObserver(forName: Utils.userDidBuyCoffeeNotification, object: nil, queue: nil, using: self.userDidBuyCoffee(_:))
   NotificationCenter.default.addObserver(forName: Utils.resetCoffeeNotification, object: nil, queue: nil, using: self.resetCoffee(_:))
}
Enter fullscreen mode Exit fullscreen mode

Those two methods we register with NotificationCenter simply set coffee to true or false.

private func userDidBuyCoffee(_ notification:Notification) {
    coffee = true
}

private func resetCoffee(_ notification:Notification) {
    coffee = false
}

Enter fullscreen mode Exit fullscreen mode

And with that, the user will see animated hearts dancing all over their screen after they've made a successful purchase:

A screenshot of the effect.

Conclusion

Pretty all right, yeah? The app will be launching in the next week or so - here's to hoping at least a couple of people see the particle effect in action!

Additional Resources

💖 💪 🙅 🚩
sgchipman
S.G. Chipman

Posted on January 1, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Using SpriteKit with SwiftUI
swiftui Using SpriteKit with SwiftUI

January 1, 2020