Using SpriteKit with SwiftUI
S.G. Chipman
Posted on January 1, 2020
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)
}
}
}
}
}
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
}
...
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(_:))
}
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
}
And with that, the user will see animated hearts dancing all over their screen after they've made a successful purchase:
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
Posted on January 1, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.