Creating an Animated Game Dialogue Box Using SwiftUI

Vitória Beltrão
Apple Developer Academy | UFPE
4 min readJun 15, 2024

While working on my second app at the Apple Developer Academy | UFPE, a horror game without sound, I realized that paying attention to the small details of the interface can greatly enhance the player’s immersive experience.

First of all, I would like to say that this project was inspired by this another project.

Thereby, with the intention of sharing my newfound knowledge in programming, I’m here to guide you through the process of creating game dialogue boxes with engaging animations, such as text with a typing effect and a “next” arrow with a moving effect, as shown below:

Creating the Typing Effect Function

Assuming your project file is already created and the SwiftUI framework is imported, let’s delve into our first animation: the typing effect. We’ll begin by creating a function called showText() and setting up the following attributes:

1º — displayedText = ""

This resets the text being displayed in the dialogue box to an empty string, initiating the typing animation from the beginning.

2º — currentIndex = 0

This resets the current index used to track the position in fullText from where the next letter will be added to displayedText.

3º — showArrow = false

This hides the “next” arrow at the start of the typing animation, ensuring that the arrow only appears after the entire text has been displayed.

func showText() {
displayedText = ""
currentIndex = 0
showArrow = false

Timer.scheduledTimer(withTimeInterval: 0.03, repeats: true) { timer in
if currentIndex < fullText.count {
let index = fullText.index(fullText.startIndex, offsetBy: currentIndex)
displayedText.append(fullText[index])
currentIndex += 1
} else {
timer.invalidate()
showArrow = true
}
}
}

Next, let’s set a Timer and define the logic:

4º — withTimeInterval: 0.03, repeats: true

This specifies the speed between each letter and indicates that the animation must repeat until the sentence is completed.

5º — { timer in ... }

This closure defines the logic for the animation.

6º —if currentIndex < fullText.count {

This checks if there are still letters to appear.

7º — let index = fullText.index(fullText.startIndex, offsetBy: currentIndex)

This calculates the index of the next letter.

8º — displayedText.append(fullText[index])

This adds the next letter to displayedText.

9º — currentIndex += 1

This advances the current index to the next one.

10º — else { timer.invalidate() showArrow = true }

When the entire sentence is complete, the timer stops, and the “next” arrow appears.

Creating the Moving Arrow Effect Function

Now, let’s create the arrow animation function called animateArrow(). This animation continuously moves the arrow, suggesting to the player that there is another dialogue or scene continuation.

func animateArrow() {
withAnimation(Animation.linear(duration: 0.5).repeatForever(autoreverses: true)) {
arrowOffset = 10
}
}

1º — withAnimation(Animation.linear(duration: 0.5)

This sets a linear animation with a duration of 0.5 seconds in a single movement.

2º — .repeatForever(autoreverses: true)

This makes the animation repeat indefinitely, causing the arrow to return to its original position at the end of each cycle.

3º — arrowOffset = 10

This sets the arrow’s displacement value.

Creating the Dialogue Box

To complete the code, let’s create a struct called DialogueBox where we'll define the base of the box, place the texts, and add the arrow. First, let's set up our variables:

@State private var displayedText: String = "" 
@State private var currentIndex: Int = 0
@State private var showArrow: Bool = false
@State private var arrowOffset: CGFloat = 0
var fullText: String

Now, let’s create a ZStack where we'll visualize our dialogue box, styling a Rectangle() as the base:

ZStack {
Rectangle()
.fill(Color.black.opacity(0.75))
.border(Color.white, width: 3)
.frame(width: 353, height: 128)
}

I’ve applied one modifier to fill the background, another to define the border, and a final one to set the width and height of the box. Feel free to customize it according to your preferences.

Within the ZStack, let's create a VStack to accommodate our text and the arrow:

 VStack(alignment: .leading) {

Text(displayedText)
.font(Font.custom("Kadwa-Regular", size: 10))
.foregroundColor(.white)
.padding(.leading, 14)
.padding(.top, 14)
.frame(maxWidth: .infinity, alignment: .leading)
.onAppear {
showText()
}

Spacer()

if showArrow {

Image(systemName: "arrowtriangle.right.fill")
.foregroundColor(.white)
.offset(x: arrowOffset)
.frame(height: 20)
.padding(.leading, 310)
.padding(.bottom, 12)
.onAppear {
animateArrow()
}
}
}
.frame(width: 353, height: 128)

After customizing both the Text and Image views, we connect our variables and functions to them. I've placed displayedText inside the Text view and called both previously created functions within the .onAppear.

To use our game dialogue box, simply wrapped those code in a swiftUI view struct, witch I named GameSceneView, and call it like this in your app:

GameSceneView(fullText: "Oh no! I missed the last train...")

By following these steps, you’ll be able to provide your users with a more immersive game experience!

(Disclaimer: This code doesn’t handle the dialogue box actions yet, a.k.a navigation)

I hope this tutorial proves helpful! For more conversations and exchange of ideas, my LinkedIn is available! See you next time. ☻

--

--

Vitória Beltrão
Apple Developer Academy | UFPE

Design undergrad, iOS Development Internship | Exploring what’s new day after day :)