Typewriter Effect in SwiftUI

Add delays to text and bring about the animation

Ix76y
Better Programming

--

Photo by Markus Winkler on Unsplash

Prerequisites

  • Mac with Xcode 13+ installed
  • Experience in Swift programming is not necessary but can make your life easier

Create a New Project

Open up Xcode and create a new App project. If you are new to Xcode and app development you can follow the detailed steps I made in one of my other articles, like “Creating an Image Card in SwiftUI”. This article also contains a small section about the interface of Xcode to get you started.

Create the UI

The UI for this example will be very simple to show you the concept. We will have one Text that will be empty at the start until we click the button below it, which will trigger the type writer effect and writes “Hello, World!”.

Text field above a button
UI with Text and Button

When you create a new view or work directly in the ContentView you should already have a Text in the body of the view:

var body: some View {
Text("Hello, World!")
}

Press ⌘ and click on Text, this will open up the action menu where we can select to embed the Text inside a vertical stack.

Press Command and left click to open action menu and select Embed in VStack
Action Menu to embed Text in VStack

Next, open up the view library (click the small + icon in the top right corner) and drag the Button below the text in the VStack. Your body should now look like this:

var body: some View {
VStack {
Text("Hello, World!")
Button("Button") {
Action
}
}
}

Let’s change the text of the Button from “Button” to “Type” and let’s add a little bit of spacing between the two elements. You can change the spacing of a VStack by clicking on it and then changing the spacing in the attribute inspector. For this one, I choose a value of 16.

For the action of the button, we will write a small function that handles the typewriter effect. Right now we will call it typeWriter(). This is how our UI code will look in the end:

var body: some View {
VStack(spacing: 16.0) {
Text("Hello, World!")
Button("Type") {
typeWriter()
}
}
}

Xcode will now complain that the function typeWriter() is not defined. So let’s write our typewriter logic!

Type Writer

First, we need the main construct of the function that will hold our code. Once we add this after the closing bracket of our body view Xcode should already stop complaining that typeWriter() is not defined.

func typeWriter() {
// some code here
}

Before writing any code we should think about the actual logic we would like to implement. In general, we want a function that does:

  1. Write one letter at position x
  2. Waits for y amount of time
  3. Start from the beginning with x + 1

To do this we need two variables. One that is holding the final text and the other one where we can write each letter to:

// This will be empty at the start and we will add each letter to it
@State var text: String = ""
// This is the final text that we want to show
let finalText: String = "Hello, World!"

We declared the text as a variable String as it will change. Additionally, we add a @State at the beginning. This means if the text changes, it will automatically update all UI elements that are connected to this variable. The finalText is declared as let as we won’t change this and is also a String.

To bind the text variable to our Text View we can just pass it to the view:

@State var text: String = ""
let finalText: String = "Hello, World!"

var body: some View {
VStack(spacing: 16.0) {
Text(text)
Button("Type") {
typeWriter()
}

}
}

Now back to our actually TypeWriter() function. We know that we need a loop in it, that iterates over each character from our finalText. Generally, there are two ways of writing repetitive functions: recursive and iterative.

Recursive vs. Iterative Functions

Iterative functions use loops like while or for loops. Recursive functions, on the other hand, call themselves and don’t use any loops. Recursive functions often require less code but can be harder to understand. In this case, we will write the code in a recursive matter but either function can be used.

We want to print the character at a specific position, so let’s pass the position as an argument to our function and check if that position is lower than our finalText length:

// passing the position as an argument with a default value of 0 for the first value
func typeWriter(at position: Int = 0) {
if position < finalText.count {
// write the letter at position "position" to "text"
}
}

Next, we want to get the letter at that position and append it to our text variable.

func typeWriter(at position: Int = 0) {
if position < finalText.count {
// get the character from finalText and append it to text
text.append(finalText[position])
// call this function again with the character at the next position
typeWriter(at: position + 1)
}
}

Unicode Encoding

The only thing that seems to be missing is the time delay but Xcode will likely complain about the above code snippet. The problem is the finalText[position]. The reason is due to the fact that Swift's native String type is built from Unicode Scalar Values. In short, the error we get here is caused by the fact that one and the same human-readable character can be produced by one or more different Unicode values. If you want to read more about this check out the official Swift Documentation.

Luckily we can very easily fix this by just telling Swift how we want to interpret the string subscription with an extension which is a shortcut for us to get a character at a specific position:

extension String {
subscript(offset: Int) -> Character {
self[index(startIndex, offsetBy: offset)]
}
}

Time Delays

Now that our code doesn’t throw any errors anymore, let’s work finally on the delay part that will make the actual typewriter effect.

Swift overs a very simple a neat way for us to run code with a time delay without blocking the UI. This is important as we don’t want the UI to freeze while the text is typed.

func typeWriter(at position: Int = 0) {
if position < finalText.count {
// Run the code inside the DispatchQueue after 0.2s
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
text.append(finalText[position])
typeWriter(at: position + 1)
}
}
}

If you want to make it faster or slower just change the 0.2 to a smaller or bigger value. Time to try our code:

GIF showing type writer effect for Hello, World!
Type Writer effect.

Final Touches

The code works just fine but you might notice that if you press the button another time it will just append the same text again, making something like “Hello, World! Hello, World!”. We can adjust that really easily but adding a simple if statement that checks if the position is zero in which case the text will be set to an empty string again.

Final Code

With that our final code should look like this.

struct TypeWriterView: View {
@State var text: String = ""
let finalText: String = "Hello, World!"

var body: some View {
VStack(spacing: 16.0) {
Text(text)
Button("Type") {
typeWriter()
}
}
}


func typeWriter(at position: Int = 0) {
if position == 0 {
text = ""
}
if position < finalText.count {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
text.append(finalText[position])
typeWriter(at: position + 1)
}
}
}
}


struct TypeWriterView_Previews: PreviewProvider {
static var previews: some View {
TypeWriterView()
}
}

extension String {
subscript(offset: Int) -> Character {
self[index(startIndex, offsetBy: offset)]
}
}

I hope this tutorial was helpful! If you have any questions let me know. Thanks for reading.

--

--

Hi my name is Izzy. I love to write code, currently my favorits are Python, Swift, Rust & C and I enjoy writing tutorials about all sorts of things 😊.